ihm_client/src/services/service.ts

955 lines
35 KiB
TypeScript
Executable File

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<Services> | 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<string, Member> = {};
private notifications: any[] | null = null;
private db!: Database;
private currentBlockHeight: number = -1;
private pendingKeyRequests: Map<string, (key: string) => 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);
}
public static async getInstance(): Promise<Services> {
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<void> {
console.log('[Services] ⏳ Initialisation...');
this.notifications = this.getNotifications();
await this.sdkService.init();
this.db = await Database.getInstance();
this.walletService = new WalletService(this.sdkService, this.db);
this.processService = new ProcessService(this.sdkService, this.db);
this.networkService.initRelays();
console.log('[Services] ✅ Initialisé.');
}
// --- 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<void> {
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<string, Process>) {
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<string, any>) {
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<void> {
if (Object.keys(this.membersList).length > 0) return;
console.warn('[Services] Tentative de récupération des membres...');
await this.connectAllRelays();
}
public getAddressesForMemberId(memberId: string): string[] | null {
if (!this.membersList[memberId]) return null;
return this.membersList[memberId].sp_addresses;
}
public compareMembers(memberA: string[], memberB: string[]): boolean {
if (!memberA || !memberB) return false;
if (memberA.length !== memberB.length) return false;
return memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item));
}
// --- Utilitaires ---
public createFaucetMessage() {
return this.sdkClient.create_faucet_msg();
}
public isChildRole(parent: any, child: any): boolean {
try {
this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child));
return true;
} catch (e) {
console.error(e);
return false;
}
}
public resetState() {
this.device1 = false;
this.device2Ready = false;
}
// --- Logique Handshake ---
public async handleHandshakeMsg(url: string, parsedMsg: any) {
try {
const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg);
if (handshakeMsg.sp_address) {
this.updateRelay(url, handshakeMsg.sp_address);
}
this.currentBlockHeight = handshakeMsg.chain_tip;
if (!this.isPaired()) {
console.log(`[Services] ⏳ Non pairé. Le Handshake de ${url} est en pause...`);
while (!this.isPaired()) {
await new Promise(r => setTimeout(r, 500));
}
console.log(`[Services] ▶️ Appareil pairé ! Reprise du traitement Handshake de ${url}.`);
}
this.updateDeviceBlockHeight();
if (handshakeMsg.peers_list) {
this.membersList = { ...this.membersList, ...handshakeMsg.peers_list as Record<string, Member> };
}
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<void> {
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<string[] | null> {
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<void> {
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<Member>();
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<string, any> | 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<string>();
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<ApiReturn | null> {
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<void> {
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<string, Process> = {};
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<string, RoleDefinition>[] = [];
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<ApiReturn> {
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<ApiReturn> {
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<ApiReturn> {
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<ApiReturn> {
const process = await this.processService.getProcess(processId);
if (!process) throw new Error('Process not found');
let lastState = this.processService.getLastCommitedState(process);
let currentProcess = process;
if (!lastState) {
const first = process.states[0];
if (this.rolesContainsUs(first.roles)) {
const appRes = await this.approveChange(processId, first.state_id);
await this.handleApiReturn(appRes);
const prdRes = await this.createPrdUpdate(processId, first.state_id);
await this.handleApiReturn(prdRes);
} else if (first.validation_tokens.length > 0) {
const res = await this.createPrdUpdate(processId, first.state_id);
await this.handleApiReturn(res);
}
const updated = await this.processService.getProcess(processId);
if (updated) currentProcess = updated;
lastState = this.processService.getLastCommitedState(currentProcess);
if (!lastState) throw new Error('Still no commited state');
}
const lastStateIndex = this.getLastCommitedStateIndex(currentProcess);
if (lastStateIndex === null) throw new Error('Index commited introuvable');
const privateData: any = {};
const publicData: any = {};
for (const field of Object.keys(newData)) {
if (lastState.public_data[field]) {
publicData[field] = newData[field];
continue;
}
if (privateFields.includes(field)) {
privateData[field] = newData[field];
continue;
}
let isPrivate = false;
for (let i = lastStateIndex; i >= 0; i--) {
if (currentProcess.states[i].pcd_commitment[field]) {
privateData[field] = newData[field];
isPrivate = true;
break;
}
}
if (!isPrivate) publicData[field] = newData[field];
}
const finalRoles = roles || this.processService.getRoles(currentProcess);
const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData);
const res = this.sdkClient.update_process(currentProcess, encodedPrivateData, finalRoles, encodedPublicData, this.membersList);
if (res.updated_process) await this.ensureConnections(res.updated_process.current_process);
return res;
}
// public async confirmPairing() {
// console.log('[Services] Confirm Pairing...');
// const pid = this.walletService.getPairingProcessId();
// const process = await this.processService.getProcess(pid);
// if (!process) return;
// let state = this.processService.getLastCommitedState(process);
// if (!state && process.states.length > 0) state = process.states[process.states.length - 1];
// if (!state) return;
// const encodedAddr = state.public_data['pairedAddresses'];
// if (!encodedAddr) return;
// const addresses = this.decodeValue(encodedAddr);
// if (!addresses || addresses.length === 0) return;
// this.sdkClient.unpair_device();
// this.walletService.pairDevice(pid, addresses);
// if (this.walletService.isPaired()) {
// const d = this.walletService.dumpDeviceFromMemory();
// if (!this.walletService.isPaired()) d.pairing_process_commitment = pid;
// await this.walletService.saveDeviceInDatabase(d);
// console.log('✅ Pairing confirmed & Saved');
// }
// }
private async prepareProcessData(priv: any, pub: any) {
const p1 = this.splitData(priv);
const p2 = this.splitData(pub);
return {
encodedPrivateData: { ...this.sdkClient.encode_json(p1.jsonCompatibleData), ...this.sdkClient.encode_binary(p1.binaryData) },
encodedPublicData: { ...this.sdkClient.encode_json(p2.jsonCompatibleData), ...this.sdkClient.encode_binary(p2.binaryData) },
};
}
// API Methods
public async createPrdUpdate(pid: string, sid: string) {
const p = await this.getProcess(pid);
await this.ensureConnections(p!);
return this.sdkClient.create_update_message(p, sid, this.membersList);
}
public async createPrdResponse(pid: string, sid: string) {
const p = await this.getProcess(pid);
return this.sdkClient.create_response_prd(p, sid, this.membersList);
}
public async approveChange(pid: string, sid: string) {
const p = await this.getProcess(pid);
const res = this.sdkClient.validate_state(p, sid, this.membersList);
if (res.updated_process) await this.ensureConnections(res.updated_process.current_process);
return res;
}
public async rejectChange(pid: string, sid: string) {
const p = await this.getProcess(pid);
return this.sdkClient.refuse_state(p, sid);
}
public async requestDataFromPeers(pid: string, sids: string[], roles: any) {
const res = this.sdkClient.request_data(pid, sids, roles, this.membersList);
await this.handleApiReturn(res);
}
public async resetDevice() {
// console.warn("[Services:resetDevice] ⚠️ RÉINITIALISATION COMPLÈTE de l'appareil et de la BDD...");
this.sdkClient.reset_device();
// Clear all stores
await this.db.clearMultipleStores(['wallet', 'shared_secrets', 'unconfirmed_secrets', 'processes', 'diffs']);
// console.warn('[Services:resetDevice] ✅ Réinitialisation terminée.');
}
public async handleApiReturn(res: ApiReturn) {
if (!res || Object.keys(res).length === 0) return;
try {
const txData = (res.partial_tx ? await this.handlePartialTx(res.partial_tx) : null) || res.new_tx_to_send;
if (txData && txData.transaction.length != 0) {
this.networkService.sendMessage('NewTx', JSON.stringify(txData));
await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.API_DELAY));
}
if (res.secrets) await this.handleSecrets(res.secrets);
if (res.updated_process) await this.handleUpdatedProcess(res.updated_process);
if (res.push_to_storage) await this.handlePushToStorage(res.push_to_storage);
if (res.commit_to_send) this.networkService.sendMessage('Commit', JSON.stringify(res.commit_to_send));
if (res.ciphers_to_send) for (const c of res.ciphers_to_send) this.networkService.sendMessage('Cipher', c);
} catch (e) {
console.error('ApiReturn Error:', e);
}
}
private async handlePartialTx(partialTx: any): Promise<any> {
try {
return this.sdkClient.sign_transaction(partialTx).new_tx_to_send;
} catch (e) {
return null;
}
}
private async handleSecrets(secrets: any) {
const { unconfirmed_secrets, shared_secrets } = secrets;
const unconfirmedList = unconfirmed_secrets && unconfirmed_secrets.length > 0 ? unconfirmed_secrets : [];
const sharedList = shared_secrets && Object.keys(shared_secrets).length > 0
? Object.entries(shared_secrets).map(([key, value]) => ({ key, value }))
: [];
if (unconfirmedList.length > 0 || sharedList.length > 0) {
try {
await this.db.saveSecretsBatch(unconfirmedList, sharedList);
} catch (e) {
console.error('[Services:handleSecrets] 💥 Échec de sauvegarde batch des secrets:', e);
}
}
}
private async handleUpdatedProcess(updated: any) {
const pid = updated.process_id;
if (updated.encrypted_data) {
for (const [h, c] of Object.entries(updated.encrypted_data as Record<string, string>)) await this.saveBlobToDb(h, this.hexToBlob(c));
}
await this.processService.saveProcessToDb(pid, updated.current_process);
if (updated.diffs) await this.saveDiffsToDb(updated.diffs);
this._resolvePendingKeyRequests(pid, updated.current_process);
const dev = await this.walletService.getDeviceFromDatabase();
if (dev && dev.pairing_process_commitment === pid) {
const last = updated.current_process.states[updated.current_process.states.length - 1];
if (last?.public_data['pairedAddresses']) await this.confirmPairing();
}
}
public async saveDiffsToDb(diffs: UserDiff[]) {
await this.db.saveDiffs(diffs);
}
private _resolvePendingKeyRequests(processId: string, process: Process) {
if (this.pendingKeyRequests.size === 0) return;
for (const state of process.states) {
if (!state.keys) continue;
for (const [attr, key] of Object.entries(state.keys)) {
const rid = `${processId}_${state.state_id}_${attr}`;
if (this.pendingKeyRequests.has(rid)) {
this.pendingKeyRequests.get(rid)?.(key as string);
this.pendingKeyRequests.delete(rid);
}
}
}
}
private async handlePushToStorage(hashes: string[]) {
for (const hash of hashes) {
try {
const blob = await this.getBlobFromDb(hash);
const diff = await this.getDiffByValue(hash);
if (blob && diff) await this.saveDataToStorage(diff.storages, hash, blob, null);
} catch (e) {
console.error('Push error', e);
}
}
}
public async handleCommitError(response: string) {
const content = JSON.parse(response);
const errorMsg = content.error['GenericError'];
if (!['State is identical to the previous state', 'Not enough valid proofs'].includes(errorMsg)) {
setTimeout(() => this.networkService.sendMessage('Commit', JSON.stringify(content)), APP_CONFIG.TIMEOUTS.RETRY_DELAY);
}
}
public rolesContainsUs(roles: any) {
return this.processService.rolesContainsMember(roles, this.getPairingProcessId());
}
public async getSecretForAddress(address: string): Promise<string | null> {
return await this.db.getSharedSecret(address);
}
public async getAllDiffs(): Promise<Record<string, UserDiff>> {
return await this.db.getAllDiffs();
}
public async getDiffByValue(value: string): Promise<UserDiff | null> {
return await this.db.getDiff(value);
}
public async getAllSecrets(): Promise<SecretsStore> {
return await this.db.getAllSecrets();
}
// Storage & DB
public async saveBlobToDb(h: string, d: Blob) {
await this.db.saveBlob(h, d);
}
public async getBlobFromDb(h: string) {
return await this.db.getBlob(h);
}
public async fetchValueFromStorage(h: string) {
return retrieveData([APP_CONFIG.URLS.STORAGE], h);
}
public async saveDataToStorage(s: string[], h: string, d: Blob, ttl: number | null) {
return storeData(s, h, d, ttl);
}
// Helpers
public getProcessName(p: Process) {
const pub = this.getPublicData(p);
if (pub && pub['processName']) return this.decodeValue(pub['processName']);
return null;
}
public getPublicData(p: Process) {
const last = this.getLastCommitedState(p);
return last ? last.public_data : p.states[0]?.public_data || null;
}
// UI helpers
public getNotifications() {
return this.notifications;
}
public setNotifications(n: any[]) {
this.notifications = n;
}
async parseCipher(msg: string) {
try {
const res = this.sdkClient.parse_cipher(msg, this.membersList, await this.getProcesses());
await this.handleApiReturn(res);
} catch (e) {
console.error('Cipher Error', e);
}
}
async parseNewTx(msg: string) {
const parsed = JSON.parse(msg);
if (parsed.error) return;
const prevouts = this.sdkClient.get_prevouts(parsed.transaction);
for (const p of Object.values(await this.getProcesses())) {
const tip = p.states[p.states.length - 1].commited_in;
if (prevouts.includes(tip)) {
const newTip = this.sdkClient.get_txid(parsed.transaction);
const newStateId = this.sdkClient.get_opreturn(parsed.transaction);
const updated = this.sdkClient.process_commit_new_state(p, newStateId, newTip);
break;
}
}
try {
const res = this.sdkClient.parse_new_tx(msg, 0, this.membersList);
if (res && (res.partial_tx || res.new_tx_to_send || res.secrets || res.updated_process)) {
await this.handleApiReturn(res);
const d = this.dumpDeviceFromMemory();
const old = await this.getDeviceFromDatabase();
if (old && old.pairing_process_commitment) d.pairing_process_commitment = old.pairing_process_commitment;
await this.saveDeviceInDatabase(d);
}
} catch (e) {}
}
public updateMemberPublicName(pid: string, name: string) {
return this.updateProcess(pid, { memberPublicName: name }, [], null);
}
public async importJSON(backup: BackUp) {
await this.resetDevice();
await this.walletService.saveDeviceInDatabase(backup.device);
this.walletService.restoreDevice(backup.device);
await this.processService.batchSaveProcesses(backup.processes);
await this.restoreSecretsFromBackUp(backup.secrets);
}
public async restoreSecretsFromBackUp(secretsStore: SecretsStore) {
const sharedList = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value }));
await this.db.saveSecretsBatch(secretsStore.unconfirmed_secrets, sharedList);
await this.restoreSecretsFromDB();
}
public async restoreSecretsFromDB() {
const secretsStore = await this.db.getAllSecrets();
this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore));
}
public async createBackUp() {
const device = await this.walletService.getDeviceFromDatabase();
if (!device) return null;
return { device, processes: await this.processService.getProcesses(), secrets: await this.getAllSecrets() };
}
public async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise<any | null> {
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<string>((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 };
}
}
}