import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client'; import Database from './database.service'; import { storeData, retrieveData } from './storage.service'; import { BackUp } from '../types/index'; import { APP_CONFIG } from '../config/constants'; // Services import { SdkService } from './core/sdk.service'; import { NetworkService } from './core/network.service'; import { WalletService } from './domain/wallet.service'; import { ProcessService } from './domain/process.service'; import { CryptoService } from './domain/crypto.service'; export default class Services { 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 membersList: Record = {}; private notifications: any[] | null = null; private currentBlockHeight: number = -1; private pendingKeyRequests: Map void> = new Map(); public device1: boolean = false; public device2Ready: boolean = false; private constructor() { this.sdkService = new SdkService(); // Utilisation de la config this.networkService = new NetworkService(APP_CONFIG.URLS.BOOTSTRAP); this.cryptoService = new CryptoService(this.sdkService); this.walletService = new WalletService(this.sdkService); this.processService = new ProcessService(this.sdkService); } public static async getInstance(): Promise { if (Services.instance) return Services.instance; if (!Services.initializing) { Services.initializing = (async () => { const instance = new Services(); await instance.init(); return instance; })(); } Services.instance = await Services.initializing; return Services.instance; } public async init(): Promise { console.log('[Services] ⏳ Initialisation...'); this.notifications = this.getNotifications(); await this.sdkService.init(); this.networkService.initRelays(); console.log('[Services] ✅ Initialisé.'); } // --- Getters & Setters --- public get sdkClient() { return this.sdkService.getClient(); } public setProcessId(id: string | null) { this.processId = id; } public setStateId(id: string | null) { this.stateId = id; } public getProcessId() { return this.processId; } public getStateId() { return this.stateId; } // --- Network Proxy --- public async connectAllRelays() { await this.networkService.connectAllRelays(); if (Object.keys(this.networkService.getAllRelays()).length > 0) { try { await this.waitForHandshakeMessage(); } catch (e: any) { console.error(e.message); } } } public async addWebsocketConnection(url: string): Promise { await this.networkService.addWebsocketConnection(url); } public getAllRelays() { const relays = this.networkService.getAllRelays(); return Object.entries(relays).map(([wsurl, spAddress]) => ({ wsurl, spAddress })); } public updateRelay(url: string, sp: string) { this.networkService.updateRelay(url, sp); } public getSpAddress(url: string) { return this.networkService.getAllRelays()[url]; } public printAllRelays() { this.networkService.printAllRelays(); } // --- Wallet Proxy --- public isPaired() { return this.walletService.isPaired(); } public getAmount() { return this.walletService.getAmount(); } public getDeviceAddress() { return this.walletService.getDeviceAddress(); } public dumpDeviceFromMemory() { return this.walletService.dumpDeviceFromMemory(); } public dumpNeuteredDevice() { return this.walletService.dumpNeuteredDevice(); } public getPairingProcessId() { return this.walletService.getPairingProcessId(); } public async getDeviceFromDatabase() { return this.walletService.getDeviceFromDatabase(); } public restoreDevice(d: Device) { this.walletService.restoreDevice(d); } public pairDevice(pid: string, list: string[]) { this.walletService.pairDevice(pid, list); } public async unpairDevice() { await this.walletService.unpairDevice(); } public async saveDeviceInDatabase(d: Device) { await this.walletService.saveDeviceInDatabase(d); } public async createNewDevice() { return this.walletService.createNewDevice(this.currentBlockHeight > 0 ? this.currentBlockHeight : 0); } public async dumpWallet() { return this.walletService.dumpWallet(); } public async getMemberFromDevice() { return this.walletService.getMemberFromDevice(); } // --- Process Proxy --- public async getProcess(id: string) { return this.processService.getProcess(id); } public async getProcesses() { return this.processService.getProcesses(); } public async restoreProcessesFromDB() { await this.processService.getProcesses(); } public getLastCommitedState(p: Process) { return this.processService.getLastCommitedState(p); } public getUncommitedStates(p: Process) { return this.processService.getUncommitedStates(p); } public getStateFromId(p: Process, id: string) { return this.processService.getStateFromId(p, id); } public getRoles(p: Process) { return this.processService.getRoles(p); } public getLastCommitedStateIndex(p: Process) { return this.processService.getLastCommitedStateIndex(p); } public async batchSaveProcessesToDb(p: Record) { return this.processService.batchSaveProcesses(p); } // --- Helpers Crypto Proxy --- public decodeValue(val: number[]) { return this.sdkService.decodeValue(val); } public hexToBlob(hex: string) { return this.cryptoService.hexToBlob(hex); } public hexToUInt8Array(hex: string) { return this.cryptoService.hexToUInt8Array(hex); } public async blobToHex(blob: Blob) { return this.cryptoService.blobToHex(blob); } public getHashForFile(c: string, l: string, f: any) { return this.cryptoService.getHashForFile(c, l, f); } public getMerkleProofForFile(s: ProcessState, a: string) { return this.cryptoService.getMerkleProofForFile(s, a); } public validateMerkleProof(p: MerkleProofResult, h: string) { return this.cryptoService.validateMerkleProof(p, h); } private splitData(obj: Record) { return this.cryptoService.splitData(obj); } // --- Membres --- public getAllMembers() { return this.membersList; } public getAllMembersSorted() { return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))); } public async ensureMembersAvailable(): Promise { if (Object.keys(this.membersList).length > 0) return; console.warn('[Services] Tentative de récupération des membres...'); await this.connectAllRelays(); } public getAddressesForMemberId(memberId: string): string[] | null { if (!this.membersList[memberId]) return null; return this.membersList[memberId].sp_addresses; } public compareMembers(memberA: string[], memberB: string[]): boolean { if (!memberA || !memberB) return false; if (memberA.length !== memberB.length) return false; return memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item)); } // --- Utilitaires --- public createFaucetMessage() { return this.sdkClient.create_faucet_msg(); } public isChildRole(parent: any, child: any): boolean { try { this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child)); return true; } catch (e) { console.error(e); return false; } } public resetState() { this.device1 = false; this.device2Ready = false; } // --- Logique Handshake --- public async handleHandshakeMsg(url: string, parsedMsg: any) { try { const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg); if (handshakeMsg.sp_address) this.updateRelay(url, handshakeMsg.sp_address); this.currentBlockHeight = handshakeMsg.chain_tip; this.updateDeviceBlockHeight(); if (handshakeMsg.peers_list) this.membersList = { ...this.membersList, ...(handshakeMsg.peers_list as Record) }; if (handshakeMsg.processes_list) this.syncProcessesFromHandshake(handshakeMsg.processes_list); } catch (e) { console.error('Handshake Error', e); } } private async waitForHandshakeMessage(timeoutMs = APP_CONFIG.TIMEOUTS.HANDSHAKE): Promise { const start = Date.now(); while (Date.now() - start < timeoutMs) { if (Object.keys(this.membersList).length > 0 || Object.values(this.networkService.getAllRelays()).some((a) => a !== '')) return; await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.POLLING_INTERVAL)); } throw new Error('Timeout waiting for handshake'); } public async updateDeviceBlockHeight() { if (this.currentBlockHeight <= 0) return; const device = await this.walletService.getDeviceFromDatabase(); if (!device) return; if (device.sp_wallet.birthday === 0) { device.sp_wallet.birthday = this.currentBlockHeight; device.sp_wallet.last_scan = this.currentBlockHeight; await this.walletService.saveDeviceInDatabase(device); this.walletService.restoreDevice(device); } else if (device.sp_wallet.last_scan < this.currentBlockHeight) { console.log(`[Services] Scan requis de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`); try { await this.sdkClient.scan_blocks(this.currentBlockHeight, APP_CONFIG.URLS.BLINDBIT); const updatedDevice = this.walletService.dumpDeviceFromMemory(); await this.walletService.saveDeviceInDatabase(updatedDevice); } catch (e) { console.error('Scan error', e); } } } // --- Logique Métier --- public async getMyProcesses(): Promise { try { const pid = this.getPairingProcessId(); return await this.processService.getMyProcesses(pid); } catch (e) { return null; } } public async ensureConnections(process: Process, stateId: string | null = null): Promise { console.info(`[ConnectionCheck] 🔄 Check connexions (StateID: ${stateId || 'default'})`); if (!process) return; let state: ProcessState | null = null; if (stateId) state = this.processService.getStateFromId(process, stateId); if (!state && process.states.length >= 2) state = process.states[process.states.length - 2]; if (!state) return; await this.ensureMembersAvailable(); const members = new Set(); if (state.roles) { for (const role of Object.values(state.roles)) { for (const memberId of role.members) { const addrs = this.getAddressesForMemberId(memberId); if (addrs) members.add({ sp_addresses: addrs }); } } } if (members.size === 0) { let publicData: Record | null = null; for (let i = process.states.length - 1; i >= 0; i--) { const s = process.states[i]; if (s.public_data && s.public_data['pairedAddresses']) { publicData = s.public_data; break; } } if (publicData && publicData['pairedAddresses']) { const decoded = this.decodeValue(publicData['pairedAddresses']); if (decoded) members.add({ sp_addresses: decoded }); } } if (members.size === 0) return; const unconnected = new Set(); const myAddress = this.getDeviceAddress(); for (const member of Array.from(members)) { if (!member.sp_addresses) continue; for (const address of member.sp_addresses) { if (address === myAddress) continue; if ((await this.getSecretForAddress(address)) === null) unconnected.add(address); } } if (unconnected.size > 0) { console.log(`[ConnectionCheck] 📡 ${unconnected.size} non connectés. Connexion...`); await this.connectAddresses(Array.from(unconnected)); } } public async connectAddresses(addresses: string[]): Promise { if (addresses.length === 0) return null; const feeRate = APP_CONFIG.FEE_RATE; try { return this.sdkClient.create_transaction(addresses, feeRate); } catch (error: any) { if (String(error).includes('Insufficient funds')) { await this.getTokensFromFaucet(); return this.sdkClient.create_transaction(addresses, feeRate); } else { throw error; } } } private async getTokensFromFaucet(): Promise { console.log('[Services] 🚰 Demande Faucet...'); const availableAmt = this.getAmount(); const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10); if (availableAmt < target) { const msg = this.sdkClient.create_faucet_msg(); this.networkService.sendMessage('Faucet', msg); let attempts = 3; while (attempts > 0) { if (this.getAmount() >= target) return; attempts--; await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.RETRY_DELAY)); } throw new Error('Montant insuffisant après faucet'); } } private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap) { if (!newProcesses || Object.keys(newProcesses).length === 0) return; console.log(`[Services] Synchro ${Object.keys(newProcesses).length} processus...`); const toSave: Record = {}; const currentProcesses = await this.getProcesses(); if (Object.keys(currentProcesses).length === 0) { await this.processService.batchSaveProcesses(newProcesses); } else { for (const [processId, process] of Object.entries(newProcesses)) { const existing = currentProcesses[processId]; if (existing) { let newStates: string[] = []; let newRoles: Record[] = []; for (const state of process.states) { if (!state || !state.state_id) continue; if (state.state_id === APP_CONFIG.EMPTY_32_BYTES) { const existingTip = existing.states[existing.states.length - 1].commited_in; if (existingTip !== state.commited_in) { existing.states.pop(); existing.states.push(state); toSave[processId] = existing; } } else if (!this.processService.getStateFromId(existing, state.state_id)) { const existingLast = existing.states.pop(); if (existingLast) { existing.states.push(state); existing.states.push(existingLast); toSave[processId] = existing; if (this.rolesContainsUs(state.roles)) { newStates.push(state.state_id); newRoles.push(state.roles); } } } else { const existingState = this.processService.getStateFromId(existing, state.state_id); if (existingState && (!existingState.keys || Object.keys(existingState.keys).length === 0)) { if (this.rolesContainsUs(state.roles)) { newStates.push(state.state_id); newRoles.push(state.roles); } } } } if (newStates.length > 0) { await this.ensureConnections(existing); await this.requestDataFromPeers(processId, newStates, newRoles); } } else { toSave[processId] = process; } } if (Object.keys(toSave).length > 0) { await this.processService.batchSaveProcesses(toSave); } } document.dispatchEvent(new CustomEvent('processes-updated')); } public async createPairingProcess(userName: string, pairWith: string[]): Promise { if (this.isPaired()) throw new Error('Déjà appairé'); const myAddress = this.getDeviceAddress(); pairWith.push(myAddress); const privateData = { description: 'pairing', counter: 0 }; const publicData = { memberPublicName: userName, pairedAddresses: pairWith }; const validation_fields = [...Object.keys(privateData), ...Object.keys(publicData), 'roles']; const roles = { pairing: { members: [], validation_rules: [{ quorum: 1.0, fields: validation_fields, min_sig_member: 1.0 }], storages: [APP_CONFIG.URLS.STORAGE], }, }; return this.createProcess(privateData, publicData, roles); } public async createProcess(privateData: any, publicData: any, roles: any, feeRate = APP_CONFIG.FEE_RATE): Promise { const relay = await this.networkService.getAvailableRelayAddress(); const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); const members = this.membersList; try { return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members); } catch (e: any) { if (String(e).includes('Insufficient funds')) { await this.getTokensFromFaucet(); return await this.attemptCreateProcess(encodedPrivateData, roles, encodedPublicData, relay, feeRate, members); } throw e; } } private async attemptCreateProcess(priv: any, roles: any, pub: any, relay: string, fee: number, members: any): Promise { const res = this.sdkClient.create_new_process(priv, roles, pub, relay, fee, members); if (res.updated_process) { await this.ensureConnections(res.updated_process.current_process); } return res; } public async updateProcess(processId: string, newData: any, privateFields: string[], roles: any): Promise { const process = await this.processService.getProcess(processId); if (!process) throw new Error('Process not found'); let lastState = this.processService.getLastCommitedState(process); let currentProcess = process; if (!lastState) { const first = process.states[0]; if (this.rolesContainsUs(first.roles)) { const appRes = await this.approveChange(processId, first.state_id); await this.handleApiReturn(appRes); const prdRes = await this.createPrdUpdate(processId, first.state_id); await this.handleApiReturn(prdRes); } else if (first.validation_tokens.length > 0) { const res = await this.createPrdUpdate(processId, first.state_id); await this.handleApiReturn(res); } const updated = await this.processService.getProcess(processId); if (updated) currentProcess = updated; lastState = this.processService.getLastCommitedState(currentProcess); if (!lastState) throw new Error('Still no commited state'); } const lastStateIndex = this.getLastCommitedStateIndex(currentProcess); if (lastStateIndex === null) throw new Error('Index commited introuvable'); const privateData: any = {}; const publicData: any = {}; for (const field of Object.keys(newData)) { if (lastState.public_data[field]) { publicData[field] = newData[field]; continue; } if (privateFields.includes(field)) { privateData[field] = newData[field]; continue; } let isPrivate = false; for (let i = lastStateIndex; i >= 0; i--) { if (currentProcess.states[i].pcd_commitment[field]) { privateData[field] = newData[field]; isPrivate = true; break; } } if (!isPrivate) publicData[field] = newData[field]; } const finalRoles = roles || this.processService.getRoles(currentProcess); const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData); const res = this.sdkClient.update_process(currentProcess, encodedPrivateData, finalRoles, encodedPublicData, this.membersList); if (res.updated_process) await this.ensureConnections(res.updated_process.current_process); return res; } public async confirmPairing() { console.log('[Services] Confirm Pairing...'); const pid = this.walletService.getPairingProcessId(); const process = await this.processService.getProcess(pid); if (!process) return; let state = this.processService.getLastCommitedState(process); if (!state && process.states.length > 0) state = process.states[process.states.length - 1]; if (!state) return; const encodedAddr = state.public_data['pairedAddresses']; if (!encodedAddr) return; const addresses = this.decodeValue(encodedAddr); if (!addresses || addresses.length === 0) return; this.sdkClient.unpair_device(); this.walletService.pairDevice(pid, addresses); if (this.walletService.isPaired()) { const d = this.walletService.dumpDeviceFromMemory(); if (!this.walletService.isPaired()) d.pairing_process_commitment = pid; await this.walletService.saveDeviceInDatabase(d); console.log('✅ Pairing confirmed & Saved'); } } private async prepareProcessData(priv: any, pub: any) { const p1 = this.splitData(priv); const p2 = this.splitData(pub); return { encodedPrivateData: { ...this.sdkClient.encode_json(p1.jsonCompatibleData), ...this.sdkClient.encode_binary(p1.binaryData) }, encodedPublicData: { ...this.sdkClient.encode_json(p2.jsonCompatibleData), ...this.sdkClient.encode_binary(p2.binaryData) }, }; } // API Methods public async createPrdUpdate(pid: string, sid: string) { const p = await this.getProcess(pid); await this.ensureConnections(p!); return this.sdkClient.create_update_message(p, sid, this.membersList); } public async createPrdResponse(pid: string, sid: string) { const p = await this.getProcess(pid); return this.sdkClient.create_response_prd(p, sid, this.membersList); } public async approveChange(pid: string, sid: string) { const p = await this.getProcess(pid); const res = this.sdkClient.validate_state(p, sid, this.membersList); if (res.updated_process) await this.ensureConnections(res.updated_process.current_process); return res; } public async rejectChange(pid: string, sid: string) { const p = await this.getProcess(pid); return this.sdkClient.refuse_state(p, sid); } public async requestDataFromPeers(pid: string, sids: string[], roles: any) { const res = this.sdkClient.request_data(pid, sids, roles, this.membersList); await this.handleApiReturn(res); } public async resetDevice() { console.warn('Resetting device...'); this.sdkClient.reset_device(); 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('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) { this.networkService.sendMessage('NewTx', JSON.stringify(txData)); await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.API_DELAY)); } if (res.secrets) await this.handleSecrets(res.secrets); if (res.updated_process) await this.handleUpdatedProcess(res.updated_process); if (res.push_to_storage) await this.handlePushToStorage(res.push_to_storage); if (res.commit_to_send) this.networkService.sendMessage('Commit', JSON.stringify(res.commit_to_send)); if (res.ciphers_to_send) for (const c of res.ciphers_to_send) this.networkService.sendMessage('Cipher', c); } catch (e) { console.error('ApiReturn Error:', e); } } private async handlePartialTx(partialTx: any): Promise { try { return this.sdkClient.sign_transaction(partialTx).new_tx_to_send; } catch (e) { return null; } } private async handleSecrets(secrets: any) { const db = await Database.getInstance(); 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(updated: any) { const pid = updated.process_id; if (updated.encrypted_data) { for (const [h, c] of Object.entries(updated.encrypted_data as Record)) await this.saveBlobToDb(h, this.hexToBlob(c)); } await this.processService.saveProcessToDb(pid, updated.current_process); if (updated.diffs) await this.saveDiffsToDb(updated.diffs); this._resolvePendingKeyRequests(pid, updated.current_process); const dev = await this.walletService.getDeviceFromDatabase(); if (dev && dev.pairing_process_commitment === pid) { const last = updated.current_process.states[updated.current_process.states.length - 1]; if (last?.public_data['pairedAddresses']) await this.confirmPairing(); } } public async saveDiffsToDb(diffs: UserDiff[]) { const db = await Database.getInstance(); 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); } } } } private async handlePushToStorage(hashes: string[]) { for (const hash of hashes) { try { const blob = await this.getBlobFromDb(hash); const diff = await this.getDiffByValue(hash); if (blob && diff) await this.saveDataToStorage(diff.storages, hash, blob, null); } catch (e) { console.error('Push error', e); } } } public async handleCommitError(response: string) { const content = JSON.parse(response); const errorMsg = content.error['GenericError']; if (!['State is identical to the previous state', 'Not enough valid proofs'].includes(errorMsg)) { setTimeout(() => this.networkService.sendMessage('Commit', JSON.stringify(content)), APP_CONFIG.TIMEOUTS.RETRY_DELAY); } } public rolesContainsUs(roles: any) { return this.processService.rolesContainsMember(roles, this.getPairingProcessId()); } public async getSecretForAddress(addr: string) { const db = await Database.getInstance(); return await db.getObject('shared_secrets', addr); } 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 res = this.sdkClient.parse_cipher(msg, this.membersList, await this.getProcesses()); await this.handleApiReturn(res); } catch (e) { console.error('Cipher Error', e); } } async parseNewTx(msg: string) { const parsed = JSON.parse(msg); if (parsed.error) return; const prevouts = this.sdkClient.get_prevouts(parsed.transaction); for (const p of Object.values(await this.getProcesses())) { const tip = p.states[p.states.length - 1].commited_in; if (prevouts.includes(tip)) { const newTip = this.sdkClient.get_txid(parsed.transaction); const newStateId = this.sdkClient.get_opreturn(parsed.transaction); const updated = this.sdkClient.process_commit_new_state(p, newStateId, newTip); break; } } try { const res = this.sdkClient.parse_new_tx(msg, 0, this.membersList); if (res && (res.partial_tx || res.new_tx_to_send || res.secrets || res.updated_process)) { await this.handleApiReturn(res); const d = this.dumpDeviceFromMemory(); const old = await this.getDeviceFromDatabase(); if (old && old.pairing_process_commitment) d.pairing_process_commitment = old.pairing_process_commitment; await this.saveDeviceInDatabase(d); } } catch (e) {} } public updateMemberPublicName(pid: string, name: string) { return this.updateProcess(pid, { memberPublicName: name }, [], null); } public async importJSON(backup: BackUp) { await this.resetDevice(); await this.walletService.saveDeviceInDatabase(backup.device); this.walletService.restoreDevice(backup.device); await this.processService.batchSaveProcesses(backup.processes); await this.restoreSecretsFromBackUp(backup.secrets); } public async restoreSecretsFromBackUp(secretsStore: SecretsStore) { const db = await Database.getInstance(); 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() { const db = await Database.getInstance(); 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() }; } public async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { console.groupCollapsed(`[Services:decryptAttribute] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`); try { let hash: string | null | undefined = state.pcd_commitment[attribute]; let key: string | null | undefined = state.keys[attribute]; const pairingProcessId = this.getPairingProcessId(); if (!hash) { console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`); return null; } if (!key) { if (!this._checkAccess(state, attribute, pairingProcessId)) { console.log(`⛔ Accès non autorisé. Abandon.`); return null; } const result = await this._fetchMissingKey(processId, state, attribute); hash = result.hash; key = result.key; } if (hash && key) { const blob = await this.getBlobFromDb(hash); if (!blob) { console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`); return null; } try { const buf = await blob.arrayBuffer(); const cipher = new Uint8Array(buf); const keyUIntArray = this.hexToUInt8Array(key); const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher); if (!clear) throw new Error('decrypt_data returned null'); const decoded = this.sdkClient.decode_value(clear); console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`); return decoded; } catch (e) { console.error(`💥 Échec du déchiffrement: ${e}`); return null; } } return null; } catch (error) { console.error(`💥 Erreur:`, error); return null; } finally { console.groupEnd(); } } private _checkAccess(state: ProcessState, attribute: string, pairingProcessId: string): boolean { const roles = state.roles; return Object.values(roles).some((role) => { const isMember = role.members.includes(pairingProcessId); if (!isMember) return false; return Object.values(role.validation_rules).some((rule) => rule.fields.includes(attribute)); }); } private async _fetchMissingKey(processId: string, state: ProcessState, attribute: string): Promise<{ hash: string | null; key: string | null }> { try { const process = await this.getProcess(processId); if (!process) return { hash: null, key: null }; await this.ensureConnections(process); await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); const requestId = `${processId}_${state.state_id}_${attribute}`; const keyRequestPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.pendingKeyRequests.delete(requestId); reject(new Error(`Timeout waiting for key: ${attribute}`)); }, APP_CONFIG.TIMEOUTS.KEY_REQUEST); this.pendingKeyRequests.set(requestId, (key: string) => { clearTimeout(timeout); resolve(key); }); }); const receivedKey = await keyRequestPromise; const updatedProcess = await this.getProcess(processId); if (!updatedProcess) return { hash: null, key: null }; const updatedState = this.getStateFromId(updatedProcess, state.state_id); const updatedHash = updatedState ? updatedState.pcd_commitment[attribute] : state.pcd_commitment[attribute]; return { hash: updatedHash, key: receivedKey }; } catch (e) { return { hash: null, key: null }; } } }