diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 0000000..afd1154 --- /dev/null +++ b/src/config/constants.ts @@ -0,0 +1,31 @@ +export const APP_CONFIG = { + // --- Cryptographie & Limites --- + U32_MAX: 4294967295, + EMPTY_32_BYTES: String('').padStart(64, '0'), + + // --- Économie --- + DEFAULT_AMOUNT: 1000n, + FEE_RATE: 1, // Sat/vByte ou unité arbitraire selon le SDK + + // --- Délais & Timeouts (ms) --- + TIMEOUTS: { + POLLING_INTERVAL: 100, // Vérification rapide (ex: handshake) + API_DELAY: 500, // Petit délai pour laisser respirer le réseau (hack) + RETRY_DELAY: 1000, // Délai avant de réessayer une action + FAUCET_WAIT: 2000, // Attente après appel faucet + WORKER_CHECK: 5000, // Vérification périodique du worker + HANDSHAKE: 10000, // Timeout max pour le handshake + KEY_REQUEST: 15000, // Timeout pour recevoir une clé d'un pair + WS_RECONNECT_MAX: 30000, // Délai max entre deux tentatives de reco WS + WS_HEARTBEAT: 30000, // Ping WebSocket + }, + + // --- URLs (Environnement) --- + URLS: { + BASE: import.meta.env.VITE_BASEURL || 'http://localhost', + BOOTSTRAP: [import.meta.env.VITE_BOOTSTRAPURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8090`], + STORAGE: import.meta.env.VITE_STORAGEURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8081`, + BLINDBIT: import.meta.env.VITE_BLINDBITURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8000`, + }, +}; + diff --git a/src/services/core/network.service.ts b/src/services/core/network.service.ts new file mode 100644 index 0000000..5ed0aff --- /dev/null +++ b/src/services/core/network.service.ts @@ -0,0 +1,85 @@ +import { initWebsocket, sendMessage } from '../websockets.service.ts'; +import { AnkFlag } from '../../../pkg/sdk_client'; + +export class NetworkService { + private relayAddresses: { [wsurl: string]: string } = {}; + private relayReadyResolver: (() => void) | null = null; + private relayReadyPromise: Promise | null = null; + + constructor(private bootstrapUrls: string[]) {} + + 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); + } + } + } + + // --- AJOUT --- + public async addWebsocketConnection(url: string): Promise { + console.log(`[Network] 🔌 Connexion à: ${url}`); + await initWebsocket(url); + } + // ------------- + + public initRelays() { + for (const wsurl of this.bootstrapUrls) { + this.updateRelay(wsurl, ''); + } + } + + 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; + } + } + + public getAllRelays() { + return this.relayAddresses; + } + + public sendMessage(flag: AnkFlag, message: string) { + sendMessage(flag, message); + } +} diff --git a/src/services/core/sdk.service.ts b/src/services/core/sdk.service.ts new file mode 100644 index 0000000..0a0949f --- /dev/null +++ b/src/services/core/sdk.service.ts @@ -0,0 +1,26 @@ +import { ApiReturn, Device } from '../../../pkg/sdk_client'; + +export class SdkService { + private client: any; + + async init() { + this.client = await import('../../../pkg/sdk_client'); + this.client.setup(); + } + + public getClient(): any { + if (!this.client) throw new Error('SDK not initialized'); + return this.client; + } + + // Méthodes utilitaires directes du SDK + public encodeJson(data: any): any { + return this.client.encode_json(data); + } + public encodeBinary(data: any): any { + return this.client.encode_binary(data); + } + public decodeValue(value: number[]): any { + return this.client.decode_value(value); + } +} diff --git a/src/services/database.service.ts b/src/services/database.service.ts index b689ee8..49e0785 100755 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -1,4 +1,5 @@ import Services from './service'; +import { APP_CONFIG } from '../config/constants'; export class Database { private static instance: Database; @@ -161,7 +162,7 @@ export class Database { if (payload && payload.length != 0) { activeWorker?.postMessage({ type: 'SCAN', payload }); } - }, 5000); + }, APP_CONFIG.TIMEOUTS.WORKER_CHECK); } catch (error) { console.error('[Database] 💥 Erreur critique Service Worker:', error); } diff --git a/src/services/domain/crypto.service.ts b/src/services/domain/crypto.service.ts new file mode 100644 index 0000000..d2db8de --- /dev/null +++ b/src/services/domain/crypto.service.ts @@ -0,0 +1,58 @@ +import { MerkleProofResult, ProcessState } from '../../../pkg/sdk_client'; +import { SdkService } from '../core/sdk.service'; + +export class CryptoService { + constructor(private sdk: SdkService) {} + + public hexToBlob(hexString: string): Blob { + const uint8Array = this.hexToUInt8Array(hexString); + return new Blob([uint8Array], { type: 'application/octet-stream' }); + } + + public hexToUInt8Array(hexString: string): Uint8Array { + if (hexString.length % 2 !== 0) throw new Error('Invalid hex string'); + const uint8Array = new Uint8Array(hexString.length / 2); + for (let i = 0; i < hexString.length; i += 2) { + uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16); + } + return uint8Array; + } + + public async blobToHex(blob: Blob): Promise { + const buffer = await blob.arrayBuffer(); + const bytes = new Uint8Array(buffer); + return Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); + } + + public getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): string { + return this.sdk.getClient().hash_value(fileBlob, commitedIn, label); + } + + public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult { + return this.sdk.getClient().get_merkle_proof(processState, attributeName); + } + + public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean { + return this.sdk.getClient().validate_merkle_proof(proof, hash); + } + + public splitData(obj: Record) { + const jsonCompatibleData: Record = {}; + const binaryData: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + if (this.isFileBlob(value)) { + binaryData[key] = value; + } else { + jsonCompatibleData[key] = value; + } + } + return { jsonCompatibleData, binaryData }; + } + + private isFileBlob(value: any): value is { type: string; data: Uint8Array } { + return typeof value === 'object' && value !== null && typeof value.type === 'string' && value.data instanceof Uint8Array; + } +} diff --git a/src/services/domain/process.service.ts b/src/services/domain/process.service.ts new file mode 100644 index 0000000..f84e6f1 --- /dev/null +++ b/src/services/domain/process.service.ts @@ -0,0 +1,100 @@ +import { Process, ProcessState, RoleDefinition } from '../../../pkg/sdk_client'; +import { SdkService } from '../core/sdk.service'; +import Database from '../database.service'; + +const EMPTY32BYTES = String('').padStart(64, '0'); + +export class ProcessService { + private processesCache: Record = {}; + private myProcesses: Set = new Set(); + + constructor(private sdk: SdkService) {} + + public async getProcess(processId: string): Promise { + if (this.processesCache[processId]) return this.processesCache[processId]; + + const db = await Database.getInstance(); + const process = await db.getObject('processes', processId); + if (process) this.processesCache[processId] = process; + return process; + } + + public async getProcesses(): Promise> { + if (Object.keys(this.processesCache).length > 0) return this.processesCache; + + const db = await Database.getInstance(); + this.processesCache = await db.dumpStore('processes'); + return this.processesCache; + } + + public async saveProcessToDb(processId: string, process: Process) { + const db = await Database.getInstance(); + await db.addObject({ storeName: 'processes', object: process, key: processId }); + this.processesCache[processId] = process; + } + + public async batchSaveProcesses(processes: Record) { + if (Object.keys(processes).length === 0) return; + const db = await Database.getInstance(); + await db.batchWriting({ storeName: 'processes', objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) }); + this.processesCache = { ...this.processesCache, ...processes }; + } + + public getLastCommitedState(process: Process): ProcessState | null { + if (process.states.length === 0) return null; + const processTip = process.states[process.states.length - 1].commited_in; + return process.states.findLast((state) => state.commited_in !== processTip) || null; + } + + public getUncommitedStates(process: Process): ProcessState[] { + if (process.states.length === 0) return []; + const processTip = process.states[process.states.length - 1].commited_in; + return process.states.filter((state) => state.commited_in === processTip).filter((state) => state.state_id !== EMPTY32BYTES); + } + + public getStateFromId(process: Process, stateId: string): ProcessState | null { + return process.states.find((state) => state.state_id === stateId) || null; + } + + public getRoles(process: Process): Record | null { + const last = this.getLastCommitedState(process); + if (last?.roles && Object.keys(last.roles).length > 0) return last.roles; + + const first = process.states[0]; + if (first?.roles && Object.keys(first.roles).length > 0) return first.roles; + + return null; + } + + public rolesContainsMember(roles: Record, memberId: string): boolean { + return Object.values(roles).some((role) => role.members.includes(memberId)); + } + + public async getMyProcesses(pairingProcessId: string): Promise { + const processes = await this.getProcesses(); + const newMyProcesses = new Set(this.myProcesses); + if (pairingProcessId) newMyProcesses.add(pairingProcessId); + + for (const [processId, process] of Object.entries(processes)) { + if (newMyProcesses.has(processId)) continue; + const roles = this.getRoles(process); + if (roles && this.rolesContainsMember(roles, pairingProcessId)) { + newMyProcesses.add(processId); + } + } + this.myProcesses = newMyProcesses; + return Array.from(this.myProcesses); + } + + // --- AJOUT : Méthode manquante --- + public getLastCommitedStateIndex(process: Process): number | null { + if (process.states.length === 0) return null; + const processTip = process.states[process.states.length - 1].commited_in; + for (let i = process.states.length - 1; i >= 0; i--) { + if (process.states[i].commited_in !== processTip) { + return i; + } + } + return null; + } +} diff --git a/src/services/domain/wallet.service.ts b/src/services/domain/wallet.service.ts new file mode 100644 index 0000000..7d9f6af --- /dev/null +++ b/src/services/domain/wallet.service.ts @@ -0,0 +1,101 @@ +import { Device } from '../../../pkg/sdk_client'; +import { SdkService } from '../core/sdk.service'; +import Database from '../database.service'; + +export class WalletService { + constructor(private sdk: SdkService) {} + + public isPaired(): boolean { + try { + return this.sdk.getClient().is_paired(); + } catch (e) { + return false; + } + } + + public getAmount(): BigInt { + return this.sdk.getClient().get_available_amount(); + } + + public getDeviceAddress(): string { + return this.sdk.getClient().get_address(); + } + + public getPairingProcessId(): string { + return this.sdk.getClient().get_pairing_process_id(); + } + + public async createNewDevice(chainTip: number): Promise { + const spAddress = await this.sdk.getClient().create_new_device(0, 'signet'); + const device = this.dumpDeviceFromMemory(); + if (device.sp_wallet.birthday === 0) { + device.sp_wallet.birthday = chainTip; + device.sp_wallet.last_scan = chainTip; + this.sdk.getClient().restore_device(device); + } + await this.saveDeviceInDatabase(device); + return spAddress; + } + + public dumpDeviceFromMemory(): Device { + return this.sdk.getClient().dump_device(); + } + + public dumpNeuteredDevice(): Device | null { + try { + return this.sdk.getClient().dump_neutered_device(); + } catch (e) { + return null; + } + } + + // --- AJOUTS (Manquants) --- + public async dumpWallet(): Promise { + return await this.sdk.getClient().dump_wallet(); + } + + public async getMemberFromDevice(): Promise { + try { + const device = await this.getDeviceFromDatabase(); + if (device) { + const pairedMember = device['paired_member']; + return pairedMember.sp_addresses; + } else { + return null; + } + } catch (e) { + throw new Error(`[WalletService] Échec: ${e}`); + } + } + // ------------------------- + + public async saveDeviceInDatabase(device: Device): Promise { + const db = await Database.getInstance(); + await db.deleteObject('wallet', '1').catch(() => {}); + await db.addObject({ + storeName: 'wallet', + object: { pre_id: '1', device }, + key: null, + }); + } + + public async getDeviceFromDatabase(): Promise { + const db = await Database.getInstance(); + const res = await db.getObject('wallet', '1'); + return res ? res['device'] : null; + } + + public restoreDevice(device: Device) { + this.sdk.getClient().restore_device(device); + } + + public pairDevice(processId: string, spAddressList: string[]): void { + this.sdk.getClient().pair_device(processId, spAddressList); + } + + public async unpairDevice(): Promise { + this.sdk.getClient().unpair_device(); + const newDevice = this.dumpDeviceFromMemory(); + await this.saveDeviceInDatabase(newDevice); + } +} diff --git a/src/services/iframe-controller.service.ts b/src/services/iframe-controller.service.ts index dee524a..2186e5d 100644 --- a/src/services/iframe-controller.service.ts +++ b/src/services/iframe-controller.service.ts @@ -1,6 +1,6 @@ import { MessageType } from '../types/index'; import Services from './service'; -import TokenService from './token'; +import TokenService from './token.service'; import { cleanSubscriptions } from '../utils/subscription.utils'; import { splitPrivateData, isValid32ByteHex } from '../utils/service.utils'; import { MerkleProofResult } from '../../pkg/sdk_client'; diff --git a/src/services/service.ts b/src/services/service.ts index c634575..7e95c03 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1,44 +1,47 @@ -import { initWebsocket, sendMessage } from './websockets.service.ts'; 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, testData } from './storage.service'; +import { storeData, retrieveData } from './storage.service'; import { BackUp } from '../types/index'; +import { APP_CONFIG } from '../config/constants'; -export const U32_MAX = 4294967295; - -const BASEURL = import.meta.env.VITE_BASEURL || `http://localhost`; -const BOOTSTRAPURL = [import.meta.env.VITE_BOOTSTRAPURL || `${BASEURL}:8090`]; -const STORAGEURL = import.meta.env.VITE_STORAGEURL || `${BASEURL}:8081`; -const BLINDBITURL = import.meta.env.VITE_BLINDBITURL || `${BASEURL}:8000`; -const DEFAULTAMOUNT = 1000n; -const EMPTY32BYTES = String('').padStart(64, '0'); +// 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'; export default class Services { - private static initializing: Promise | null = null; private static instance: Services; + private static initializing: Promise | null = null; + + private sdkService: SdkService; + public networkService: NetworkService; + private walletService: WalletService; + private processService: ProcessService; + private cryptoService: CryptoService; + private processId: string | null = null; private stateId: string | null = null; - private sdkClient: any; - private processesCache: Record = {}; - private myProcesses: Set = new Set(); - private notifications: any[] | null = null; - private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; - private database: any; - private relayAddresses: { [wsurl: string]: string } = {}; private membersList: Record = {}; + private notifications: any[] | null = null; private currentBlockHeight: number = -1; - private relayReadyResolver: (() => void) | null = null; - private relayReadyPromise: Promise | null = null; private pendingKeyRequests: Map void> = new Map(); - // Private constructor to prevent direct instantiation from outside - private constructor() {} - // Method to access the singleton instance of Services + public 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); + this.processService = new ProcessService(this.sdkService); + } + public static async getInstance(): Promise { - if (Services.instance) { - return Services.instance; - } - + if (Services.instance) return Services.instance; if (!Services.initializing) { Services.initializing = (async () => { const instance = new Services(); @@ -46,670 +49,477 @@ export default class Services { return instance; })(); } - - console.log('[Services] ⏳ Initialisation des services...'); Services.instance = await Services.initializing; - Services.initializing = null; // Reset for potential future use - console.log('[Services] ✅ Services initialisés.'); return Services.instance; } public async init(): Promise { + console.log('[Services] ⏳ Initialisation...'); this.notifications = this.getNotifications(); - this.sdkClient = await import('../../pkg/sdk_client'); - this.sdkClient.setup(); - for (const wsurl of Object.values(BOOTSTRAPURL)) { - this.updateRelay(wsurl, ''); - } + await this.sdkService.init(); + this.networkService.initRelays(); + console.log('[Services] ✅ Initialisé.'); } - public setProcessId(processId: string | null) { - this.processId = processId; + // --- Getters & Setters --- + public get sdkClient() { + return this.sdkService.getClient(); } - - public setStateId(stateId: string | null) { - this.stateId = stateId; + public setProcessId(id: string | null) { + this.processId = id; } - - public getProcessId(): string | null { + public setStateId(id: string | null) { + this.stateId = id; + } + public getProcessId() { return this.processId; } - - public getStateId(): string | null { + public getStateId() { return this.stateId; } - /** - * Calls `this.addWebsocketConnection` for each `wsurl` in relayAddresses. - * Waits for at least one handshake message before returning. - */ - public async connectAllRelays(): Promise { - const connectedUrls: string[] = []; - - // Connect to all relays - for (const wsurl of Object.keys(this.relayAddresses)) { - try { - console.log(`[Services:connectAllRelays] 🔌 Connexion à: ${wsurl}`); - await this.addWebsocketConnection(wsurl); - connectedUrls.push(wsurl); - console.log(`[Services:connectAllRelays] ✅ Connecté avec succès à: ${wsurl}`); - } catch (error) { - console.error(`[Services:connectAllRelays] ❌ Échec de la connexion à ${wsurl}:`, error); - } - } - - // Wait for at least one handshake message if we have connections - if (connectedUrls.length > 0) { + // --- 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(`[Services:connectAllRelays] ⌛️ ${e.message}`); + console.error(e.message); } } } - - private getRelayReadyPromise(): Promise { - if (!this.relayReadyPromise) { - this.relayReadyPromise = new Promise((resolve) => { - this.relayReadyResolver = resolve; - }); - } - return this.relayReadyPromise; - } - - private resolveRelayReady(): void { - if (this.relayReadyResolver) { - this.relayReadyResolver(); - this.relayReadyResolver = null; - this.relayReadyPromise = null; - } - } - public async addWebsocketConnection(url: string): Promise { - console.log("[Services:addWebsocketConnection] 🕸️ Ouverture d'une nouvelle connexion websocket..."); - await initWebsocket(url); + 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) { + this.networkService.updateRelay(url, sp); + } + public getSpAddress(url: string) { + return this.networkService.getAllRelays()[url]; + } + public printAllRelays() { + this.networkService.printAllRelays(); } - /** - * Add or update a key/value pair in relayAddresses. - * @param wsurl - The WebSocket URL (key). - * @param spAddress - The SP Address (value). - */ - public updateRelay(url: string, spAddress: string) { - console.log(`[Services:updateRelay] ✅ Mise à jour du relais ${url} avec spAddress ${spAddress}`); - this.relayAddresses[url] = spAddress; + // --- 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(); } - /** - * Retrieve the spAddress for a given wsurl. - * @param wsurl - The WebSocket URL to look up. - * @returns The SP Address if found, or undefined if not. - */ - public getSpAddress(wsurl: string): string | undefined { - return this.relayAddresses[wsurl]; + // --- 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); } - /** - * Get all key/value pairs from relayAddresses. - * @returns An array of objects containing wsurl and spAddress. - */ - public getAllRelays(): { wsurl: string; spAddress: string }[] { - return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({ - wsurl, - spAddress, - })); + // --- 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); } - /** - * Print all key/value pairs for debugging. - */ - public printAllRelays(): void { - console.log('[Services:printAllRelays] Adresses relais actuelles:'); - for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) { - console.log(`${wsurl} -> ${spAddress}`); - } + // --- Membres --- + public getAllMembers() { + return this.membersList; } - - public isPaired(): boolean { - try { - const result = this.sdkClient.is_paired(); - return result; - } catch (e) { - throw new Error(`[Services:isPaired] Erreur: ${e}`); - } + public getAllMembersSorted() { + return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))); } - - public async unpairDevice(): Promise { - try { - console.log("[Services:unpairDevice] 🚫 Dissociation de l'appareil..."); - this.sdkClient.unpair_device(); - const newDevice = this.dumpDeviceFromMemory(); - await this.saveDeviceInDatabase(newDevice); - console.log('[Services:unpairDevice] ✅ Appareil dissocié et sauvegardé.'); - } catch (e) { - throw new Error(`[Services:unpairDevice] Échec de la dissociation: ${e}`); - } - } - - public async getSecretForAddress(address: string): Promise { - const db = await Database.getInstance(); - return await db.getObject('shared_secrets', address); - } - - public async getAllSecrets(): Promise { - const db = await Database.getInstance(); - const sharedSecrets = await db.dumpStore('shared_secrets'); - const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); // keys are numeric values - - const secretsStore = { - shared_secrets: sharedSecrets, - unconfirmed_secrets: Object.values(unconfirmedSecrets), - }; - - return secretsStore; - } - - public async getAllDiffs(): Promise> { - const db = await Database.getInstance(); - return await db.dumpStore('diffs'); - } - - /** - * Ensure that the in-memory members list is populated. - * If empty, (re)connect to relays and wait for a handshake to fill it. - */ public async ensureMembersAvailable(): Promise { + 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 { - if (Object.keys(this.membersList).length > 0) { - // console.debug('[Services:ensureMembersAvailable] ✅ Liste des membres déjà disponible.'); - return; - } - console.warn('[Services:ensureMembersAvailable] ⚠️ Liste des membres vide. Tentative de connexion aux relais...'); - // Attempt to connect to relays and wait for handshake which updates membersList - await this.connectAllRelays(); - console.log(`[Services:ensureMembersAvailable] ✅ Connexion aux relais terminée. ${Object.keys(this.membersList).length} membres chargés.`); + this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child)); + return true; } catch (e) { - console.error('[Services:ensureMembersAvailable] ❌ Échec de la récupération des membres:', e); + console.error(e); + return false; } } - public async getDiffByValue(value: string): Promise { - const db = await Database.getInstance(); - const store = 'diffs'; - const res = await db.getObject(store, value); - return res; + 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; + 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:getTokensFromFaucet] 🚰 Demande de tokens au faucet...'); - try { - await this.ensureSufficientAmount(); - } catch (e) { - console.error('[Services:getTokensFromFaucet] ❌ Échec, vérifiez la connexion au relais.'); - return; - } - } - - /** - * Tente d'établir des connexions (secrets partagés) avec les membres d'un état de processus. - */ - public async ensureConnections(process: Process, stateId: string | null = null): Promise { - console.info(`[ConnectionCheck] 🔄 Démarrage de la vérification des connexions pour le processus (StateID: ${stateId || 'par défaut'})`); - - if (!process) { - console.error(`[ConnectionCheck] 💥 ERREUR CRITIQUE: ensureConnections a été appelée avec un processus nul ou undefined.`); - return; - } - - // 1. Déterminer quel état analyser - const state = this.getStateToCheck(process, stateId); - if (!state) { - console.warn(`[ConnectionCheck] ⚠️ Aucun état valide trouvé pour le processus. (States: ${process.states.length}, StateID demandé: ${stateId}). Abandon.`); - return; - } - - // 2. Tenter de trouver les membres dans les rôles de cet état - // --- AMÉLIORATION: Appel 'await' ajouté pour corriger la race condition --- - let members = await this.getMembersFromState(state); - if (members.size === 0) { - console.log(`[ConnectionCheck] ℹ️ Aucun membre trouvé dans les rôles. Vérification s'il s'agit d'un processus de pairing...`); - members = this.getPairingMembers(process); // Tente la logique de pairing - } - - if (members.size === 0) { - console.log(`[ConnectionCheck] 🏁 Aucun membre (rôles ou pairing) trouvé à qui se connecter. Tâche terminée.`); - return; - } - - // 3. Trouver les membres auxquels nous ne sommes pas encore connectés - const unconnectedAddresses = await this.findUnconnectedAddresses(members); - - if (unconnectedAddresses.size === 0) { - console.log(`[ConnectionCheck] ✅ Déjà connecté aux ${members.size} membre(s) trouvés.`); - return; - } - - // 4. Se connecter aux membres manquants - console.log(`[ConnectionCheck] 📡 ${unconnectedAddresses.size} adresse(s) non connectée(s) trouvée(s). Tentative de connexion...`, Array.from(unconnectedAddresses)); - - // getTokensFromFaucet() est maintenant géré DANS connectAddresses - try { - const apiResult = await this.connectAddresses(Array.from(unconnectedAddresses)); - - if (apiResult) { - console.log(`[ConnectionCheck] 🎁 Réponse de 'connectAddresses' reçue, transfert à handleApiReturn...`); - await this.handleApiReturn(apiResult); - } else { - console.log(`[ConnectionCheck] 🤷 'connectAddresses' n'a renvoyé aucun résultat (peut-être un 409 Conflict géré).`); - } - } catch (error) { - console.error(`[ConnectionCheck] 💥 Échec lors de l'appel à connectAddresses: ${error}`, error); - } - } - - // --- FONCTIONS D'AIDE (à placer dans la même classe) --- - - /** - * Helper pour obtenir l'état de processus pertinent à vérifier. - * La logique par défaut (si stateId est nul) est de prendre l'avant-dernier état. - */ - private getStateToCheck(process: Process, stateId: string | null): ProcessState | null { - if (stateId) { - const state = process.states.find((s) => s.state_id === stateId); - if (!state) { - console.warn(`[ConnectionCheck] ⚠️ Impossible de trouver l'état avec l'ID: ${stateId}`); - return null; - } - return state; - } - - // Logique par défaut: prendre l'avant-dernier état (nécessite au moins 2 états) - if (process.states.length < 2) { - console.warn(`[ConnectionCheck] ⚠️ Logique par défaut requiert 2 états, mais seulement ${process.states.length} trouvé(s).`); - return null; - } - // AMÉLIORATION: Log pour cette logique fragile - console.debug(`[ConnectionCheck] ℹ️ Utilisation de l'état n°${process.states.length - 2} (l'avant-dernier) comme état par défaut.`); - return process.states[process.states.length - 2]; - } - - /** - * Helper pour extraire les membres des rôles d'un état. - * --- AMÉLIORATION: Devenu 'async' pour corriger une race condition --- - */ - private async getMembersFromState(state: ProcessState): Promise> { - await this.ensureMembersAvailable(); // S'ASSURE que membersList est chargé - const members = new Set(); - if (!state.roles) { - console.warn(`[ConnectionCheck] ⚠️ L'état ${state.state_id} n'a pas de propriété 'roles'.`); - return members; - } - - for (const role of Object.values(state.roles)) { - for (const memberId of role.members) { - const memberAddresses = this.getAddressesForMemberId(memberId); - if (memberAddresses && memberAddresses.length > 0) { - members.add({ sp_addresses: memberAddresses }); - } else { - console.warn(`[ConnectionCheck] ⚠️ Impossible de trouver les adresses pour le membre ${memberId} (présent dans les rôles).`); - } - } - } - return members; - } - - /** - * Helper pour la logique spécifique de "pairing" : - * cherche 'pairedAddresses' dans l'historique du processus. - */ - private getPairingMembers(process: Process): Set { - const members = new Set(); - let publicData: Record | null = null; - - // Cherche 'pairedAddresses' en remontant l'historique - for (let i = process.states.length - 1; i >= 0; i--) { - const state = process.states[i]; - if (state.public_data && state.public_data['pairedAddresses']) { - publicData = state.public_data; - console.log(`[ConnectionCheck] ℹ️ 'pairedAddresses' trouvé dans l'état ${i} (state_id: ${state.state_id})`); - break; - } - } - - if (publicData && publicData['pairedAddresses']) { - const decodedAddresses = this.decodeValue(publicData['pairedAddresses']); - if (decodedAddresses && decodedAddresses.length > 0) { - members.add({ sp_addresses: decodedAddresses }); - } else { - console.warn(`[ConnectionCheck] ⚠️ 'pairedAddresses' trouvé mais vide après décodage.`); - } - } - - return members; - } - - /** - * Helper pour filtrer une liste de membres et ne garder que ceux - * pour qui nous n'avons pas de secret local. - */ - private async findUnconnectedAddresses(members: Set): Promise> { - const unconnected = new Set(); - const myAddress = await this.getDeviceAddress(); - - for (const member of Array.from(members)) { - const sp_addresses = member.sp_addresses; - if (!sp_addresses || sp_addresses.length === 0) continue; - - for (const address of sp_addresses) { - if (address === myAddress) continue; // On s'ignore soi-même - - if ((await this.getSecretForAddress(address)) === null) { - unconnected.add(address); - } - } - } - - return unconnected; - } - - // --- AMÉLIORATION: Ajout de la logique "try-catch-retry" du faucet --- - public async connectAddresses(addresses: string[]): Promise { - if (addresses.length === 0) { - console.warn("[Services:connectAddresses] Appel avec une liste d'adresses vide."); - return null; - } - - const feeRate = 1; // Devrait être un paramètre ? - - try { - // 1. Première tentative - console.log(`[Services:connectAddresses] 💬 Tentative de connexion (create_transaction) à ${addresses.length} adresse(s).`); - return this.sdkClient.create_transaction(addresses, feeRate); - } catch (error) { - // 2. Vérifier si c'est *exactement* l'erreur de fonds - if (this.isInsufficientFundsError(error)) { - console.warn('[Services:connectAddresses] 💰 Fonds insuffisants détectés. Appel du faucet pour recharger...'); - - try { - // 3. Appel au faucet (le "remède") - await this.getTokensFromFaucet(); // On recharge - - // 4. Seconde (et dernière) tentative - console.log('[Services:connectAddresses] 💬 Nouvelle tentative de connexion post-recharge...'); - return this.sdkClient.create_transaction(addresses, feeRate); - } catch (retryError) { - console.error('[Services:connectAddresses] 💥 Échec critique : Impossible de se connecter, même après recharge.', retryError); - throw new Error("Le système n'a pas pu financer la connexion. Échec."); - } - } else { - // 5. Ce n'était pas une erreur de fonds. - console.error(`[Services:connectAddresses] 💥 Erreur non liée aux fonds lors de la connexion: ${error}`, error); - throw error; // Relancer l'erreur originale - } - } - } - - private async ensureSufficientAmount(): Promise { + console.log('[Services] 🚰 Demande Faucet...'); const availableAmt = this.getAmount(); - const target: BigInt = DEFAULTAMOUNT * BigInt(10); - + const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10); if (availableAmt < target) { - console.log(`[Services:ensureSufficientAmount] 💵 Montant insuffisant (${availableAmt}). Demande au faucet...`); - const faucetMsg = this.createFaucetMessage(); - this.sendFaucetMessage(faucetMsg); - - await this.waitForAmount(target); - console.log(`[Services:ensureSufficientAmount] ✅ Montant suffisant atteint.`); + 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 waitForAmount(target: BigInt): Promise { - let attempts = 3; + private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap) { + if (!newProcesses || Object.keys(newProcesses).length === 0) return; + console.log(`[Services] Synchro ${Object.keys(newProcesses).length} processus...`); - while (attempts > 0) { - const amount = this.getAmount(); - if (amount >= target) { - return amount; + 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; + } } - console.log(`[Services:waitForAmount] ⏳ Attente de fonds... Tentative ${4 - attempts}/3`); - attempts--; - if (attempts > 0) { - await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second + if (Object.keys(toSave).length > 0) { + await this.processService.batchSaveProcesses(toSave); } } - - throw new Error('Le montant est toujours 0 après 3 tentatives'); + document.dispatchEvent(new CustomEvent('processes-updated')); } public async createPairingProcess(userName: string, pairWith: string[]): Promise { - console.log("[Services:createPairingProcess] 🤝 Création d'un processus de pairing..."); - if (this.sdkClient.is_paired()) { - throw new Error("L'appareil est déjà appairé"); - } - const myAddress: string = this.sdkClient.get_address(); + 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: string[] = [...Object.keys(privateData), ...Object.keys(publicData), 'roles']; - const roles: Record = { + 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: [STORAGEURL], + validation_rules: [{ quorum: 1.0, fields: validation_fields, min_sig_member: 1.0 }], + storages: [APP_CONFIG.URLS.STORAGE], }, }; - try { - return this.createProcess(privateData, publicData, roles); - } catch (e) { - throw new Error(`[Services:createPairingProcess] 💥 Échec: ${e}`); - } + return this.createProcess(privateData, publicData, roles); } - private isFileBlob(value: any): value is { type: string; data: Uint8Array } { - return typeof value === 'object' && value !== null && typeof value.type === 'string' && value.data instanceof Uint8Array; - } - - private splitData(obj: Record) { - const jsonCompatibleData: Record = {}; - const binaryData: Record = {}; - - for (const [key, value] of Object.entries(obj)) { - if (this.isFileBlob(value)) { - binaryData[key] = value; - } else { - jsonCompatibleData[key] = value; - } - } - - return { jsonCompatibleData, binaryData }; - } - - // --- AMÉLIORATION: Logique de 'ensureConnections' déplacée ici --- - public async createProcess(privateData: Record, publicData: Record, roles: Record, feeRate: number = 1): Promise { - console.log("[Services:createProcess] 📝 Création d'un nouveau processus..."); - const relayAddress = await this.getAvailableRelayAddress(); + 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.getAllMembers(); - + const members = this.membersList; try { - // 1. Première tentative - const result = await this.attemptProcessCreation(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members); - - // --- AMÉLIORATION: Déplacé ici depuis le 'router' --- - // On s'assure qu'on est connecté aux membres du processus qu'on vient de créer. - console.log(`[Services:createProcess] 📞 Vérification des connexions pour le nouveau processus ${result.updated_process!.process_id}`); - await this.ensureConnections(result.updated_process!.current_process); - return result; - } catch (error) { - // 2. Vérifier si c'est *exactement* l'erreur de fonds - if (this.isInsufficientFundsError(error)) { - console.warn('[Services:createProcess] 💰 Fonds insuffisants détectés. Appel du faucet pour recharger...'); - - try { - // 3. Appel au faucet - await this.getTokensFromFaucet(); // On recharge - - // 4. Seconde (et dernière) tentative - console.log('[Services:createProcess] 🔄 Nouvelle tentative de création de processus post-recharge...'); - const result = await this.attemptProcessCreation(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members); - - // --- AMÉLIORATION: Déplacé ici depuis le 'router' --- - console.log(`[Services:createProcess] 📞 Vérification des connexions pour le nouveau processus ${result.updated_process!.process_id} (après retry)`); - await this.ensureConnections(result.updated_process!.current_process); - return result; - } catch (retryError) { - console.error('[Services:createProcess] 💥 Échec critique : Impossible de créer le processus, même après recharge.', retryError); - throw new Error("Le système n'a pas pu financer l'opération. Échec de la création."); - } - } else { - // 5. Ce n'était pas une erreur de fonds. - console.error('[Services:createProcess] 💥 Erreur non liée aux fonds lors de la création du processus:', error); - throw error; // Relancer l'erreur originale + 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; } } - /** - * Encapsule l'appel au SDK pour le réutiliser (tentative 1 et 2). - */ - private async attemptProcessCreation(encodedPrivateData: any, roles: any, encodedPublicData: any, relayAddress: string, feeRate: number, members: any): Promise { - console.log('[Services:attemptProcessCreation] 📦 Appel de sdkClient.create_new_process...'); - const result = this.sdkClient.create_new_process(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members); - if (result.updated_process) { - console.log('[Services:attemptProcessCreation] ✅ Processus créé avec succès:', result.updated_process.process_id); - return result; - } else { - throw new Error("[Services:attemptProcessCreation] 💥 sdkClient.create_new_process n'a renvoyé aucun processus mais n'a pas levé d'erreur."); + 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; } - /** - * Vérifie de manière robuste si l'erreur est bien celle des fonds insuffisants. - */ - private isInsufficientFundsError(error: any): boolean { - const errorString = String(error.message || error.error || error); - return errorString.includes('Insufficient funds'); - } + 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'); - /** - * Tente d'obtenir une adresse de relais, en attendant si nécessaire. - */ - private async getAvailableRelayAddress(): Promise { - let relayAddress = this.getAllRelays()[0]?.spAddress; // TODO: Améliorer la sélection - - if (!relayAddress) { - console.log('[Services:getAvailableRelayAddress] ⏳ Aucun relais prêt. En attente du handshake...'); - await this.getRelayReadyPromise(); - relayAddress = this.getAllRelays()[0]?.spAddress; - } - - if (!relayAddress) { - throw new Error('[Services:getAvailableRelayAddress] ❌ Aucune adresse de relais disponible après attente'); - } - return relayAddress; - } - - /** - * Sépare et encode les données JSON et binaires. - */ - private async prepareProcessData(privateData: any, publicData: any): Promise<{ encodedPrivateData: any; encodedPublicData: any }> { - // TODO: Exécuter l'encodage lourd dans un Web Worker - const privateSplitData = this.splitData(privateData); - const publicSplitData = this.splitData(publicData); - - const encodedPrivateData = { - ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), - ...this.sdkClient.encode_binary(privateSplitData.binaryData), - }; - const encodedPublicData = { - ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), - ...this.sdkClient.encode_binary(publicSplitData.binaryData), - }; - - return { encodedPrivateData, encodedPublicData }; - } - - /** - * Met à jour un processus. - * Gère la logique complexe de recherche du dernier état "commited" - * et tente une auto-approbation si nécessaire. - */ - public async updateProcess( - processId: string, // Changement : on reçoit l'ID, pas l'objet Process - newData: Record, // Changement : on reçoit les nouvelles données - privateFields: string[], // Changement : on reçoit les champs privés - roles: Record | null, - ): Promise { - // 1. Récupérer le processus - const process = await this.getProcess(processId); - if (!process) { - throw new Error(`[Services:updateProcess] Processus ${processId} non trouvé`); - } - - // --- DEBUT DE LA LOGIQUE DÉPLACÉE DU ROUTEUR --- - - // 2. Trouver le dernier état et le "réparer" si nécessaire - let lastState = this.getLastCommitedState(process); - let currentProcess = process; // Garde une trace du processus à jour + let lastState = this.processService.getLastCommitedState(process); + let currentProcess = process; if (!lastState) { - console.warn(`[Services:updateProcess] ⚠️ Processus ${processId} n'a pas d'état "commited". Tentative d'auto-approbation...`); - const firstState = process.states[0]; - - if (this.rolesContainsUs(firstState.roles)) { - // ON REMPLACE LE setTimeout PAR UN VRAI AWAIT - console.log(`[Services:updateProcess] Auto-approbation de l'état ${firstState.state_id}...`); - const approveChangeRes = await this.approveChange(processId, firstState.state_id); - await this.handleApiReturn(approveChangeRes); // On attend que la BDD soit à jour - - console.log(`[Services:updateProcess] Création de la PRD update pour l'état ${firstState.state_id}...`); - const prdUpdateRes = await this.createPrdUpdate(processId, firstState.state_id); - await this.handleApiReturn(prdUpdateRes); // On attend à nouveau - } else { - if (firstState.validation_tokens.length > 0) { - console.log(`[Services:updateProcess] Création de la PRD update (sans approbation) pour l'état ${firstState.state_id}...`); - const res = await this.createPrdUpdate(processId, firstState.state_id); - await this.handleApiReturn(res); - } + 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); } - - // 3. Re-synchroniser l'état après nos actions - console.log(`[Services:updateProcess] Re-vérification de l'état "commited" après auto-approbation...`); - const updatedProcess = await this.getProcess(processId); // On recharge depuis la BDD - if (!updatedProcess) throw new Error('Le processus a disparu après la tentative de réparation'); - - currentProcess = updatedProcess; // On met à jour notre référence - lastState = this.getLastCommitedState(currentProcess); - - if (!lastState) { - // Si ça échoue toujours, on abandonne - throw new Error("Le processus n'a toujours pas d'état 'commited' après la tentative de réparation."); - } - console.log(`[Services:updateProcess] ✅ État "commited" ${lastState.state_id} trouvé.`); + const 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) { - // Sécurité, bien que logiquement couvert par le bloc ci-dessus - throw new Error("Impossible de trouver l'index du dernier état 'commited'."); - } + if (lastStateIndex === null) throw new Error('Index commited introuvable'); - // 4. Calculer les diffs (logique de séparation privée/publique) - const privateData: Record = {}; - const publicData: Record = {}; + const privateData: any = {}; + const publicData: any = {}; for (const field of Object.keys(newData)) { if (lastState.public_data[field]) { @@ -720,1090 +530,316 @@ export default class Services { privateData[field] = newData[field]; continue; } - // Logique de recherche dans l'historique + let isPrivate = false; for (let i = lastStateIndex; i >= 0; i--) { - const state = currentProcess.states[i]; - if (state.pcd_commitment[field]) { + if (currentProcess.states[i].pcd_commitment[field]) { privateData[field] = newData[field]; + isPrivate = true; break; } } - if (privateData[field]) continue; - - // Par défaut, c'est public - publicData[field] = newData[field]; + if (!isPrivate) publicData[field] = newData[field]; } - // --- FIN DE LA LOGIQUE DÉPLACÉE DU ROUTEUR --- + const finalRoles = roles || this.processService.getRoles(currentProcess); + const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); - // 5. Exécuter la mise à jour - if (!roles) { - roles = this.getRoles(currentProcess); // Important : utiliser currentProcess + 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'); } + } - console.log('[Services:updateProcess] ℹ️ Préparation des données binaires et JSON...'); - const privateSplitData = this.splitData(privateData); - const publicSplitData = this.splitData(publicData); - const encodedPrivateData = { - ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), - ...this.sdkClient.encode_binary(privateSplitData.binaryData), + 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) }, }; - const encodedPublicData = { - ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), - ...this.sdkClient.encode_binary(publicSplitData.binaryData), - }; - - try { - console.log('[Services:updateProcess] 🚀 Appel de sdkClient.update_process...'); - const result = this.sdkClient.update_process( - currentProcess, // Utiliser le processus potentiellement mis à jour - encodedPrivateData, - roles, - encodedPublicData, - this.getAllMembers(), - ); - - if (result.updated_process) { - console.log(`[Services:updateProcess] ✅ Processus mis à jour. Vérification des connexions...`); - // On s'assure qu'on est connecté aux membres du nouvel état - await this.ensureConnections(result.updated_process.current_process); - return result; - } else { - throw new Error('[Services:updateProcess] 💥 updated_process vide dans updateProcessReturn'); - } - } catch (e) { - throw new Error(`[Services:updateProcess] 💥 Échec: ${e}`); - } } - public async createPrdUpdate(processId: string, stateId: string): Promise { - console.log(`[Services:createPrdUpdate] 📤 Création d'une mise à jour PRD pour ${processId}:${stateId}`); - const process = await this.getProcess(processId); - if (!process) { - throw new Error('[Services:createPrdUpdate] 💥 Processus inconnu'); - } else { - await this.ensureConnections(process); - } - try { - return this.sdkClient.create_update_message(process, stateId, this.getAllMembers()); - } catch (e) { - throw new Error(`[Services:createPrdUpdate] 💥 Échec: ${e}`); - } + // 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(processId: string, stateId: string): Promise { - console.log(`[Services:createPrdResponse] 📥 Création d'une réponse PRD pour ${processId}:${stateId}`); - const process = await this.getProcess(processId); - if (!process) { - throw new Error('[Services:createPrdResponse] 💥 Processus inconnu'); - } - try { - return this.sdkClient.create_response_prd(process, stateId, this.getAllMembers()); - } catch (e) { - throw new Error(`[Services:createPrdResponse] 💥 Échec: ${e}`); - } + public async createPrdResponse(pid: string, sid: string) { + const p = await this.getProcess(pid); + return this.sdkClient.create_response_prd(p, sid, this.membersList); } - - public async approveChange(processId: string, stateId: string): Promise { - console.log(`[Services:approveChange] 👍 Approbation du changement ${processId}:${stateId}`); - const process = await this.getProcess(processId); - if (!process) { - throw new Error("[Services:approveChange] 💥 Échec de l'obtention du processus depuis la BDD"); - } - try { - const result = this.sdkClient.validate_state(process, stateId, this.getAllMembers()); - if (result.updated_process) { - await this.ensureConnections(result.updated_process.current_process); - return result; - } else { - throw new Error('[Services:approveChange] 💥 updated_process vide dans approveChangeReturn'); - } - } catch (e) { - throw new Error(`[Services:approveChange] 💥 Échec: ${e}`); - } + public async 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(processId: string, stateId: string): Promise { - console.log(`[Services:rejectChange] 👎 Rejet du changement ${processId}:${stateId}`); - const process = await this.getProcess(processId); - if (!process) { - throw new Error("[Services:rejectChange] 💥 Échec de l'obtention du processus depuis la BDD"); - } - try { - return this.sdkClient.refuse_state(process, stateId); - } catch (e) { - throw new Error(`[Services:rejectChange] 💥 Échec: ${e}`); - } + public async rejectChange(pid: string, sid: string) { + const p = await this.getProcess(pid); + return this.sdkClient.refuse_state(p, sid); } - - async resetDevice() { - console.warn("[Services:resetDevice] ⚠️ RÉINITIALISATION COMPLÈTE de l'appareil et de la BDD..."); + 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('Resetting device...'); this.sdkClient.reset_device(); - - // Clear all stores const db = await Database.getInstance(); await db.clearStore('wallet'); + await db.clearStore('processes'); await db.clearStore('shared_secrets'); await db.clearStore('unconfirmed_secrets'); - await db.clearStore('processes'); await db.clearStore('diffs'); - console.warn('[Services:resetDevice] ✅ Réinitialisation terminée.'); } - sendNewTxMessage(message: string) { - console.log('[Services:sendNewTxMessage] ✉️ Envoi de NewTx...'); - sendMessage('NewTx', message); - } - - sendCommitMessage(message: string) { - console.log('[Services:sendCommitMessage] ✉️ Envoi de Commit...'); - sendMessage('Commit', message); - } - - sendCipherMessages(ciphers: string[]) { - console.log(`[Services:sendCipherMessages] ✉️ Envoi de ${ciphers.length} cipher(s)...`); - for (let i = 0; i < ciphers.length; i++) { - const cipher = ciphers[i]; - sendMessage('Cipher', cipher); - } - } - - sendFaucetMessage(message: string): void { - console.log('[Services:sendFaucetMessage] ✉️ Envoi de Faucet...'); - sendMessage('Faucet', message); - } - - // --- AMÉLIORATION: Ajout de la solution "bombe" pour casser la boucle --- - async parseCipher(message: string) { - const membersList = this.getAllMembers(); - const processes = await this.getProcesses(); + public async handleApiReturn(res: ApiReturn) { + if (!res || Object.keys(res).length === 0) return; try { - console.debug('[Services:parseCipher] 🤫 Tentative de déchiffrement du message...'); - const apiReturn = this.sdkClient.parse_cipher(message, membersList, processes); - console.debug('[Services:parseCipher] ✅ Message déchiffré, traitement...'); - await this.handleApiReturn(apiReturn); - } catch (e) { - console.error(`[Services:parseCipher] 💥 Échec critique du déchiffrement: ${e}`); - } - } - - async parseNewTx(newTxMsg: string) { - console.log('[Services:parseNewTx] 📄 Nouveau message NewTx reçu.'); - const parsedMsg: NewTxMessage = JSON.parse(newTxMsg); - if (parsedMsg.error !== null) { - console.error('[Services:parseNewTx] 💥 Erreur dans le message NewTx:', parsedMsg.error); - return; - } - - const membersList = this.getAllMembers(); - - // 1. Mettre à jour les processus affectés par cette transaction - await this.updateProcessesFromNewTx(parsedMsg.transaction); - - // 2. Mettre à jour le portefeuille et l'état de l'appareil - await this.updateWalletFromNewTx(newTxMsg, membersList); - } - - /** - * Sous-fonction de parseNewTx: Met à jour les processus en cache. - */ - private async updateProcessesFromNewTx(transaction: any) { - try { - const prevouts = this.sdkClient.get_prevouts(transaction); - // console.debug('[Services:updateProcessesFromNewTx] Prevouts de la tx:', prevouts); - for (const process of Object.values(this.processesCache)) { - const tip = process.states[process.states.length - 1].commited_in; - if (prevouts.includes(tip)) { - const processId = process.states[0].commited_in; - const newTip = this.sdkClient.get_txid(transaction); - console.log(`[Services:updateProcessesFromNewTx] 🔗 La Tx ${newTip} dépense le tip du processus ${processId}`); - - const newStateId = this.sdkClient.get_opreturn(transaction); - console.log('[Services:updateProcessesFromNewTx] 📄 Nouvel stateId (op_return):', newStateId); - - const updatedProcess = this.sdkClient.process_commit_new_state(process, newStateId, newTip); - this.processesCache[processId] = updatedProcess; - console.log('[Services:updateProcessesFromNewTx] ✅ Processus mis à jour en cache:', updatedProcess); - break; // On suppose qu'une tx ne met à jour qu'un seul processus - } - } - } catch (e) { - console.error("[Services:updateProcessesFromNewTx] 💥 Échec de l'analyse NewTx pour les commitments:", e); - } - } - - /** - * Sous-fonction de parseNewTx: Met à jour le portefeuille. - */ - private async updateWalletFromNewTx(newTxMsg: string, membersList: Record) { - try { - const parsedTx = this.sdkClient.parse_new_tx(newTxMsg, 0, membersList); - if (parsedTx && (parsedTx.partial_tx || parsedTx.new_tx_to_send || parsedTx.secrets || parsedTx.updated_process)) { - console.log('[Services:updateWalletFromNewTx] ℹ️ La Tx contient des données pertinentes. Traitement par handleApiReturn...'); - try { - await this.handleApiReturn(parsedTx); - const newDevice = this.dumpDeviceFromMemory(); - - // Preserve pairing_process_commitment from existing device - const existingDevice = await this.getDeviceFromDatabase(); - if (existingDevice && existingDevice.pairing_process_commitment) { - newDevice.pairing_process_commitment = existingDevice.pairing_process_commitment; - } - - await this.saveDeviceInDatabase(newDevice); - console.log('[Services:updateWalletFromNewTx] ✅ Appareil mis à jour et sauvegardé.'); - } catch (e) { - console.error("[Services:updateWalletFromNewTx] 💥 Échec de la mise à jour de l'appareil après NewTx:", e); - } - } else { - // console.debug('[Services:updateWalletFromNewTx] ℹ️ La Tx ne contenait pas de données pertinentes pour le portefeuille.'); - } - } catch (e) { - // C'est souvent normal (ex: une tx qui ne nous concerne pas) - // console.debug('[Services:updateWalletFromNewTx] ℹ️ sdkClient.parse_new_tx n\'a rien trouvé:', e); - } - } - - // --- AMÉLIORATION: Logs ajoutés --- - public async handleApiReturn(apiReturn: ApiReturn) { - console.log("[Services:handleApiReturn] 📥 Traitement d'un nouvel objet ApiReturn...", apiReturn); - - // 1. Validation initiale - if (!this.isValidApiReturn(apiReturn)) { - console.log('[Services:handleApiReturn] ⏩ ApiReturn vide ou invalide. Skip.'); - return; - } - - try { - // 2. Gestion de la signature de transaction - const newTxFromSigning = apiReturn.partial_tx ? await this.handlePartialTx(apiReturn.partial_tx) : null; - - // 3. Gestion de l'envoi de transaction - const txData = newTxFromSigning || apiReturn.new_tx_to_send; + const txData = (res.partial_tx ? await this.handlePartialTx(res.partial_tx) : null) || res.new_tx_to_send; if (txData && txData.transaction.length != 0) { - console.log("[Services:handleApiReturn] 📤 Envoi d'une nouvelle transaction..."); - await this.handleNewTx(txData); + this.networkService.sendMessage('NewTx', JSON.stringify(txData)); + await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.API_DELAY)); } - - // 4. Gestion des secrets - if (apiReturn.secrets) { - console.log('[Services:handleApiReturn] 🔑 Gestion des secrets...'); - await this.handleSecrets(apiReturn.secrets); - } - - // 5. Gestion du processus mis à jour - if (apiReturn.updated_process) { - console.log('[Services:handleApiReturn] 🔄 Gestion de la mise à jour de processus...'); - await this.handleUpdatedProcess(apiReturn.updated_process); - } - - // 6. Gestion du push vers le stockage - if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) { - console.log('[Services:handleApiReturn] ☁️ Poussée de données vers le stockage...'); - await this.handlePushToStorage(apiReturn.push_to_storage); - } - - // 7. Gestion du "commit" à envoyer - if (apiReturn.commit_to_send) { - console.log('[Services:handleApiReturn] 📤 Envoi de Commit...'); - this.handleCommit(apiReturn.commit_to_send); - } - - // 8. Gestion des "ciphers" à envoyer - if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) { - console.log('[Services:handleApiReturn] 📤 Envoi de Ciphers...'); - this.handleCiphers(apiReturn.ciphers_to_send); - } - } catch (error) { - console.error('[Services:handleApiReturn] 💥 ERREUR CRITIQUE lors du traitement de ApiReturn:', error); + 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 isValidApiReturn(apiReturn: ApiReturn): boolean { - if (!apiReturn || Object.keys(apiReturn).length === 0) { - return false; - } - const hasValidData = Object.values(apiReturn).some((value) => value !== null && value !== undefined); - return hasValidData; - } - private async handlePartialTx(partialTx: any): Promise { - console.log("[Services:handlePartialTx] ✍️ Signature d'une transaction partielle..."); try { - const res = this.sdkClient.sign_transaction(partialTx); - return res.new_tx_to_send; + return this.sdkClient.sign_transaction(partialTx).new_tx_to_send; } catch (e) { - console.error('[Services:handlePartialTx] 💥 Échec de la signature:', e); return null; } } - private async handleNewTx(txData: any) { - this.sendNewTxMessage(JSON.stringify(txData)); - // 🚨 ATTENTION: C'est un anti-pattern (code smell). - // Cette attente arbitraire doit être remplacée par un - // véritable mécanisme d'acquittement (par ex. une Promise - // retournée par sendNewTxMessage). - console.warn('[Services:handleNewTx] ⏳ Attente arbitraire de 500ms...'); - await new Promise((r) => setTimeout(r, 500)); - } - private async handleSecrets(secrets: any) { - const { unconfirmed_secrets, shared_secrets } = secrets; const db = await Database.getInstance(); - - // Sauvegarder les secrets non confirmés - if (unconfirmed_secrets && unconfirmed_secrets.length > 0) { - console.log(`[Services:handleSecrets] 💾 Sauvegarde de ${unconfirmed_secrets.length} secret(s) non confirmé(s)`); - for (const secret of unconfirmed_secrets) { - try { - await db.addObject({ - storeName: 'unconfirmed_secrets', - object: secret, - key: null, - }); - } catch (e) { - console.error("[Services:handleSecrets] 💥 Échec de sauvegarde d'un secret non confirmé:", e); - } - } - } - - // Sauvegarder les secrets partagés (confirmés) - if (shared_secrets && Object.keys(shared_secrets).length > 0) { - const entries = Object.entries(shared_secrets).map(([key, value]) => ({ key, value })); - console.log(`[Services:handleSecrets] 💾 Sauvegarde de ${entries.length} secret(s) partagé(s)`); - for (const entry of entries) { - try { - await db.addObject({ - storeName: 'shared_secrets', - object: entry.value, - key: entry.key, - }); - console.log(`[Services:handleSecrets] ✅ Secret partagé pour ${entry.key} sauvegardé.`); - } catch (e) { - console.error(`[Services:handleSecrets] 💥 Échec de l'ajout du secret partagé pour ${entry.key}:`, e); - } - } - } + if (secrets.unconfirmed_secrets) for (const s of secrets.unconfirmed_secrets) await db.addObject({ storeName: 'unconfirmed_secrets', object: s, key: null }); + if (secrets.shared_secrets) for (const [k, v] of Object.entries(secrets.shared_secrets)) await db.addObject({ storeName: 'shared_secrets', object: v, key: k }); } - private async handleUpdatedProcess(updatedProcess: any) { - const processId: string = updatedProcess.process_id; - console.log(`[Services:handleUpdatedProcess] 🔄 Traitement des mises à jour pour le processus ${processId}`); - - // Sauvegarder les données chiffrées - if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) { - await this.saveEncryptedData(updatedProcess.encrypted_data); + 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); - // Sauvegarder le processus lui-même - await this.saveProcessToDb(processId, updatedProcess.current_process); - - // Sauvegarder les diffs - if (updatedProcess.diffs && updatedProcess.diffs.length != 0) { - try { - await this.saveDiffsToDb(updatedProcess.diffs); - } catch (e) { - console.error('[Services:handleUpdatedProcess] 💥 Échec de la sauvegarde des diffs:', e); - } + this._resolvePendingKeyRequests(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(); } - - this._resolvePendingKeyRequests(processId, updatedProcess.current_process); - - // Vérifier la logique métier spécifique au pairing - await this.checkAndConfirmPairing(processId, updatedProcess); - } - - private _resolvePendingKeyRequests(processId: string, process: Process) { - if (this.pendingKeyRequests.size === 0) { - return; // Optimisation : ne rien faire si personne n'attend - } - - console.log(`[Services:KeyResolver] 🔍 Vérification de ${this.pendingKeyRequests.size} requête(s) de clé en attente...`); - - for (const state of process.states) { - if (!state.keys || Object.keys(state.keys).length === 0) { - continue; // Pas de clés dans cet état - } - - for (const [attributeName, key] of Object.entries(state.keys)) { - const requestId = `${processId}_${state.state_id}_${attributeName}`; - - // Avons-nous une requête en attente pour CETTE clé ? - if (this.pendingKeyRequests.has(requestId)) { - console.log(`[Services:KeyResolver] ✅ Résolution de la requête pour ${requestId}`); - - // Récupérer la fonction "resolve" et l'appeler avec la clé - const resolveCallback = this.pendingKeyRequests.get(requestId); - if (resolveCallback) { - resolveCallback(key as string); - } - - // Nettoyer la requête - this.pendingKeyRequests.delete(requestId); - } - } - } - } - - private async saveEncryptedData(encryptedData: Record) { - console.log(`[Services:saveEncryptedData] 💾 Sauvegarde de ${Object.keys(encryptedData).length} blob(s) chiffré(s)...`); - for (const [hash, cipher] of Object.entries(encryptedData)) { - const blob = this.hexToBlob(cipher); - try { - await this.saveBlobToDb(hash, blob); - } catch (e) { - console.error(`[Services:saveEncryptedData] 💥 Échec de la sauvegarde du blob pour ${hash}:`, e); - } - } - } - - private async checkAndConfirmPairing(processId: string, updatedProcess: any) { - try { - const existingDevice = await this.getDeviceFromDatabase(); - if (!existingDevice || existingDevice.pairing_process_commitment !== processId) { - // console.debug('[Services:checkAndConfirmPairing] ℹ️ Ce n\'est pas le processus de pairing de cet appareil. Skip.'); - return; // Ce n'est pas le processus de pairing de cet appareil - } - - // C'est notre processus de pairing, vérifions s'il est prêt - const lastState = updatedProcess.current_process.states[updatedProcess.current_process.states.length - 1]; - if (lastState && lastState.public_data && lastState.public_data['pairedAddresses']) { - console.log('[Services:checkAndConfirmPairing] 🤝 Processus de pairing mis à jour avec les adresses. Confirmation automatique...'); - await this.confirmPairing(); - } - } catch (e) { - console.error("[Services:checkAndConfirmPairing] 💥 Échec de l'auto-confirmation du pairing:", e); - } - } - - private async handlePushToStorage(hashes: string[]) { - console.log(`[Services:handlePushToStorage] ☁️ Demande de push pour ${hashes.length} hash(es)`); - for (const hash of hashes) { - try { - const blob = await this.getBlobFromDb(hash); - if (!blob) { - console.error(`[Services:handlePushToStorage] 💥 Échec: blob non trouvé en BDD pour le hash ${hash}`); - continue; - } - - const diff = await this.getDiffByValueFromDb(hash); - if (!diff) { - console.error(`[Services:handlePushToStorage] 💥 Échec: diff non trouvé en BDD pour le hash ${hash}`); - continue; - } - - const storages = diff.storages; - console.log(`[Services:handlePushToStorage] ☁️ Poussée de ${hash} vers ${storages.length} storage(s)...`); - await this.saveDataToStorage(storages, hash, blob, null); - } catch (e) { - console.error(`[Services:handlePushToStorage] 💥 Échec du push pour ${hash}:`, e); - } - } - } - - private handleCommit(commit: any) { - this.sendCommitMessage(JSON.stringify(commit)); - } - - private handleCiphers(ciphers: any[]) { - this.sendCipherMessages(ciphers); - } - - - - public async confirmPairing() { - console.log('[Services:confirmPairing] 🤝 Confirmation du pairing...'); - try { - // Get the pairing process ID from database - const existingDevice = await this.getDeviceFromDatabase(); - if (!existingDevice || !existingDevice.pairing_process_commitment) { - console.error('[Services:confirmPairing] 💥 Aucun engagement de processus de pairing trouvé'); - return; - } - - const pairingProcessId = existingDevice.pairing_process_commitment; - console.log(`[Services:confirmPairing] ℹ️ Processus ID: ${pairingProcessId}`); - - // Get the pairing process to extract paired addresses - const myPairingProcess = await this.getProcess(pairingProcessId); - if (!myPairingProcess) { - console.error('[Services:confirmPairing] 💥 Processus de pairing inconnu'); - return; - } - - // Try to get committed state first, fallback to current state - let myPairingState = this.getLastCommitedState(myPairingProcess); - if (!myPairingState && myPairingProcess.states.length > 0) { - // If no committed state, use the current state - myPairingState = myPairingProcess.states[myPairingProcess.states.length - 1]; - console.log('[Services:confirmPairing] ⚠️ Utilisation de l\'état actuel au lieu de l\'état "commited"'); - } - - if (!myPairingState) { - console.error('[Services:confirmPairing] 💥 Aucun état trouvé dans le processus de pairing'); - return; - } - - const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; - if (!encodedSpAddressList) { - console.error("[Services:confirmPairing] 💥 Aucune adresse d'appairage trouvée dans l'état"); - return; - } - - const spAddressList = this.decodeValue(encodedSpAddressList); - if (spAddressList.length === 0) { - console.error('[Services:confirmPairing] 💥 pairedAddresses est vide'); - return; - } - console.log(`[Services:confirmPairing] ℹ️ ${spAddressList.length} adresses trouvées pour l'appairage.`); - - // ... (Suppression du bloc de test 'test_process_id_parsing' pour la clarté) - - this.sdkClient.unpair_device(); // Clear any existing pairing - - try { - console.log('[Services:confirmPairing] 📞 Appel de sdkClient.pair_device()...'); - this.sdkClient.pair_device(pairingProcessId, spAddressList); - console.log('[Services:confirmPairing] ✅ Appel de pair_device() réussi (côté SDK).'); - } catch (pairError) { - console.error('[Services:confirmPairing] 💥 sdkClient.pair_device() a échoué:', pairError); - throw pairError; - } - - // Verify pairing was successful - const isPairedAfterPairing = this.sdkClient.is_paired(); - console.log('[Services:confirmPairing] ❓ Statut is_paired après appel:', isPairedAfterPairing); - - // Save the updated device - const newDevice = this.dumpDeviceFromMemory(); - console.log('[Services:confirmPairing] ℹ️ Appareil en mémoire après appairage:', { - pairing_process_commitment: newDevice.pairing_process_commitment, - paired_member: newDevice.paired_member, - }); - - // IMPORTANT: Only set pairing_process_commitment if WASM pairing succeeded - if (isPairedAfterPairing) { - console.log("[Services:confirmPairing] ℹ️ L'appairage WASM a réussi, conservation de l'engagement WASM"); - } else { - console.warn("[Services:confirmPairing] ⚠️ L'appairage WASM a échoué, définition manuelle de l'engagement (fallback)"); - newDevice.pairing_process_commitment = pairingProcessId; - } - - await this.saveDeviceInDatabase(newDevice); - - // Final verification - const finalIsPaired = this.sdkClient.is_paired(); - console.log('[Services:confirmPairing] ✅ Statut final is_paired:', finalIsPaired); - console.log(`[Services:confirmPairing] ✅ Appareil appairé avec succès au processus: ${pairingProcessId}`); - } catch (e) { - console.error('[Services:confirmPairing] 💥 Échec global de la confirmation du pairing:', e); - return; - } - } - - public async updateDevice(): Promise { - console.log("[Services:updateDevice] 🔄 Mise à jour de l'appareil..."); - let myPairingProcessId: string; - try { - myPairingProcessId = this.getPairingProcessId(); - } catch (e) { - console.error("[Services:updateDevice] 💥 Échec de l'obtention du pairing process id"); - return; - } - - const myPairingProcess = await this.getProcess(myPairingProcessId); - if (!myPairingProcess) { - console.error('[Services:updateDevice] 💥 Processus de pairing inconnu'); - return; - } - const myPairingState = this.getLastCommitedState(myPairingProcess); - if (myPairingState) { - const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; - const spAddressList = this.decodeValue(encodedSpAddressList); - if (spAddressList.length === 0) { - console.error('[Services:updateDevice] 💥 pairedAddresses est vide'); - return; - } - // We can check if our address is included and simply unpair if it's not - if (!spAddressList.includes(this.getDeviceAddress())) { - console.warn("[Services:updateDevice] ⚠️ Notre adresse n'est plus dans la liste. Dissociation..."); - await this.unpairDevice(); - return; - } - // We can update the device with the new addresses - console.log("[Services:updateDevice] 🔄 Ré-appairage avec la nouvelle liste d'adresses..."); - this.sdkClient.unpair_device(); - this.sdkClient.pair_device(myPairingProcessId, spAddressList); - const newDevice = this.dumpDeviceFromMemory(); - await this.saveDeviceInDatabase(newDevice); - console.log('[Services:updateDevice] ✅ Appareil mis à jour.'); - } - } - - public pairDevice(processId: string, spAddressList: string[]): void { - try { - this.sdkClient.pair_device(processId, spAddressList); - } catch (e) { - throw new Error(`[Services:pairDevice] 💥 Échec: ${e}`); - } - } - - public getAmount(): BigInt { - const amount = this.sdkClient.get_available_amount(); - return amount; - } - - getDeviceAddress(): string { - try { - return this.sdkClient.get_address(); - } catch (e) { - throw new Error(`[Services:getDeviceAddress] 💥 Échec: ${e}`); - } - } - - public dumpDeviceFromMemory(): Device { - try { - return this.sdkClient.dump_device(); - } catch (e) { - throw new Error(`[Services:dumpDeviceFromMemory] 💥 Échec: ${e}`); - } - } - - public dumpNeuteredDevice(): Device | null { - try { - return this.sdkClient.dump_neutered_device(); - } catch (e) { - console.error(`[Services:dumpNeuteredDevice] 💥 Échec: ${e}`); - return null; - } - } - - public getPairingProcessId(): string { - try { - return this.sdkClient.get_pairing_process_id(); - } catch (e) { - throw new Error(`[Services:getPairingProcessId] 💥 Échec (Probablement non appairé): ${e}`); - } - } - - async saveDeviceInDatabase(device: Device): Promise { - const db = await Database.getInstance(); - const walletStore = 'wallet'; - try { - console.log("[Services:saveDeviceInDatabase] 💾 Sauvegarde de l'appareil en BDD...", { - pairing_process_commitment: device.pairing_process_commitment, - paired_member: device.paired_member, - }); - - const prevDevice = await this.getDeviceFromDatabase(); - if (prevDevice) { - // console.debug('[Services:saveDeviceInDatabase] ℹ️ Appareil précédent trouvé, suppression...'); - await db.deleteObject(walletStore, '1'); - } - - await db.addObject({ - storeName: walletStore, - object: { pre_id: '1', device }, - key: null, - }); - - console.log('[Services:saveDeviceInDatabase] ✅ Appareil sauvegardé avec succès'); - - // // Verify save - // const savedDevice = await this.getDeviceFromDatabase(); - // console.log('[Services:saveDeviceInDatabase] 🔎 Vérification:', { - // pairing_process_commitment: savedDevice?.pairing_process_commitment, - // paired_member: savedDevice?.paired_member, - // }); - } catch (e) { - console.error('[Services:saveDeviceInDatabase] 💥 Erreur lors de la sauvegarde:', e); - } - } - - async getDeviceFromDatabase(): Promise { - const db = await Database.getInstance(); - const walletStore = 'wallet'; - try { - const dbRes = await db.getObject(walletStore, '1'); - if (dbRes) { - return dbRes['device']; - } else { - return null; - } - } catch (e) { - throw new Error(`[Services:getDeviceFromDatabase] 💥 Échec: ${e}`); - } - } - - async getMemberFromDevice(): Promise { - try { - const device = await this.getDeviceFromDatabase(); - if (device) { - const pairedMember = device['paired_member']; - return pairedMember.sp_addresses; - } else { - return null; - } - } catch (e) { - throw new Error(`[Services:getMemberFromDevice] 💥 Échec: ${e}`); - } - } - - isChildRole(parent: any, child: any): boolean { - try { - this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child)); - } catch (e) { - console.error(e); - return false; - } - - return true; - } - - rolesContainsUs(roles: Record): boolean { - let us; - try { - us = this.sdkClient.get_pairing_process_id(); - } catch (e) { - // Si non appairé, nous ne pouvons être dans aucun rôle - return false; - } - - return this.rolesContainsMember(roles, us); - } - - rolesContainsMember(roles: Record, pairingProcessId: string): boolean { - for (const roleDef of Object.values(roles)) { - if (roleDef.members.includes(pairingProcessId)) { - return true; - } - } - return false; - } - - async dumpWallet() { - const wallet = await this.sdkClient.dump_wallet(); - return wallet; - } - - public createFaucetMessage() { - const message = this.sdkClient.create_faucet_msg(); - return message; - } - - async createNewDevice() { - let spAddress = ''; - try { - console.log("[Services:createNewDevice] ✨ Création d'un nouvel appareil..."); - // We set birthday later when we have the chain tip from relay - spAddress = await this.sdkClient.create_new_device(0, 'signet'); - const device = this.dumpDeviceFromMemory(); - await this.saveDeviceInDatabase(device); - console.log('[Services:createNewDevice] ✅ Appareil créé et sauvegardé.'); - } catch (e) { - console.error('[Services:createNewDevice] 💥 Erreur:', e); - } - - return spAddress; - } - - public restoreDevice(device: Device) { - try { - console.log("[Services:restoreDevice] 🔄 Restauration de l'appareil en mémoire..."); - this.sdkClient.restore_device(device); - } catch (e) { - console.error(e); - } - } - - public async updateDeviceBlockHeight(): Promise { - if (this.currentBlockHeight === -1) { - console.warn('[Services:updateDeviceBlockHeight] ⚠️ Hauteur de bloc actuelle non définie. Skip.'); - return; - } - - let device: Device | null = null; - try { - device = await this.getDeviceFromDatabase(); - } catch (e) { - throw new Error(`[Services:updateDeviceBlockHeight] 💥 Échec de l'obtention de l'appareil depuis la BDD: ${e}`); - } - - if (!device) { - console.error('[Services:updateDeviceBlockHeight] 💥 Appareil non trouvé. Skip.'); - return; - } - - const birthday = device.sp_wallet.birthday; - if (birthday === undefined || birthday === null) { - console.error('[Services:updateDeviceBlockHeight] 💥 "Birthday" non trouvé. Skip.'); - return; - } - - if (birthday === 0) { - console.log(`[Services:updateDeviceBlockHeight] 🎂 C'est un nouvel appareil. Définition du "birthday" à ${this.currentBlockHeight}`); - // This is a new device, so current chain tip is its birthday - device.sp_wallet.birthday = this.currentBlockHeight; - // We also set last_scan, impossible that we need to scan earlier than this - device.sp_wallet.last_scan = this.currentBlockHeight; - try { - // First set the updated device in memory - this.sdkClient.restore_device(device); - // Then save it to database - await this.saveDeviceInDatabase(device); - } catch (e) { - throw new Error(`[Services:updateDeviceBlockHeight] 💥 Échec de la sauvegarde de l'appareil mis à jour: ${e}`); - } - } else { - // This is existing device, we need to catch up if last_scan is lagging behind chain_tip - if (device.sp_wallet.last_scan < this.currentBlockHeight) { - console.log(`[Services:updateDeviceBlockHeight] 🏃 Rattrapage... Scan des blocs de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`); - try { - await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL); - } catch (e) { - console.error(`[Services:updateDeviceBlockHeight] 💥 Échec du scan des blocs: ${e}`); - return; - } - - // If everything went well, we can update our storage - try { - const device = this.dumpDeviceFromMemory(); - await this.saveDeviceInDatabase(device); - console.log('[Services:updateDeviceBlockHeight] ✅ Scan terminé et appareil sauvegardé.'); - } catch (e) { - console.error(`[Services:updateDeviceBlockHeight] 💥 Échec de la sauvegarde de l'appareil après scan: ${e}`); - } - } else { - // console.debug('[Services:updateDeviceBlockHeight] ℹ️ Portefeuille à jour. Rien à faire.'); - return; - } - } - } - - private async removeProcess(processId: string): Promise { - const db = await Database.getInstance(); - const storeName = 'processes'; - - try { - console.log(`[Services:removeProcess] 🗑️ Suppression du processus ${processId}`); - await db.deleteObject(storeName, processId); - } catch (e) { - console.error(e); - } - } - - public async batchSaveProcessesToDb(processes: Record) { - if (Object.keys(processes).length === 0) { - return; - } - console.log(`[Services:batchSaveProcessesToDb] 💾 Sauvegarde de ${Object.keys(processes).length} processus en BDD...`); - const db = await Database.getInstance(); - const storeName = 'processes'; - try { - await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) }); - this.processesCache = { ...this.processesCache, ...processes }; - } catch (e) { - console.error('[Services:batchSaveProcessesToDb] 💥 Échec:', e); - throw e; - } - } - - public async saveProcessToDb(processId: string, process: Process) { - const db = await Database.getInstance(); - const storeName = 'processes'; - try { - await db.addObject({ - storeName, - object: process, - key: processId, - }); - - // Update the process in the cache - this.processesCache[processId] = process; - } catch (e) { - console.error(`[Services:saveProcessToDb] 💥 Échec de la sauvegarde du processus ${processId}: ${e}`); - } - } - - public async saveBlobToDb(hash: string, data: Blob) { - const db = await Database.getInstance(); - try { - await db.addObject({ - storeName: 'data', - object: data, - key: hash, - }); - } catch (e) { - console.error(`[Services:saveBlobToDb] 💥 Échec de la sauvegarde du blob ${hash}: ${e}`); - } - } - - public async getBlobFromDb(hash: string): Promise { - const db = await Database.getInstance(); - try { - return await db.getObject('data', hash); - } catch (e) { - return null; - } - } - - public async saveDataToStorage(storages: string[], hash: string, data: Blob, ttl: number | null) { - try { - await storeData(storages, hash, data, ttl); - } catch (e) { - console.error(`[Services:saveDataToStorage] 💥 Échec du stockage du hash ${hash}: ${e}`); - } - } - - public async fetchValueFromStorage(hash: string): Promise { - const storages = [STORAGEURL]; - - return await retrieveData(storages, hash); - } - - public async getDiffByValueFromDb(hash: string): Promise { - const db = await Database.getInstance(); - const diff = await db.getObject('diffs', hash); - return diff; } public async saveDiffsToDb(diffs: UserDiff[]) { const db = await Database.getInstance(); - try { - for (const diff of diffs) { - await db.addObject({ - storeName: 'diffs', - object: diff, - key: null, - }); + for (const d of diffs) await db.addObject({ storeName: 'diffs', object: d, key: null }); + } + + 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); + } } - } catch (e) { - throw new Error(`[Services:saveDiffsToDb] 💥 Échec: ${e}`); } } - public async getProcess(processId: string): Promise { - // 1. Essayer le cache en mémoire - if (this.processesCache[processId]) { - return this.processesCache[processId]; - } - - // 2. Si non trouvé, essayer la BDD - try { - const db = await Database.getInstance(); - const process = await db.getObject('processes', processId); - if (process) { - this.processesCache[processId] = process; // Mettre en cache + 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); } - return process; - } catch (e) { - console.error(`[Services:getProcess] 💥 Échec de récupération du processus ${processId}:`, e); - return null; } } - public async getProcesses(): Promise> { - // 1. Essayer le cache en mémoire - if (Object.keys(this.processesCache).length > 0) { - return this.processesCache; - } - - // 2. Si non trouvé, charger depuis la BDD - try { - console.log('[Services:getProcesses] ℹ️ Cache de processus vide. Chargement depuis la BDD...'); - const db = await Database.getInstance(); - this.processesCache = await db.dumpStore('processes'); - console.log(`[Services:getProcesses] ✅ ${Object.keys(this.processesCache).length} processus chargés en cache.`); - return this.processesCache; - } catch (e) { - console.error('[Services:getProcesses] 💥 Échec du chargement des processus:', e); - throw e; + public async 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 async restoreProcessesFromBackUp(processes: Record) { - console.log(`[Services:restoreProcessesFromBackUp] 💾 Restauration de ${Object.keys(processes).length} processus depuis un backup...`); + public rolesContainsUs(roles: any) { + return this.processService.rolesContainsMember(roles, this.getPairingProcessId()); + } + + public async getSecretForAddress(addr: string) { const db = await Database.getInstance(); - const storeName = 'processes'; - try { - await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) }); - } catch (e) { - throw e; - } - - await this.restoreProcessesFromDB(); + return await db.getObject('shared_secrets', addr); } - // Restore processes cache from persistent storage - public async restoreProcessesFromDB() { + public async getAllDiffs(): Promise> { const db = await Database.getInstance(); + return await db.dumpStore('diffs'); + } + + public async getDiffByValueFromDb(hash: string): Promise { + const db = await Database.getInstance(); + return await db.getObject('diffs', hash); + } + + public async getAllSecrets() { + const db = await Database.getInstance(); + return { + shared_secrets: await db.dumpStore('shared_secrets'), + unconfirmed_secrets: Object.values(await db.dumpStore('unconfirmed_secrets')), + }; + } + + // Storage & DB + public async saveBlobToDb(h: string, b: Blob) { + const db = await Database.getInstance(); + await db.addObject({ storeName: 'data', object: b, key: h }); + } + public async getBlobFromDb(h: string) { + const db = await Database.getInstance(); + return await db.getObject('data', 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); + } + public async getDiffByValue(val: string) { + const db = await Database.getInstance(); + return await db.getObject('diffs', val); + } + + // 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 processes: Record = await db.dumpStore('processes'); - if (processes && Object.keys(processes).length != 0) { - console.log(`[Services:restoreProcessesFromDB] 🔄 Restauration de ${Object.keys(processes).length} processus depuis la BDD vers le cache...`); - this.processesCache = processes; - } else { - console.log('[Services:restoreProcessesFromDB] ℹ️ Aucun processus à restaurer.'); - } + const res = this.sdkClient.parse_cipher(msg, this.membersList, await this.getProcesses()); + await this.handleApiReturn(res); } catch (e) { - throw 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) { - console.log('[Services:restoreSecretsFromBackUp] 💾 Restauration des secrets depuis un backup...'); const db = await Database.getInstance(); - - for (const secret of secretsStore.unconfirmed_secrets) { - await db.addObject({ - storeName: 'unconfirmed_secrets', - object: secret, - key: null, - }); - } - const entries = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value })); - for (const entry of entries) { - await db.addObject({ - storeName: 'shared_secrets', - object: entry.value, - key: entry.key, - }); - } - - // Now we can transfer them to memory + for (const secret of secretsStore.unconfirmed_secrets) await db.addObject({ storeName: 'unconfirmed_secrets', object: secret, key: null }); + for (const [key, value] of Object.entries(secretsStore.shared_secrets)) await db.addObject({ storeName: 'shared_secrets', object: value, key }); await this.restoreSecretsFromDB(); } - public async restoreSecretsFromDB() { - console.log('[Services:restoreSecretsFromDB] 🔄 Restauration des secrets depuis la BDD vers la mémoire SDK...'); const db = await Database.getInstance(); - try { - const sharedSecrets: Record = await db.dumpStore('shared_secrets'); - const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); - const secretsStore = { - shared_secrets: sharedSecrets, - unconfirmed_secrets: Object.values(unconfirmedSecrets), - }; - this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore)); - console.log(`[Services:restoreSecretsFromDB] ✅ ${Object.keys(sharedSecrets).length} secrets partagés restaurés.`); - } catch (e) { - throw e; - } + const sharedSecrets: Record = await db.dumpStore('shared_secrets'); + const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); + const secretsStore = { shared_secrets: sharedSecrets, unconfirmed_secrets: Object.values(unconfirmedSecrets) }; + this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore)); + } + public async createBackUp() { + const device = await this.walletService.getDeviceFromDatabase(); + if (!device) return null; + return { device, processes: await this.processService.getProcesses(), secrets: await this.getAllSecrets() }; } - decodeValue(value: number[]): any | null { - try { - return this.sdkClient.decode_value(value); - } catch (e) { - console.error(`[Services:decodeValue] 💥 Échec: ${e}`); - return null; - } - } - - // La fonction principale est maintenant beaucoup plus simple à lire. - // Elle sert d'orchestrateur. - async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { + public async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { console.groupCollapsed(`[Services:decryptAttribute] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`); try { @@ -1811,31 +847,23 @@ export default class Services { let key: string | null | undefined = state.keys[attribute]; const pairingProcessId = this.getPairingProcessId(); - // 1. Garde : A-t-on au moins le hash ? if (!hash) { console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`); return null; } - // 2. Si la clé est manquante, on la récupère if (!key) { - // 2a. Vérifier l'accès (logique extraite) if (!this._checkAccess(state, attribute, pairingProcessId)) { console.log(`⛔ Accès non autorisé. Abandon.`); return null; } - - // 2b. Tenter de récupérer la clé (logique extraite) const result = await this._fetchMissingKey(processId, state, attribute); - hash = result.hash; // Mettre à jour le hash (il a pu être rafraîchi) - key = result.key; // Mettre à jour la clé + hash = result.hash; + key = result.key; } - // 3. Si on a tout (soit depuis le début, soit après récupération) if (hash && key) { - console.log(`ℹ️ Clé et hash trouvés. Tentative de déchiffrement...`); const blob = await this.getBlobFromDb(hash); - if (!blob) { console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`); return null; @@ -1847,601 +875,65 @@ export default class Services { const keyUIntArray = this.hexToUInt8Array(key); const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher); - if (!clear) { - throw new Error('decrypt_data returned null'); - } + if (!clear) throw new Error('decrypt_data returned null'); const decoded = this.sdkClient.decode_value(clear); console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`); return decoded; } catch (e) { - console.error(`💥 Échec du déchiffrement (decrypt_data): ${e}`); + console.error(`💥 Échec du déchiffrement: ${e}`); return null; } } - - // 4. Échec final si la clé ou le hash manque toujours - console.error(`💥 Échec: Clé ou hash manquant après tentatives pour '${attribute}'.`); return null; } catch (error) { - console.error(`💥 Erreur inattendue dans decryptAttribute:`, error); + console.error(`💥 Erreur:`, error); return null; } finally { - // Garantit que le groupe principal est TOUJOURS fermé console.groupEnd(); } } - /** - * NOUVELLE MÉTHODE PRIVÉE - * Vérifie si l'utilisateur courant a le droit d'accéder à cet attribut - * en se basant sur les rôles du state. - */ private _checkAccess(state: ProcessState, attribute: string, pairingProcessId: string): boolean { const roles = state.roles; - - // Utilise .some() pour une lecture plus claire et un arrêt anticipé - // "Existe-t-il AU MOINS UN rôle..." return Object.values(roles).some((role) => { - // "...tel que l'utilisateur est membre..." const isMember = role.members.includes(pairingProcessId); - if (!isMember) { - return false; // Passe au rôle suivant - } - - // "...ET ce rôle a AU MOINS UNE règle de validation qui inclut cet attribut" + if (!isMember) return false; return Object.values(role.validation_rules).some((rule) => rule.fields.includes(attribute)); }); } - /** - * NOUVELLE MÉTHODE PRIVÉE (MODIFIÉE) - * Gère la logique de demande aux pairs en utilisant un système de Promise au lieu de polling. - */ private async _fetchMissingKey(processId: string, state: ProcessState, attribute: string): Promise<{ hash: string | null; key: string | null }> { - console.group(`🔐 Gestion de la clé manquante pour '${attribute}' (via Promise)`); - try { const process = await this.getProcess(processId); - if (!process) { - console.error(`💥 Impossible de trouver le processus ${processId} pour ensureConnections.`); - return { hash: null, key: null }; - } + if (!process) return { hash: null, key: null }; - // 1. Demander les connexions et envoyer la requête (comme avant) await this.ensureConnections(process); - console.log(`🗣️ Demande de données aux pairs...`); await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); - // 2. CRÉER LA PROMISE D'ATTENTE const requestId = `${processId}_${state.state_id}_${attribute}`; const keyRequestPromise = new Promise((resolve, reject) => { - // Sécurité : Définir un timeout (ex: 15 secondes) const timeout = setTimeout(() => { - console.warn(`⌛ Timeout: La clé pour '${attribute}' n'est jamais arrivée.`); - this.pendingKeyRequests.delete(requestId); // Nettoyer + this.pendingKeyRequests.delete(requestId); reject(new Error(`Timeout waiting for key: ${attribute}`)); - }, 15000); // 15 secondes + }, APP_CONFIG.TIMEOUTS.KEY_REQUEST); - // Enregistrer le "resolve" pour qu'il soit appelé de l'extérieur this.pendingKeyRequests.set(requestId, (key: string) => { - clearTimeout(timeout); // Annuler le timeout - console.log(`✅ Clé reçue via listener pour '${attribute}'!`); + clearTimeout(timeout); resolve(key); }); }); - // 3. Attendre la clé const receivedKey = await keyRequestPromise; - - // 4. Re-vérifier l'état (le hash a pu changer) const updatedProcess = await this.getProcess(processId); - if (!updatedProcess) { - return { hash: null, key: null }; - } + 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) { - console.error(`💥 Erreur durant _fetchMissingKey: ${e}`); return { hash: null, key: null }; - } finally { - console.groupEnd(); - } - } - - getNotifications(): any[] | null { - // ... (Logique inchangée) - return this.notifications; - } - - setNotifications(notifications: any[]) { - this.notifications = notifications; - } - - async importJSON(backup: BackUp): Promise { - console.log("[Services:importJSON] 📥 Importation d'un backup JSON..."); - const device = backup.device; - - // Reset current device - await this.resetDevice(); - - await this.saveDeviceInDatabase(device); - - this.restoreDevice(device); - - // TODO restore secrets and processes from file - const secretsStore = backup.secrets; - await this.restoreSecretsFromBackUp(secretsStore); - - const processes = backup.processes; - await this.restoreProcessesFromBackUp(processes); - console.log('[Services:importJSON] ✅ Backup importé avec succès.'); - } - - public async createBackUp(): Promise { - console.log("[Services:createBackUp] 📤 Création d'un backup..."); - // Get the device from indexedDB - const device = await this.getDeviceFromDatabase(); - if (!device) { - console.error('[Services:createBackUp] 💥 Aucun appareil chargé'); - return null; - } - - // Get the processes - const processes = await this.getProcesses(); - - // Get the shared secrets - const secrets = await this.getAllSecrets(); - - // Create a backup object - const backUp = { - device: device, - secrets: secrets, - processes: processes, - }; - console.log('[Services:createBackUp] ✅ Backup créé.'); - return backUp; - } - - // Device 1 wait Device 2 - public device1: boolean = false; - public device2Ready: boolean = false; - - public resetState() { - this.device1 = false; - this.device2Ready = false; - } - - // --- AMÉLIORATION: Refactorisé en sous-fonctions --- - public async handleHandshakeMsg(url: string, parsedMsg: any) { - try { - const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg); - - // 1. Mettre à jour les infos du relais (SP Address, Block Height) - this.updateRelayInfo(url, handshakeMsg); - - // 2. Mettre à jour la liste globale des membres - this.updateGlobalMembersList(handshakeMsg.peers_list); - - // 3. Lancer la synchronisation (lourde) des processus en arrière-plan - // (Anciennement dans un setTimeout, maintenant dans une fonction dédiée) - this.syncProcessesFromHandshake(handshakeMsg.processes_list, url).catch((e) => console.error(`[Services:syncProcessesFromHandshake] 💥 Échec de la synchro des processus pour ${url}:`, e)); - } catch (e) { - console.error(`[Services:handleHandshakeMsg] 💥 Échec de l'analyse du message init:`, e); - } - } - - /** - * Sous-fonction de handleHandshakeMsg: Met à jour les infos du relais. - */ - private updateRelayInfo(url: string, handshakeMsg: HandshakeMessage) { - if (handshakeMsg.sp_address) { - this.updateRelay(url, handshakeMsg.sp_address); - this.relayAddresses[url] = handshakeMsg.sp_address; - this.resolveRelayReady(); - } - - console.log(`[Services:updateRelayInfo] ℹ️ Handshake reçu de ${url}:`, handshakeMsg); - this.currentBlockHeight = handshakeMsg.chain_tip; - console.log(`[Services:updateRelayInfo] ⛓️ Hauteur de bloc actuelle: ${this.currentBlockHeight}`); - this.updateDeviceBlockHeight(); - } - - /** - * Sous-fonction de handleHandshakeMsg: Met à jour la liste globale des membres. - */ - private updateGlobalMembersList(peers_list: Record | null) { - console.log('[Services:updateGlobalMembersList] 🔄 Mise à jour de la liste des membres...'); - if (!peers_list || Object.keys(peers_list).length === 0) { - console.log('[Services:updateGlobalMembersList] ℹ️ Aucune liste de pairs reçue.'); - return; - } - - if (this.membersList && Object.keys(this.membersList).length === 0) { - console.log('[Services:updateGlobalMembersList] ⚠️ Écrasement de la membersList vide avec la liste des pairs reçue.'); - this.membersList = peers_list; - } else { - console.log('[Services:updateGlobalMembersList] ➕ Incrémentation de la membersList existante avec les nouveaux pairs.'); - for (const [processId, member] of Object.entries(peers_list)) { - this.membersList[processId] = member as Member; - } - } - console.log(`[Services:updateGlobalMembersList] ✅ Liste des membres mise à jour. Total: ${Object.keys(this.membersList).length}`); - } - - /** - * Sous-fonction de handleHandshakeMsg: Gère la logique complexe de synchro des processus. - */ - private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap, url: string) { - if (!newProcesses || Object.keys(newProcesses).length === 0) { - console.debug(`[Services:syncProcesses] ℹ️ Reçu une liste de processus vide de ${url}.`); - return; - } - - console.log(`[Services:syncProcesses] 🔄 Synchronisation de ${Object.keys(newProcesses).length} processus depuis ${url}...`); - - if (this.processesCache && Object.keys(this.processesCache).length === 0) { - // Cas simple: BDD vide, on sauvegarde tout - console.log('[Services:syncProcesses] ℹ️ Cache vide, sauvegarde en batch...'); - try { - await this.batchSaveProcessesToDb(newProcesses); - } catch (e) { - console.error('[Services:syncProcesses] 💥 Échec de la sauvegarde en batch:', e); - } - } else { - // Cas complexe: Mise à jour différentielle - const toSave: Record = {}; - for (const [processId, process] of Object.entries(newProcesses)) { - const existing = await this.getProcess(processId); - - if (existing) { - // --- Logique de mise à jour d'un processus existant --- - let newStates: string[] = []; - let newRoles: Record[] = []; - for (const state of process.states) { - if (!state || !state.state_id) continue; - - if (state.state_id === EMPTY32BYTES) { - // ... (Logique du 'tip') ... - const existingTip = existing.states[existing.states.length - 1].commited_in; - if (existingTip !== state.commited_in) { - console.log(`[Services:syncProcesses] ℹ️ Nouveau 'tip' trouvé pour ${processId}`); - existing.states.pop(); - existing.states.push(state); - toSave[processId] = existing; - } - } else if (!this.lookForStateId(existing, state.state_id)) { - // ... (Logique d'ajout de nouvel état) ... - console.log(`[Services:syncProcesses] ℹ️ Nouvel état ${state.state_id} trouvé pour ${processId}`); - const existingLastState = existing.states.pop(); - if (!existingLastState) { - console.error(`[Services:syncProcesses] 💥 Échec: impossible de trouver le dernier état pour ${processId}`); - break; - } - existing.states.push(state); - existing.states.push(existingLastState); - toSave[processId] = existing; - if (this.rolesContainsUs(state.roles)) { - newStates.push(state.state_id); - newRoles.push(state.roles); - } - } else { - // ... (Logique de vérification des clés pour un état existant) ... - const existingState = this.getStateFromId(existing, state.state_id); - if (existingState!.keys && Object.keys(existingState!.keys).length != 0) { - continue; // On a déjà les clés - } else { - if (this.rolesContainsUs(state.roles)) { - console.log(`[Services:syncProcesses] ℹ️ État ${state.state_id} (processus ${processId}) existe, mais clés manquantes. Demande...`); - newStates.push(state.state_id); - newRoles.push(state.roles); - } else { - continue; // Pas notre état - } - } - } - } - - if (newStates.length != 0) { - console.log(`[Services:syncProcesses] 📞 Demande de données pour ${newStates.length} nouvel(s) état(s) dans ${processId}`); - await this.ensureConnections(existing); - await this.requestDataFromPeers(processId, newStates, newRoles); - } - } else { - // Processus totalement nouveau, on l'ajoute - console.log(`[Services:syncProcesses] ℹ️ Nouveau processus ${processId} trouvé. Ajout.`); - toSave[processId] = process; - } - } - - if (toSave && Object.keys(toSave).length > 0) { - console.log('[Services:syncProcesses] 💾 Sauvegarde en batch des processus mis à jour/nouveaux...', Object.keys(toSave)); - await this.batchSaveProcessesToDb(toSave); - } - } - - console.log('[Services:syncProcesses] ✅ Synchronisation terminée. Émission de l\'événement "processes-updated".'); - document.dispatchEvent(new CustomEvent('processes-updated')); - } - - private lookForStateId(process: Process, stateId: string): boolean { - for (const state of process.states) { - if (state.state_id === stateId) { - return true; - } - } - - return false; - } - - /** - * Waits for at least one handshake message to be received from any connected relay. - */ - private async waitForHandshakeMessage(timeoutMs: number = 10000): Promise { - const startTime = Date.now(); - const pollInterval = 100; // Check every 100ms - - return new Promise((resolve, reject) => { - const checkForHandshake = () => { - // Check if we have any members or any relays (indicating handshake was received) - if (Object.keys(this.membersList).length > 0 || Object.values(this.relayAddresses).some((addr) => addr !== '')) { - console.log('[Services:waitForHandshakeMessage] ✅ Handshake reçu (membres ou relais présents)'); - resolve(); - return; - } - - // Check timeout - if (Date.now() - startTime >= timeoutMs) { - reject(new Error(`[Services:waitForHandshakeMessage] ❌ Aucun handshake reçu après ${timeoutMs}ms`)); - return; - } - - // Continue polling - setTimeout(checkForHandshake, pollInterval); - }; - - checkForHandshake(); - }); - } - - /** - * Retourne la liste de tous les membres ordonnés par leur process id - * @returns Un tableau contenant tous les membres - */ - public getAllMembersSorted(): Record { - return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))); - } - - public getAllMembers(): Record { - return this.membersList; - } - - public getAddressesForMemberId(memberId: string): string[] | null { - try { - // console.debug('[Services:getAddressesForMemberId] 🔍 Recherche d\'adresses pour:', memberId); - - if (Object.keys(this.membersList).length === 0) { - console.error('[Services:getAddressesForMemberId] ❌ ERREUR: membersList est VIDE. Impossible de trouver le membre:', memberId); - return null; - } - - if (!this.membersList[memberId]) { - console.error(`[Services:getAddressesForMemberId] ❌ ERREUR: Membre non trouvé dans membersList: ${memberId}`); - // console.debug('Membres actuels:', this.membersList); - return null; - } - - return this.membersList[memberId].sp_addresses; - } catch (e) { - console.error('[Services:getAddressesForMemberId] 💥 Erreur inattendue:', e); - return null; - } - } - - public compareMembers(memberA: string[], memberB: string[]): boolean { - if (!memberA || !memberB) { - return false; - } - if (memberA.length !== memberB.length) { - return false; - } - - const res = memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item)); - - return res; - } - - public async handleCommitError(response: string) { - const content = JSON.parse(response); - const error = content.error; - const errorMsg = error['GenericError']; - console.warn(`[Services:handleCommitError] ⚠️ Erreur de Commit reçue: ${errorMsg}`); - - const dontRetry = ['State is identical to the previous state', 'Not enough valid proofs', 'Not enough members to validate']; - if (dontRetry.includes(errorMsg)) { - console.warn('[Services:handleCommitError] ⏩ Erreur non récupérable. Pas de nouvelle tentative.'); - return; - } - // Wait and retry - console.warn('[Services:handleCommitError] 🔄 Tentative de renvoi du Commit dans 1s...'); - setTimeout(async () => { - this.sendCommitMessage(JSON.stringify(content)); - }, 1000); - } - - public getRoles(process: Process): Record | null { - const lastCommitedState = this.getLastCommitedState(process); - if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) { - return lastCommitedState!.roles; - } else if (process.states.length === 2) { - // Cas spécial pour les processus venant d'être créés ? - const firstState = process.states[0]; - if (firstState && firstState.roles && Object.keys(firstState.roles).length != 0) { - return firstState!.roles; - } - } - return null; - } - - public getPublicData(process: Process): Record | null { - const lastCommitedState = this.getLastCommitedState(process); - if (lastCommitedState && lastCommitedState.public_data && Object.keys(lastCommitedState.public_data).length != 0) { - return lastCommitedState!.public_data; - } else if (process.states.length === 2) { - const firstState = process.states[0]; - if (firstState && firstState.public_data && Object.keys(firstState.public_data).length != 0) { - return firstState!.public_data; - } - } - return null; - } - - public getProcessName(process: Process): string | null { - const lastCommitedState = this.getLastCommitedState(process); - if (lastCommitedState && lastCommitedState.public_data) { - const processName = lastCommitedState!.public_data['processName']; - if (processName) { - return this.decodeValue(processName); - } else { - return null; - } - } else { - return null; - } - } - - public async getMyProcesses(): Promise { - // If we're not paired yet, just skip it - let pairingProcessId = null; - try { - pairingProcessId = this.getPairingProcessId(); - } catch (e) { - return null; // Pas appairé - } - if (!pairingProcessId) { - return null; - } - - try { - const processes = await this.getProcesses(); - - const newMyProcesses = new Set(this.myProcesses || []); - // MyProcesses automatically contains pairing process - newMyProcesses.add(pairingProcessId); - for (const [processId, process] of Object.entries(processes)) { - if (newMyProcesses.has(processId)) { - continue; - } - try { - const roles = this.getRoles(process); - - if (roles && this.rolesContainsUs(roles)) { - newMyProcesses.add(processId); - } - } catch (e) { - console.error(e); - } - } - this.myProcesses = newMyProcesses; // atomic update - return Array.from(this.myProcesses); - } catch (e) { - console.error('[Services:getMyProcesses] 💥 Échec:', e); - return null; - } - } - - public async requestDataFromPeers(processId: string, stateIds: string[], roles: Record[]) { - console.log(`[Services:requestDataFromPeers] 🗣️ Demande de données pour ${processId} (états: ${stateIds.join(', ')})`); - const membersList = this.getAllMembers(); - try { - const res = this.sdkClient.request_data(processId, stateIds, roles, membersList); - await this.handleApiReturn(res); - } catch (e) { - console.error(e); - } - } - - public hexToBlob(hexString: string): Blob { - const uint8Array = this.hexToUInt8Array(hexString); - - return new Blob([uint8Array], { type: 'application/octet-stream' }); - } - - public hexToUInt8Array(hexString: string): Uint8Array { - if (hexString.length % 2 !== 0) { - throw new Error('Invalid hex string: length must be even'); - } - const uint8Array = new Uint8Array(hexString.length / 2); - for (let i = 0; i < hexString.length; i += 2) { - uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16); - } - - return uint8Array; - } - - public async blobToHex(blob: Blob): Promise { - const buffer = await blob.arrayBuffer(); - const bytes = new Uint8Array(buffer); - return Array.from(bytes) - .map((byte) => byte.toString(16).padStart(2, '0')) - .join(''); - } - - public getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): string { - return this.sdkClient.hash_value(fileBlob, commitedIn, label); - } - - public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult { - return this.sdkClient.get_merkle_proof(processState, attributeName); - } - - public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean { - try { - return this.sdkClient.validate_merkle_proof(proof, hash); - } catch (e) { - throw new Error(`[Services:validateMerkleProof] 💥 Échec: ${e}`); - } - } - - public getLastCommitedState(process: Process): ProcessState | null { - if (process.states.length === 0) return null; - const processTip = process.states[process.states.length - 1].commited_in; - const lastCommitedState = process.states.findLast((state) => state.commited_in !== processTip); - if (lastCommitedState) { - return lastCommitedState; - } else { - return null; - } - } - - public getLastCommitedStateIndex(process: Process): number | null { - if (process.states.length === 0) return null; - const processTip = process.states[process.states.length - 1].commited_in; - for (let i = process.states.length - 1; i >= 0; i--) { - if (process.states[i].commited_in !== processTip) { - return i; - } - } - return null; - } - - public getUncommitedStates(process: Process): ProcessState[] { - if (process.states.length === 0) return []; - const processTip = process.states[process.states.length - 1].commited_in; - const res = process.states.filter((state) => state.commited_in === processTip); - return res.filter((state) => state.state_id !== EMPTY32BYTES); - } - - public getStateFromId(process: Process, stateId: string): ProcessState | null { - if (process.states.length === 0) return null; - const state = process.states.find((state) => state.state_id === stateId); - if (state) { - return state; - } else { - return null; } } } diff --git a/src/services/token.ts b/src/services/token.service.ts similarity index 100% rename from src/services/token.ts rename to src/services/token.service.ts diff --git a/src/services/websockets.service.ts b/src/services/websockets.service.ts index 9bbe53e..117e2fa 100755 --- a/src/services/websockets.service.ts +++ b/src/services/websockets.service.ts @@ -1,10 +1,11 @@ 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 = 1000; // Délai initial de 1s avant reconnexion -const MAX_RECONNECT_INTERVAL = 30000; // Max 30s +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; @@ -24,7 +25,7 @@ function connect() { ws.onopen = async () => { console.log('[WS] ✅ Connexion établie !'); isConnecting = false; - reconnectInterval = 1000; // Reset du délai + reconnectInterval = APP_CONFIG.TIMEOUTS.RETRY_DELAY; // Reset du délai // Démarrer le Heartbeat (Ping pour garder la connexion vivante) startHeartbeat(); @@ -95,7 +96,7 @@ function startHeartbeat() { // Adapter selon ce que ton serveur attend comme Ping, ou envoyer un message vide // ws.send(JSON.stringify({ flag: 'Ping', content: '' })); } - }, 30000); + }, APP_CONFIG.TIMEOUTS.WS_HEARTBEAT); } function stopHeartbeat() {