import * as Comlink from "comlink"; import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff, } from "../../pkg/sdk_client"; import Database from "../services/database.service"; import { storeData, retrieveData } from "../services/storage.service"; import { BackUp } from "../types/index"; import { APP_CONFIG } from "../config/constants"; import { splitPrivateData } from "../utils/service.utils"; // Services internes au worker import { WalletService } from "../services/wallet.service"; import { ProcessService } from "../services/process.service"; import { CryptoService } from "../services/crypto.service"; // Types for services passed from main thread type WasmServiceProxy = { callMethod(method: string, ...args: any[]): Promise; parseCipher(msg: string, membersList: any, processes: any): Promise; parseNewTx(msg: string, blockHeight: number, membersList: any): Promise; createTransaction(addresses: string[], feeRate: number): Promise; signTransaction(partialTx: any): Promise; getTxid(transaction: string): Promise; getOpReturn(transaction: string): Promise; getPrevouts(transaction: string): Promise; createProcess(privateData: any, publicData: any, roles: any, membersList: any): Promise; createNewProcess(privateData: any, roles: any, publicData: any, relayAddress: string, feeRate: number, membersList: any): Promise; updateProcess(process: any, privateData: any, publicData: any, roles: any, membersList: any): Promise; createUpdateMessage(process: any, stateId: string, membersList: any): Promise; createPrdResponse(process: any, stateId: string, membersList: any): Promise; validateState(process: any, stateId: string, membersList: any): Promise; refuseState(process: any, stateId: string): Promise; requestData(processId: string, stateIds: string[], roles: any, membersList: any): Promise; processCommitNewState(process: any, newStateId: string, newTip: string): Promise; encodeJson(data: any): Promise; encodeBinary(data: any): Promise; decodeValue(value: number[]): Promise; createFaucetMessage(): Promise; isChildRole(parent: any, child: any): Promise; // Wallet methods isPaired(): Promise; getAvailableAmount(): Promise; getAddress(): Promise; getPairingProcessId(): Promise; createNewDevice(birthday: number, network: string): Promise; dumpDevice(): Promise; dumpNeuteredDevice(): Promise; dumpWallet(): Promise; restoreDevice(device: any): Promise; pairDevice(processId: string, spAddresses: string[]): Promise; unpairDevice(): Promise; resetDevice(): Promise; // Crypto methods hashValue(fileBlob: { type: string; data: Uint8Array }, commitedIn: string, label: string): Promise; getMerkleProof(processState: any, attributeName: string): Promise; validateMerkleProof(proof: any, hash: string): Promise; decryptData(key: Uint8Array, data: Uint8Array): Promise; // Secrets management setSharedSecrets(secretsJson: string): Promise; // Blockchain scanning scanBlocks(tipHeight: number, blindbitUrl: string): Promise; }; type DatabaseServiceProxy = { getStoreList(): Promise<{ [key: string]: string }>; addObject(payload: { storeName: string; object: any; key: any }): Promise; batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise; getObject(storeName: string, key: string): Promise; dumpStore(storeName: string): Promise>; deleteObject(storeName: string, key: string): Promise; clearStore(storeName: string): Promise; requestStoreByIndex(storeName: string, indexName: string, request: string): Promise; clearMultipleStores(storeNames: string[]): Promise; saveDevice(device: any): Promise; getDevice(): Promise; saveProcess(processId: string, process: any): Promise; saveProcessesBatch(processes: Record): Promise; getProcess(processId: string): Promise; getAllProcesses(): Promise>; saveBlob(hash: string, data: Blob): Promise; getBlob(hash: string): Promise; saveDiffs(diffs: any[]): Promise; getDiff(hash: string): Promise; getAllDiffs(): Promise>; getSharedSecret(address: string): Promise; saveSecretsBatch(unconfirmedSecrets: any[], sharedSecrets: { key: string; value: any }[]): Promise; getAllSecrets(): Promise<{ shared_secrets: Record; unconfirmed_secrets: any[] }>; }; export class CoreBackend { // Services (passed from main thread via Comlink) private wasmService!: WasmServiceProxy; private db!: DatabaseServiceProxy; // Domain services private walletService!: WalletService; private processService!: ProcessService; private cryptoService!: CryptoService; // État (State) private processId: string | null = null; private stateId: string | null = null; private membersList: Record = {}; private notifications: any[] | null = null; private currentBlockHeight: number = -1; private pendingKeyRequests: Map void> = new Map(); // Flags publics (State) public device1: boolean = false; public device2Ready: boolean = false; private isInitialized = false; // Callbacks vers le Main Thread private notifier: ((event: string, data?: any) => void) | null = null; private networkSender: ((flag: string, content: string) => void) | null = null; private relayUpdater: ((url: string, sp: string) => void) | null = null; private relayGetter: (() => Promise) | null = null; constructor() { // Services will be set via setServices() from main thread } public async init(): Promise { if (this.isInitialized) return; console.log("[CoreWorker] ⚙️ Initialisation du Backend..."); // Services must be set before init if (!this.wasmService || !this.db) { throw new Error("Services must be set via setServices() before init()"); } // Initialize domain services with WASM and Database proxies this.cryptoService = new CryptoService(this.wasmService); this.walletService = new WalletService(this.wasmService, this.db); // @ts-ignore - ProcessService accepts DatabaseServiceProxy via Comlink but TypeScript sees Database type this.processService = new ProcessService(this.db); this.notifications = this.getNotifications(); this.isInitialized = true; console.log("[CoreWorker] ✅ Backend prêt."); } // --- CONFIGURATION DES SERVICES (depuis Main Thread) --- public setServices( wasmService: WasmServiceProxy, db: DatabaseServiceProxy ) { this.wasmService = wasmService; this.db = db; } // --- CONFIGURATION DES CALLBACKS --- public setCallbacks( notifier: (event: string, data?: any) => void, networkSender: (flag: string, content: string) => void, relayUpdater: (url: string, sp: string) => void, relayGetter: () => Promise ) { this.notifier = notifier; this.networkSender = networkSender; this.relayUpdater = relayUpdater; this.relayGetter = relayGetter; } // ========================================== // GETTERS & SETTERS (STATE) // ========================================== public setProcessId(id: string | null) { this.processId = id; } public setStateId(id: string | null) { this.stateId = id; } public getProcessId() { return this.processId; } public getStateId() { return this.stateId; } public getDevice1() { return this.device1; } // Ajouté public getDevice2Ready() { return this.device2Ready; } // Ajouté public resetState() { this.device1 = false; this.device2Ready = false; } // ========================================== // WALLET PROXY // ========================================== public async isPaired() { return await this.walletService.isPaired(); } public async getAmount() { return await this.walletService.getAmount(); } public async getDeviceAddress() { return await this.walletService.getDeviceAddress(); } public async dumpDeviceFromMemory() { return await this.walletService.dumpDeviceFromMemory(); } public async dumpNeuteredDevice() { return await this.walletService.dumpNeuteredDevice(); } public async getPairingProcessId() { return await this.walletService.getPairingProcessId(); } public async getDeviceFromDatabase() { return this.walletService.getDeviceFromDatabase(); } public async restoreDevice(d: Device) { await this.walletService.restoreDevice(d); } public async pairDevice(pid: string, list: string[]) { await this.walletService.pairDevice(pid, list); } public async unpairDevice() { await this.walletService.unpairDevice(); } public async saveDeviceInDatabase(d: Device) { await this.walletService.saveDeviceInDatabase(d); } public async createNewDevice() { return this.walletService.createNewDevice( this.currentBlockHeight > 0 ? this.currentBlockHeight : 0 ); } public async dumpWallet() { return this.walletService.dumpWallet(); } public async getMemberFromDevice() { return this.walletService.getMemberFromDevice(); } // ========================================== // PROCESS PROXY // ========================================== public async getProcess(id: string) { return this.processService.getProcess(id); } public async getProcesses() { return this.processService.getProcesses(); } public async restoreProcessesFromDB() { await this.processService.getProcesses(); } public getLastCommitedState(p: Process) { return this.processService.getLastCommitedState(p); } public getUncommitedStates(p: Process) { return this.processService.getUncommitedStates(p); } public getStateFromId(p: Process, id: string) { return this.processService.getStateFromId(p, id); } public getRoles(p: Process) { return this.processService.getRoles(p); } public getLastCommitedStateIndex(p: Process) { return this.processService.getLastCommitedStateIndex(p); } public async batchSaveProcessesToDb(p: Record) { return this.processService.batchSaveProcesses(p); } // ========================================== // CRYPTO HELPERS // ========================================== public async decodeValue(val: number[]) { return await this.wasmService.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 async getHashForFile(c: string, l: string, f: any) { return await this.cryptoService.getHashForFile(c, l, f); } public async getMerkleProofForFile(s: ProcessState, a: string) { return await this.cryptoService.getMerkleProofForFile(s, a); } public async validateMerkleProof(p: MerkleProofResult, h: string) { return await this.cryptoService.validateMerkleProof(p, h); } private splitData(obj: Record) { return this.cryptoService.splitData(obj); } // ========================================== // MEMBERS // ========================================== public getAllMembers() { return this.membersList; } public getAllMembersSorted() { return Object.fromEntries( Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB) ) ); } public async ensureMembersAvailable(): Promise { if (Object.keys(this.membersList).length > 0) return; console.warn("[CoreWorker] Tentative de récupération des membres..."); } public getAddressesForMemberId(memberId: string): string[] | null { if (!this.membersList[memberId]) return null; return this.membersList[memberId].sp_addresses; } public compareMembers(memberA: string[], memberB: string[]): boolean { if (!memberA || !memberB) return false; if (memberA.length !== memberB.length) return false; return ( memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item)) ); } // ========================================== // UTILITAIRES DIVERS // ========================================== public async createFaucetMessage() { return await this.wasmService.createFaucetMessage(); } public async isChildRole(parent: any, child: any): Promise { try { return await this.wasmService.isChildRole(parent, child); } catch (e) { console.error(e); return false; } } // ========================================== // LOGIQUE HANDSHAKE // ========================================== public async handleHandshakeMsg(url: string, parsedMsg: any) { try { const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg); if (handshakeMsg.sp_address && this.relayUpdater) { await this.relayUpdater(url, handshakeMsg.sp_address); } this.currentBlockHeight = handshakeMsg.chain_tip; if (!(await this.isPaired())) { console.log(`[CoreWorker] ⏳ Non pairé. Attente appairage...`); } this.updateDeviceBlockHeight(); if (handshakeMsg.peers_list) { this.membersList = { ...this.membersList, ...(handshakeMsg.peers_list as Record), }; } if (handshakeMsg.processes_list) { await this.syncProcessesFromHandshake(handshakeMsg.processes_list); } } catch (e) { console.error("Handshake Error", e); } } public async updateDeviceBlockHeight() { if (this.currentBlockHeight <= 0) return; const device = await this.walletService.getDeviceFromDatabase(); if (!device) return; if (device.sp_wallet.birthday === 0) { device.sp_wallet.birthday = this.currentBlockHeight; device.sp_wallet.last_scan = this.currentBlockHeight; await this.walletService.saveDeviceInDatabase(device); await this.walletService.restoreDevice(device); } else if (device.sp_wallet.last_scan < this.currentBlockHeight) { console.log( `[CoreWorker] Scan requis de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}` ); try { await this.wasmService.scanBlocks(this.currentBlockHeight, APP_CONFIG.URLS.BLINDBIT); const updatedDevice = await this.walletService.dumpDeviceFromMemory(); await this.walletService.saveDeviceInDatabase(updatedDevice); } catch (e) { console.error("Scan error", e); } } } private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap) { if (!newProcesses || Object.keys(newProcesses).length === 0) return; const toSave: Record = {}; const currentProcesses = await this.getProcesses(); let updatesCount = 0; // Compteur de changements réels if (Object.keys(currentProcesses).length === 0) { await this.processService.batchSaveProcesses(newProcesses); updatesCount = Object.keys(newProcesses).length; } else { for (const [processId, process] of Object.entries(newProcesses)) { const existing = currentProcesses[processId]; if (existing) { let hasChanged = false; // On tracke si ce process a bougé 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; hasChanged = true; } } 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; hasChanged = true; if (await this.rolesContainsUs(state.roles)) { newStates.push(state.state_id); newRoles.push(state.roles); } } } else { // Logique existante pour les clés manquantes const existingState = this.processService.getStateFromId( existing, state.state_id ); if ( existingState && (!existingState.keys || Object.keys(existingState.keys).length === 0) ) { if (await this.rolesContainsUs(state.roles)) { newStates.push(state.state_id); newRoles.push(state.roles); // Ici on ne marque pas forcément hasChanged pour la DB, mais on demande les clés } } } } if (hasChanged) updatesCount++; if (newStates.length > 0) { await this.ensureConnections(existing); await this.requestDataFromPeers(processId, newStates, newRoles); } } else { // C'est un nouveau process qu'on ne connaissait pas toSave[processId] = process; updatesCount++; } } if (Object.keys(toSave).length > 0) { await this.processService.batchSaveProcesses(toSave); } } // ✅ ON LOG ET NOTIFIE SEULEMENT SI QUELQUE CHOSE A CHANGÉ if (updatesCount > 0) { console.log( `[CoreWorker] 🔄 Synchro effectuée : ${updatesCount} processus mis à jour.` ); if (this.notifier) this.notifier("processes-updated"); } else { // Silence radio si les données reçues sont déjà connues (idempotence) } } // ========================================== // LOGIQUE MÉTIER // ========================================== public async getMyProcesses(): Promise { try { if (!(await this.isPaired())) { return null; } const pid = await this.getPairingProcessId(); return await this.processService.getMyProcesses(pid); } catch (e) { return null; } } public async ensureConnections( process: Process, stateId: string | null = null ): Promise { 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 = await this.decodeValue(publicData["pairedAddresses"]); if (decoded) members.add({ sp_addresses: decoded }); } } if (members.size === 0) return; const unconnected = new Set(); const myAddress = await this.getDeviceAddress(); for (const member of Array.from(members)) { if (!member.sp_addresses) continue; for (const address of member.sp_addresses) { if (address === myAddress) continue; if ((await this.getSecretForAddress(address)) === null) unconnected.add(address); } } if (unconnected.size > 0) { console.log( `[CoreWorker] 📡 ${unconnected.size} non connectés. Connexion...` ); await this.connectAddresses(Array.from(unconnected)); } } public async connectAddresses( addresses: string[] ): Promise { if (addresses.length === 0) return null; const feeRate = APP_CONFIG.FEE_RATE; try { return await this.wasmService.createTransaction(addresses, feeRate); } catch (error: any) { if (String(error).includes("Insufficient funds")) { await this.getTokensFromFaucet(); return await this.wasmService.createTransaction(addresses, feeRate); } else { throw error; } } } private async getTokensFromFaucet(): Promise { console.log("[CoreWorker] 🚰 Demande Faucet..."); const availableAmt = await this.getAmount(); const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10); if (availableAmt < target) { const msg = await this.wasmService.createFaucetMessage(); if (this.networkSender) this.networkSender("Faucet", msg); let attempts = 3; while (attempts > 0) { if ((await this.getAmount()) >= target) return; attempts--; await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.RETRY_DELAY) ); } throw new Error("Montant insuffisant après faucet"); } } public async createPairingProcess( userName: string, pairWith: string[] ): Promise { if (await this.isPaired()) throw new Error("Déjà appairé"); const myAddress = await this.getDeviceAddress(); pairWith.push(myAddress); const privateData = { description: "pairing", counter: 0 }; const publicData = { memberPublicName: userName, pairedAddresses: pairWith, }; const validation_fields = [ ...Object.keys(privateData), ...Object.keys(publicData), "roles", ]; const roles = { pairing: { members: [], validation_rules: [ { quorum: 1.0, fields: validation_fields, min_sig_member: 1.0 }, ], storages: [APP_CONFIG.URLS.STORAGE], }, }; return this.createProcess(privateData, publicData, roles); } public async createProcess( privateData: any, publicData: any, roles: any, feeRate = APP_CONFIG.FEE_RATE ): Promise { // Appel au main thread pour avoir l'adresse du relais const relay = this.relayGetter ? await this.relayGetter() : ""; if (!relay) throw new Error("Aucun relais disponible"); const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); const members = this.membersList; try { return await this.attemptCreateProcess( encodedPrivateData, roles, encodedPublicData, relay, feeRate, members ); } catch (e: any) { if (String(e).includes("Insufficient funds")) { await this.getTokensFromFaucet(); return await this.attemptCreateProcess( encodedPrivateData, roles, encodedPublicData, relay, feeRate, members ); } throw e; } } private async attemptCreateProcess( priv: any, roles: any, pub: any, relay: string, fee: number, members: any ): Promise { const res = await this.wasmService.createNewProcess(priv, roles, pub, relay, fee, members); if (res.updated_process) { await this.ensureConnections(res.updated_process.current_process); } return res; } public async updateProcess( processId: string, newData: any, privateFields: string[], roles: any ): Promise { const process = await this.processService.getProcess(processId); if (!process) throw new Error("Process not found"); let lastState = this.processService.getLastCommitedState(process); let currentProcess = process; if (!lastState) { const first = process.states[0]; if (await this.rolesContainsUs(first.roles)) { const appRes = await this.approveChange(processId, first.state_id); await this.handleApiReturn(appRes); const prdRes = await this.createPrdUpdate(processId, first.state_id); await this.handleApiReturn(prdRes); } else if (first.validation_tokens.length > 0) { const res = await this.createPrdUpdate(processId, first.state_id); await this.handleApiReturn(res); } const updated = await this.processService.getProcess(processId); if (updated) currentProcess = updated; lastState = this.processService.getLastCommitedState(currentProcess); if (!lastState) throw new Error("Still no commited state"); } const lastStateIndex = this.getLastCommitedStateIndex(currentProcess); if (lastStateIndex === null) throw new Error("Index commited introuvable"); const privateData: any = {}; const publicData: any = {}; for (const field of Object.keys(newData)) { if (lastState.public_data[field]) { publicData[field] = newData[field]; continue; } if (privateFields.includes(field)) { privateData[field] = newData[field]; continue; } let isPrivate = false; for (let i = lastStateIndex; i >= 0; i--) { if (currentProcess.states[i].pcd_commitment[field]) { privateData[field] = newData[field]; isPrivate = true; break; } } if (!isPrivate) publicData[field] = newData[field]; } const finalRoles = roles || this.processService.getRoles(currentProcess); const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); const res = await this.wasmService.updateProcess( currentProcess, encodedPrivateData, encodedPublicData, finalRoles, this.membersList ); if (res.updated_process) await this.ensureConnections(res.updated_process.current_process); return res; } private async prepareProcessData(priv: any, pub: any) { const p1 = this.splitData(priv); const p2 = this.splitData(pub); return { encodedPrivateData: { ...(await this.wasmService.encodeJson(p1.jsonCompatibleData)), ...(await this.wasmService.encodeBinary(p1.binaryData)), }, encodedPublicData: { ...(await this.wasmService.encodeJson(p2.jsonCompatibleData)), ...(await this.wasmService.encodeBinary(p2.binaryData)), }, }; } // ========================================== // API METHODS (Actions) // ========================================== public async createPrdUpdate(pid: string, sid: string) { const p = await this.getProcess(pid); await this.ensureConnections(p!); return await this.wasmService.createUpdateMessage(p, sid, this.membersList); } public async createPrdResponse(pid: string, sid: string) { const p = await this.getProcess(pid); return await this.wasmService.createPrdResponse(p, sid, this.membersList); } public async approveChange(pid: string, sid: string) { const p = await this.getProcess(pid); const res = await this.wasmService.validateState(p, sid, this.membersList); if (res.updated_process) await this.ensureConnections(res.updated_process.current_process); return res; } public async rejectChange(pid: string, sid: string) { const p = await this.getProcess(pid); return await this.wasmService.refuseState(p, sid); } public async requestDataFromPeers(pid: string, sids: string[], roles: any) { const res = await this.wasmService.requestData(pid, sids, roles, this.membersList); await this.handleApiReturn(res); } public async resetDevice() { await this.wasmService.resetDevice(); await this.db.clearMultipleStores([ "wallet", "shared_secrets", "unconfirmed_secrets", "processes", "diffs", ]); } public async handleApiReturn(res: ApiReturn) { if (!res || Object.keys(res).length === 0) return; try { const txData = (res.partial_tx ? await this.handlePartialTx(res.partial_tx) : null) || res.new_tx_to_send; if (txData && txData.transaction.length != 0) { if (this.networkSender) this.networkSender("NewTx", JSON.stringify(txData)); await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.API_DELAY)); } if (res.secrets) await this.handleSecrets(res.secrets); if (res.updated_process) await this.handleUpdatedProcess(res.updated_process); if (res.push_to_storage) await this.handlePushToStorage(res.push_to_storage); if (res.commit_to_send && this.networkSender) this.networkSender("Commit", JSON.stringify(res.commit_to_send)); if (res.ciphers_to_send && this.networkSender) for (const c of res.ciphers_to_send) this.networkSender("Cipher", c); } catch (e) { console.error("ApiReturn Error:", e); } } private async handlePartialTx(partialTx: any): Promise { try { const result = await this.wasmService.signTransaction(partialTx); return result.new_tx_to_send; } catch (e) { return null; } } private async handleSecrets(secrets: any) { const { unconfirmed_secrets, shared_secrets } = secrets; const unconfirmedList = unconfirmed_secrets && unconfirmed_secrets.length > 0 ? unconfirmed_secrets : []; const sharedList = shared_secrets && Object.keys(shared_secrets).length > 0 ? Object.entries(shared_secrets).map(([key, value]) => ({ key, value })) : []; if (unconfirmedList.length > 0 || sharedList.length > 0) { await this.db.saveSecretsBatch(unconfirmedList, sharedList); } } private async handleUpdatedProcess(updated: any) { const pid = updated.process_id; if (updated.encrypted_data) { for (const [h, c] of Object.entries( updated.encrypted_data as Record )) await this.saveBlobToDb(h, this.hexToBlob(c)); } await this.processService.saveProcessToDb(pid, updated.current_process); if (updated.diffs) await this.saveDiffsToDb(updated.diffs); this._resolvePendingKeyRequests(pid, updated.current_process); // Notification UI if (this.notifier) this.notifier("processes-updated"); } public async saveDiffsToDb(diffs: UserDiff[]) { await this.db.saveDiffs(diffs); } private _resolvePendingKeyRequests(processId: string, process: Process) { if (this.pendingKeyRequests.size === 0) return; for (const state of process.states) { if (!state.keys) continue; for (const [attr, key] of Object.entries(state.keys)) { const rid = `${processId}_${state.state_id}_${attr}`; if (this.pendingKeyRequests.has(rid)) { this.pendingKeyRequests.get(rid)?.(key as string); this.pendingKeyRequests.delete(rid); } } } } private async handlePushToStorage(hashes: string[]) { for (const hash of hashes) { try { const blob = await this.getBlobFromDb(hash); const diff = await this.getDiffByValue(hash); if (blob && diff) await this.saveDataToStorage(diff.storages, hash, blob, null); } catch (e) { console.error("Push error", e); } } } public async handleCommitError(response: string) { const content = JSON.parse(response); const errorMsg = content.error["GenericError"]; if ( ![ "State is identical to the previous state", "Not enough valid proofs", ].includes(errorMsg) ) { // Retry via network callback if (this.networkSender) { setTimeout( () => this.networkSender!("Commit", JSON.stringify(content)), APP_CONFIG.TIMEOUTS.RETRY_DELAY ); } } } public async rolesContainsUs(roles: any) { try { if (!(await this.isPaired())) { return false; } return this.processService.rolesContainsMember( roles, await this.getPairingProcessId() ); } catch (e) { console.error("RolesContainsUs Error:", e); return false; } } public async getSecretForAddress(address: string): Promise { return await this.db.getSharedSecret(address); } public async getAllDiffs(): Promise> { return await this.db.getAllDiffs(); } public async getDiffByValue(value: string): Promise { return await this.db.getDiff(value); } public async getAllSecrets(): Promise { return await this.db.getAllSecrets(); } // ========================================== // STORAGE & DB // ========================================== public async saveBlobToDb(h: string, d: Blob) { await this.db.saveBlob(h, d); } public async getBlobFromDb(h: string) { return await this.db.getBlob(h); } public async fetchValueFromStorage(h: string) { return retrieveData([APP_CONFIG.URLS.STORAGE], h); } public async saveDataToStorage( s: string[], h: string, d: Blob, ttl: number | null ) { return storeData(s, h, d, ttl); } // ========================================== // HELPERS // ========================================== public getProcessName(p: Process) { const pub = this.getPublicData(p); if (pub && pub["processName"]) return this.decodeValue(pub["processName"]); return null; } public getPublicData(p: Process) { const last = this.getLastCommitedState(p); return last ? last.public_data : p.states[0]?.public_data || null; } public updateMemberPublicName(pid: string, name: string) { return this.updateProcess(pid, { memberPublicName: name }, [], null); } // ========================================== // UI HELPERS // ========================================== public getNotifications() { return this.notifications; } public setNotifications(n: any[]) { this.notifications = n; } // ========================================== // PARSING & RESEAU ENTRANT // ========================================== async parseCipher(msg: string) { try { const res = await this.wasmService.parseCipher(msg, this.membersList, await this.getProcesses()); await this.handleApiReturn(res); } catch (e) { console.error("Cipher Error", e); } } async parseNewTx(msg: string) { const parsed = JSON.parse(msg); if (parsed.error) return; const prevouts = await this.wasmService.getPrevouts(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 = await this.wasmService.getTxid(parsed.transaction); const newStateId = await this.wasmService.getOpReturn(parsed.transaction); await this.wasmService.processCommitNewState(p, newStateId, newTip); break; } } try { const res = await this.wasmService.parseNewTx(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 = await this.dumpDeviceFromMemory(); const old = await this.getDeviceFromDatabase(); if (old && old.pairing_process_commitment) d.pairing_process_commitment = old.pairing_process_commitment; await this.saveDeviceInDatabase(d); } } catch (e) {} } // ========================================== // BACKUP & RESTORE // ========================================== public async importJSON(backup: BackUp) { await this.resetDevice(); await this.walletService.saveDeviceInDatabase(backup.device); await this.walletService.restoreDevice(backup.device); await this.processService.batchSaveProcesses(backup.processes); await this.restoreSecretsFromBackUp(backup.secrets); } public async restoreSecretsFromBackUp(secretsStore: SecretsStore) { const sharedList = Object.entries(secretsStore.shared_secrets).map( ([key, value]) => ({ key, value }) ); await this.db.saveSecretsBatch( secretsStore.unconfirmed_secrets, sharedList ); await this.restoreSecretsFromDB(); } public async restoreSecretsFromDB() { const secretsStore = await this.db.getAllSecrets(); await this.wasmService.setSharedSecrets(JSON.stringify(secretsStore)); console.log("[CoreWorker] 🔐 Secrets restaurés depuis la DB"); } public async createBackUp() { const device = await this.walletService.getDeviceFromDatabase(); if (!device) return null; return { device, processes: await this.processService.getProcesses(), secrets: await this.getAllSecrets(), }; } // ========================================== // DECRYPT ATTRIBUTE // ========================================== public async decryptAttribute( processId: string, state: ProcessState, attribute: string ): Promise { console.groupCollapsed( `[CoreWorker] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})` ); try { if (!(await this.isPaired())) { console.warn(`⚠️ Device is not paired. Cannot decrypt attribute.`); return null; } let hash: string | null | undefined = state.pcd_commitment[attribute]; let key: string | null | undefined = state.keys[attribute]; const pairingProcessId = await this.getPairingProcessId(); if (!hash) { console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`); return null; } if (!key) { if (!this._checkAccess(state, attribute, pairingProcessId)) { console.log(`⛔ Accès non autorisé. Abandon.`); return null; } const result = await this._fetchMissingKey(processId, state, attribute); hash = result.hash; key = result.key; } if (hash && key) { const blob = await this.getBlobFromDb(hash); if (!blob) { console.error( `💥 Échec: Blob non trouvé en BDD pour le hash ${hash}` ); return null; } try { const buf = await blob.arrayBuffer(); const cipher = new Uint8Array(buf); const keyUIntArray = this.hexToUInt8Array(key); const clear = await this.wasmService.decryptData(keyUIntArray, cipher); if (!clear) throw new Error("decrypt_data returned null"); const decoded = await this.wasmService.decodeValue(Array.from(clear)); console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`); return decoded; } catch (e) { console.error(`💥 Échec du déchiffrement: ${e}`); return null; } } return null; } catch (error) { console.error(`💥 Erreur:`, error); return null; } finally { console.groupEnd(); } } private _checkAccess( state: ProcessState, attribute: string, pairingProcessId: string ): boolean { const roles = state.roles; return Object.values(roles).some((role) => { const isMember = role.members.includes(pairingProcessId); if (!isMember) return false; return Object.values(role.validation_rules).some((rule) => rule.fields.includes(attribute) ); }); } private async _fetchMissingKey( processId: string, state: ProcessState, attribute: string ): Promise<{ hash: string | null; key: string | null }> { try { const process = await this.getProcess(processId); if (!process) return { hash: null, key: null }; await this.ensureConnections(process); await this.requestDataFromPeers( processId, [state.state_id], [state.roles] ); const requestId = `${processId}_${state.state_id}_${attribute}`; const keyRequestPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingKeyRequests.delete(requestId); reject(new Error(`Timeout waiting for key: ${attribute}`)); }, APP_CONFIG.TIMEOUTS.KEY_REQUEST); this.pendingKeyRequests.set(requestId, (key: string) => { clearTimeout(timeout); resolve(key); }); }); const receivedKey = await keyRequestPromise; const updatedProcess = await this.getProcess(processId); if (!updatedProcess) return { hash: null, key: null }; const updatedState = this.getStateFromId(updatedProcess, state.state_id); const updatedHash = updatedState ? updatedState.pcd_commitment[attribute] : state.pcd_commitment[attribute]; return { hash: updatedHash, key: receivedKey }; } catch (e) { return { hash: null, key: null }; } } } Comlink.expose(new CoreBackend());