ihm_client/src/services/service.ts

2356 lines
92 KiB
TypeScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-nocheck
import { INotification } from '~/models/notification.model';
import { IProcess } from '~/models/process.model';
import { initWebsocket, sendMessage } from '../websockets';
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, NewTxMessage, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client';
import ModalService from './modal.service';
import Database from './database.service';
import { storeData, retrieveData, testData } from './storage.service';
import { BackUp } from '~/models/backup.model';
export const U32_MAX = 4294967295;
const BASEURL = import.meta.env.VITE_BASEURL || `http://localhost`;
const BOOTSTRAPURL = [import.meta.env.VITE_BOOTSTRAPURL || `${BASEURL}:8090`];
const STORAGEURL = import.meta.env.VITE_STORAGEURL || `${BASEURL}:8081`;
const BLINDBITURL = import.meta.env.VITE_BLINDBITURL || `${BASEURL}:8000`;
const DEFAULTAMOUNT = 1000n;
const EMPTY32BYTES = String('').padStart(64, '0');
export default class Services {
private static initializing: Promise<Services> | null = null;
private static instance: Services;
private processId: string | null = null;
private stateId: string | null = null;
private sdkClient: any;
private processesCache: Record<string, Process> = {};
private myProcesses: Set<string> = new Set();
private notifications: any[] | null = null;
private subscriptions: { element: Element; event: string; eventHandler: string }[] = [];
private database: any;
private routingInstance!: ModalService;
private relayAddresses: { [wsurl: string]: string } = {};
private membersList: Record<string, Member> = {};
private currentBlockHeight: number = -1;
private relayReadyResolver: (() => void) | null = null;
private relayReadyPromise: Promise<void> | null = null;
private secretsAreCompromised: boolean = false;
// Private constructor to prevent direct instantiation from outside
private constructor() {}
// Method to access the singleton instance of Services
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();
instance.routingInstance = await ModalService.getInstance();
return instance;
})();
}
console.log('[Services] ⏳ Initialisation des services...');
Services.instance = await Services.initializing;
Services.initializing = null; // Reset for potential future use
console.log('[Services] ✅ Services initialisés.');
return Services.instance;
}
public async init(): Promise<void> {
this.notifications = this.getNotifications();
this.sdkClient = await import('../../pkg/sdk_client');
this.sdkClient.setup();
for (const wsurl of Object.values(BOOTSTRAPURL)) {
this.updateRelay(wsurl, '');
}
}
public setProcessId(processId: string | null) {
this.processId = processId;
}
public setStateId(stateId: string | null) {
this.stateId = stateId;
}
public getProcessId(): string | null {
return this.processId;
}
public getStateId(): string | null {
return this.stateId;
}
/**
* Calls `this.addWebsocketConnection` for each `wsurl` in relayAddresses.
* Waits for at least one handshake message before returning.
*/
public async connectAllRelays(): Promise<void> {
const connectedUrls: string[] = [];
// Connect to all relays
for (const wsurl of Object.keys(this.relayAddresses)) {
try {
console.log(`[Services:connectAllRelays] 🔌 Connexion à: ${wsurl}`);
await this.addWebsocketConnection(wsurl);
connectedUrls.push(wsurl);
console.log(`[Services:connectAllRelays] ✅ Connecté avec succès à: ${wsurl}`);
} catch (error) {
console.error(`[Services:connectAllRelays] ❌ Échec de la connexion à ${wsurl}:`, error);
}
}
// Wait for at least one handshake message if we have connections
if (connectedUrls.length > 0) {
try {
await this.waitForHandshakeMessage();
} catch (e) {
console.error(`[Services:connectAllRelays] ⌛️ ${e.message}`);
}
}
}
private getRelayReadyPromise(): Promise<void> {
if (!this.relayReadyPromise) {
this.relayReadyPromise = new Promise<void>((resolve) => {
this.relayReadyResolver = resolve;
});
}
return this.relayReadyPromise;
}
private resolveRelayReady(): void {
if (this.relayReadyResolver) {
this.relayReadyResolver();
this.relayReadyResolver = null;
this.relayReadyPromise = null;
}
}
public async addWebsocketConnection(url: string): Promise<void> {
console.log("[Services:addWebsocketConnection] 🕸️ Ouverture d'une nouvelle connexion websocket...");
await initWebsocket(url);
}
/**
* Add or update a key/value pair in relayAddresses.
* @param wsurl - The WebSocket URL (key).
* @param spAddress - The SP Address (value).
*/
public updateRelay(url: string, spAddress: string) {
console.log(`[Services:updateRelay] ✅ Mise à jour du relais ${url} avec spAddress ${spAddress}`);
this.relayAddresses[url] = spAddress;
}
/**
* Retrieve the spAddress for a given wsurl.
* @param wsurl - The WebSocket URL to look up.
* @returns The SP Address if found, or undefined if not.
*/
public getSpAddress(wsurl: string): string | undefined {
return this.relayAddresses[wsurl];
}
/**
* Get all key/value pairs from relayAddresses.
* @returns An array of objects containing wsurl and spAddress.
*/
public getAllRelays(): { wsurl: string; spAddress: string }[] {
return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({
wsurl,
spAddress,
}));
}
/**
* Print all key/value pairs for debugging.
*/
public printAllRelays(): void {
console.log('[Services:printAllRelays] Adresses relais actuelles:');
for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) {
console.log(`${wsurl} -> ${spAddress}`);
}
}
public isPaired(): boolean {
try {
const result = this.sdkClient.is_paired();
return result;
} catch (e) {
throw new Error(`[Services:isPaired] Erreur: ${e}`);
}
}
public async unpairDevice(): Promise<void> {
try {
console.log("[Services:unpairDevice] 🚫 Dissociation de l'appareil...");
this.sdkClient.unpair_device();
const newDevice = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(newDevice);
console.log('[Services:unpairDevice] ✅ Appareil dissocié et sauvegardé.');
} catch (e) {
throw new Error(`[Services:unpairDevice] Échec de la dissociation: ${e}`);
}
}
public async getSecretForAddress(address: string): Promise<string | null> {
const db = await Database.getInstance();
return await db.getObject('shared_secrets', address);
}
public async getAllSecrets(): Promise<SecretsStore> {
const db = await Database.getInstance();
const sharedSecrets = await db.dumpStore('shared_secrets');
const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); // keys are numeric values
const secretsStore = {
shared_secrets: sharedSecrets,
unconfirmed_secrets: Object.values(unconfirmedSecrets),
};
return secretsStore;
}
public async getAllDiffs(): Promise<Record<string, UserDiff>> {
const db = await Database.getInstance();
return await db.dumpStore('diffs');
}
/**
* Ensure that the in-memory members list is populated.
* If empty, (re)connect to relays and wait for a handshake to fill it.
*/
public async ensureMembersAvailable(): Promise<void> {
try {
if (Object.keys(this.membersList).length > 0) {
// console.debug('[Services:ensureMembersAvailable] ✅ Liste des membres déjà disponible.');
return;
}
console.warn('[Services:ensureMembersAvailable] ⚠️ Liste des membres vide. Tentative de connexion aux relais...');
// Attempt to connect to relays and wait for handshake which updates membersList
await this.connectAllRelays();
console.log(`[Services:ensureMembersAvailable] ✅ Connexion aux relais terminée. ${Object.keys(this.membersList).length} membres chargés.`);
} catch (e) {
console.error('[Services:ensureMembersAvailable] ❌ Échec de la récupération des membres:', e);
}
}
public async getDiffByValue(value: string): Promise<UserDiff | null> {
const db = await Database.getInstance();
const store = 'diffs';
const res = await db.getObject(store, value);
return res;
}
private async getTokensFromFaucet(): Promise<void> {
console.log('[Services:getTokensFromFaucet] 🚰 Demande de tokens au faucet...');
try {
await this.ensureSufficientAmount();
} catch (e) {
console.error('[Services:getTokensFromFaucet] ❌ Échec, vérifiez la connexion au relais.');
return;
}
}
/**
* Tente d'établir des connexions (secrets partagés) avec les membres d'un état de processus.
*/
public async ensureConnections(process: Process, stateId: string | null = null): Promise<void> {
const processId = process?.process_id; // Utilisation de l'optional chaining au cas où process est null
console.info(`[ConnectionCheck] 🔄 Démarrage de la vérification des connexions pour le processus ${processId} (StateID: ${stateId || 'par défaut'})`);
if (!process) {
console.error(`[ConnectionCheck] 💥 ERREUR CRITIQUE: ensureConnections a été appelée avec un processus nul ou undefined.`);
return;
}
// 1. Déterminer quel état analyser
const state = this.getStateToCheck(process, stateId);
if (!state) {
console.warn(`[ConnectionCheck] ⚠️ Aucun état valide trouvé pour le processus ${processId}. (States: ${process.states.length}, StateID demandé: ${stateId}). Abandon.`);
return;
}
// 2. Tenter de trouver les membres dans les rôles de cet état
// --- AMÉLIORATION: Appel 'await' ajouté pour corriger la race condition ---
let members = await this.getMembersFromState(state);
if (members.size === 0) {
console.log(`[ConnectionCheck] Aucun membre trouvé dans les rôles. Vérification s'il s'agit d'un processus de pairing...`);
members = this.getPairingMembers(process); // Tente la logique de pairing
}
if (members.size === 0) {
console.log(`[ConnectionCheck] 🏁 Aucun membre (rôles ou pairing) trouvé à qui se connecter. Tâche terminée.`);
return;
}
// 3. Trouver les membres auxquels nous ne sommes pas encore connectés
const unconnectedAddresses = await this.findUnconnectedAddresses(members);
if (unconnectedAddresses.size === 0) {
console.log(`[ConnectionCheck] ✅ Déjà connecté aux ${members.size} membre(s) trouvés.`);
return;
}
// 4. Se connecter aux membres manquants
console.log(`[ConnectionCheck] 📡 ${unconnectedAddresses.size} adresse(s) non connectée(s) trouvée(s). Tentative de connexion...`, Array.from(unconnectedAddresses));
// getTokensFromFaucet() est maintenant géré DANS connectAddresses
try {
const apiResult = await this.connectAddresses(Array.from(unconnectedAddresses));
if (apiResult) {
console.log(`[ConnectionCheck] 🎁 Réponse de 'connectAddresses' reçue, transfert à handleApiReturn...`);
await this.handleApiReturn(apiResult);
} else {
console.log(`[ConnectionCheck] 🤷 'connectAddresses' n'a renvoyé aucun résultat (peut-être un 409 Conflict géré).`);
}
} catch (error) {
console.error(`[ConnectionCheck] 💥 Échec lors de l'appel à connectAddresses: ${error}`, error);
}
}
// --- FONCTIONS D'AIDE (à placer dans la même classe) ---
/**
* Helper pour obtenir l'état de processus pertinent à vérifier.
* La logique par défaut (si stateId est nul) est de prendre l'avant-dernier état.
*/
private getStateToCheck(process: Process, stateId: string | null): ProcessState | null {
if (stateId) {
const state = process.states.find((s) => s.state_id === stateId);
if (!state) {
console.warn(`[ConnectionCheck] ⚠️ Impossible de trouver l'état avec l'ID: ${stateId}`);
return null;
}
return state;
}
// Logique par défaut: prendre l'avant-dernier état (nécessite au moins 2 états)
if (process.states.length < 2) {
console.warn(`[ConnectionCheck] ⚠️ Logique par défaut requiert 2 états, mais seulement ${process.states.length} trouvé(s).`);
return null;
}
// AMÉLIORATION: Log pour cette logique fragile
console.debug(`[ConnectionCheck] Utilisation de l'état n°${process.states.length - 2} (l'avant-dernier) comme état par défaut.`);
return process.states[process.states.length - 2];
}
/**
* Helper pour extraire les membres des rôles d'un état.
* --- AMÉLIORATION: Devenu 'async' pour corriger une race condition ---
*/
private async getMembersFromState(state: ProcessState): Promise<Set<Member>> {
await this.ensureMembersAvailable(); // S'ASSURE que membersList est chargé
const members = new Set<Member>();
if (!state.roles) {
console.warn(`[ConnectionCheck] ⚠️ L'état ${state.state_id} n'a pas de propriété 'roles'.`);
return members;
}
for (const role of Object.values(state.roles)) {
for (const memberId of role.members) {
const memberAddresses = this.getAddressesForMemberId(memberId);
if (memberAddresses && memberAddresses.length > 0) {
members.add({ sp_addresses: memberAddresses });
} else {
console.warn(`[ConnectionCheck] ⚠️ Impossible de trouver les adresses pour le membre ${memberId} (présent dans les rôles).`);
}
}
}
return members;
}
/**
* Helper pour la logique spécifique de "pairing" :
* cherche 'pairedAddresses' dans l'historique du processus.
*/
private getPairingMembers(process: Process): Set<Member> {
const members = new Set<Member>();
let publicData: Record<string, any> | null = null;
// Cherche 'pairedAddresses' en remontant l'historique
for (let i = process.states.length - 1; i >= 0; i--) {
const state = process.states[i];
if (state.public_data && state.public_data['pairedAddresses']) {
publicData = state.public_data;
console.log(`[ConnectionCheck] 'pairedAddresses' trouvé dans l'état ${i} (state_id: ${state.state_id})`);
break;
}
}
if (publicData && publicData['pairedAddresses']) {
const decodedAddresses = this.decodeValue(publicData['pairedAddresses']);
if (decodedAddresses && decodedAddresses.length > 0) {
members.add({ sp_addresses: decodedAddresses });
} else {
console.warn(`[ConnectionCheck] ⚠️ 'pairedAddresses' trouvé mais vide après décodage.`);
}
}
return members;
}
/**
* Helper pour filtrer une liste de membres et ne garder que ceux
* pour qui nous n'avons pas de secret local.
*/
private async findUnconnectedAddresses(members: Set<Member>): Promise<Set<string>> {
const unconnected = new Set<string>();
const myAddress = await this.getDeviceAddress();
for (const member of Array.from(members)) {
const sp_addresses = member.sp_addresses;
if (!sp_addresses || sp_addresses.length === 0) continue;
if (this.secretsAreCompromised) {
console.warn(`[findUnconnectedAddresses] 🚩 Flag 'secretsAreCompromised' détecté. Forçage de la reconnexion pour ${address}.`);
unconnected.add(address);
continue; // Important: passe au membre suivant
}
for (const address of sp_addresses) {
if (address === myAddress) continue; // On s'ignore soi-même
if ((await this.getSecretForAddress(address)) === null) {
unconnected.add(address);
}
}
}
if (this.secretsAreCompromised && unconnected.size > 0) {
console.log("[findUnconnectedAddresses] 🚩 Drapeau 'secretsAreCompromised' réinitialisé car une reconnexion va être tentée.");
this.secretsAreCompromised = false;
}
return unconnected;
}
// --- AMÉLIORATION: Ajout de la logique "try-catch-retry" du faucet ---
public async connectAddresses(addresses: string[]): Promise<ApiReturn> {
if (addresses.length === 0) {
console.warn("[Services:connectAddresses] Appel avec une liste d'adresses vide.");
return null;
}
const feeRate = 1; // Devrait être un paramètre ?
try {
// 1. Première tentative
console.log(`[Services:connectAddresses] 💬 Tentative de connexion (create_transaction) à ${addresses.length} adresse(s).`);
return this.sdkClient.create_transaction(addresses, feeRate);
} catch (error) {
// 2. Vérifier si c'est *exactement* l'erreur de fonds
if (this.isInsufficientFundsError(error)) {
console.warn('[Services:connectAddresses] 💰 Fonds insuffisants détectés. Appel du faucet pour recharger...');
try {
// 3. Appel au faucet (le "remède")
await this.getTokensFromFaucet(); // On recharge
// 4. Seconde (et dernière) tentative
console.log('[Services:connectAddresses] 💬 Nouvelle tentative de connexion post-recharge...');
return this.sdkClient.create_transaction(addresses, feeRate);
} catch (retryError) {
console.error('[Services:connectAddresses] 💥 Échec critique : Impossible de se connecter, même après recharge.', retryError);
throw new Error("Le système n'a pas pu financer la connexion. Échec.");
}
} else {
// 5. Ce n'était pas une erreur de fonds.
console.error(`[Services:connectAddresses] 💥 Erreur non liée aux fonds lors de la connexion: ${error}`, error);
throw error; // Relancer l'erreur originale
}
}
}
private async ensureSufficientAmount(): Promise<void> {
const availableAmt = this.getAmount();
const target: BigInt = DEFAULTAMOUNT * BigInt(10);
if (availableAmt < target) {
console.log(`[Services:ensureSufficientAmount] 💵 Montant insuffisant (${availableAmt}). Demande au faucet...`);
const faucetMsg = this.createFaucetMessage();
this.sendFaucetMessage(faucetMsg);
await this.waitForAmount(target);
console.log(`[Services:ensureSufficientAmount] ✅ Montant suffisant atteint.`);
}
}
private async waitForAmount(target: BigInt): Promise<BigInt> {
let attempts = 3;
while (attempts > 0) {
const amount = this.getAmount();
if (amount >= target) {
return amount;
}
console.log(`[Services:waitForAmount] ⏳ Attente de fonds... Tentative ${4 - attempts}/3`);
attempts--;
if (attempts > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second
}
}
throw new Error('Le montant est toujours 0 après 3 tentatives');
}
public async createPairingProcess(userName: string, pairWith: string[]): Promise<ApiReturn> {
console.log("[Services:createPairingProcess] 🤝 Création d'un processus de pairing...");
if (this.sdkClient.is_paired()) {
throw new Error("L'appareil est déjà appairé");
}
const myAddress: string = this.sdkClient.get_address();
pairWith.push(myAddress);
const privateData = {
description: 'pairing',
counter: 0,
};
const publicData = {
memberPublicName: userName,
pairedAddresses: pairWith,
};
const validation_fields: string[] = [...Object.keys(privateData), ...Object.keys(publicData), 'roles'];
const roles: Record<string, RoleDefinition> = {
pairing: {
members: [],
validation_rules: [
{
quorum: 1.0,
fields: validation_fields,
min_sig_member: 1.0,
},
],
storages: [STORAGEURL],
},
};
try {
return this.createProcess(privateData, publicData, roles);
} catch (e) {
throw new Error(`[Services:createPairingProcess] 💥 Échec: ${e}`);
}
}
private isFileBlob(value: any): value is { type: string; data: Uint8Array } {
return typeof value === 'object' && value !== null && typeof value.type === 'string' && value.data instanceof Uint8Array;
}
private splitData(obj: Record<string, any>) {
const jsonCompatibleData: Record<string, any> = {};
const binaryData: Record<string, { type: string; data: Uint8Array }> = {};
for (const [key, value] of Object.entries(obj)) {
if (this.isFileBlob(value)) {
binaryData[key] = value;
} else {
jsonCompatibleData[key] = value;
}
}
return { jsonCompatibleData, binaryData };
}
// --- AMÉLIORATION: Logique de 'ensureConnections' déplacée ici ---
public async createProcess(privateData: Record<string, any>, publicData: Record<string, any>, roles: Record<string, RoleDefinition>, feeRate: number = 1): Promise<ApiReturn> {
console.log("[Services:createProcess] 📝 Création d'un nouveau processus...");
const relayAddress = await this.getAvailableRelayAddress();
const { encodedPrivateData, encodedPublicData } = await this.prepareProcessData(privateData, publicData);
const members = this.getAllMembers();
try {
// 1. Première tentative
const result = await this.attemptProcessCreation(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members);
// --- AMÉLIORATION: Déplacé ici depuis le 'router' ---
// On s'assure qu'on est connecté aux membres du processus qu'on vient de créer.
console.log(`[Services:createProcess] 📞 Vérification des connexions pour le nouveau processus ${result.updated_process.process_id}`);
await this.ensureConnections(result.updated_process.current_process);
return result;
} catch (error) {
// 2. Vérifier si c'est *exactement* l'erreur de fonds
if (this.isInsufficientFundsError(error)) {
console.warn('[Services:createProcess] 💰 Fonds insuffisants détectés. Appel du faucet pour recharger...');
try {
// 3. Appel au faucet
await this.getTokensFromFaucet(); // On recharge
// 4. Seconde (et dernière) tentative
console.log('[Services:createProcess] 🔄 Nouvelle tentative de création de processus post-recharge...');
const result = await this.attemptProcessCreation(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members);
// --- AMÉLIORATION: Déplacé ici depuis le 'router' ---
console.log(`[Services:createProcess] 📞 Vérification des connexions pour le nouveau processus ${result.updated_process.process_id} (après retry)`);
await this.ensureConnections(result.updated_process.current_process);
return result;
} catch (retryError) {
console.error('[Services:createProcess] 💥 Échec critique : Impossible de créer le processus, même après recharge.', retryError);
throw new Error("Le système n'a pas pu financer l'opération. Échec de la création.");
}
} else {
// 5. Ce n'était pas une erreur de fonds.
console.error('[Services:createProcess] 💥 Erreur non liée aux fonds lors de la création du processus:', error);
throw error; // Relancer l'erreur originale
}
}
}
/**
* Encapsule l'appel au SDK pour le réutiliser (tentative 1 et 2).
*/
private async attemptProcessCreation(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members): Promise<ApiReturn> {
console.log('[Services:attemptProcessCreation] 📦 Appel de sdkClient.create_new_process...');
const result = this.sdkClient.create_new_process(encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, members);
if (result.updated_process) {
console.log('[Services:attemptProcessCreation] ✅ Processus créé avec succès:', result.updated_process.process_id);
return result;
} else {
throw new Error("[Services:attemptProcessCreation] 💥 sdkClient.create_new_process n'a renvoyé aucun processus mais n'a pas levé d'erreur.");
}
}
/**
* Vérifie de manière robuste si l'erreur est bien celle des fonds insuffisants.
*/
private isInsufficientFundsError(error: any): boolean {
const errorString = String(error.message || error.error || error);
return errorString.includes('Insufficient funds');
}
/**
* Tente d'obtenir une adresse de relais, en attendant si nécessaire.
*/
private async getAvailableRelayAddress(): Promise<string> {
let relayAddress = this.getAllRelays()[0]?.spAddress; // TODO: Améliorer la sélection
if (!relayAddress) {
console.log('[Services:getAvailableRelayAddress] ⏳ Aucun relais prêt. En attente du handshake...');
await this.getRelayReadyPromise();
relayAddress = this.getAllRelays()[0]?.spAddress;
}
if (!relayAddress) {
throw new Error('[Services:getAvailableRelayAddress] ❌ Aucune adresse de relais disponible après attente');
}
return relayAddress;
}
/**
* Sépare et encode les données JSON et binaires.
*/
private async prepareProcessData(privateData: any, publicData: any): Promise<{ encodedPrivateData: any; encodedPublicData: any }> {
// TODO: Exécuter l'encodage lourd dans un Web Worker
const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData);
const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(privateSplitData.binaryData),
};
const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(publicSplitData.binaryData),
};
return { encodedPrivateData, encodedPublicData };
}
public async updateProcess(process: Process, privateData: Record<string, any>, publicData: Record<string, any>, roles: Record<string, RoleDefinition> | null): Promise<ApiReturn> {
console.log(`[Services:updateProcess] 🔄 Mise à jour du processus ${process.process_id}...`);
// If roles is null, we just take the last commited state roles
if (!roles) {
roles = this.getRoles(process);
} else {
console.log('[Services:updateProcess] Utilisation de nouveaux rôles fournis:', JSON.stringify(roles));
}
const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData);
const encodedPrivateData = {
...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(privateSplitData.binaryData),
};
const encodedPublicData = {
...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData),
...this.sdkClient.encode_binary(publicSplitData.binaryData),
};
try {
const result = this.sdkClient.update_process(process, encodedPrivateData, roles, encodedPublicData, this.getAllMembers());
if (result.updated_process) {
console.log(`[Services:updateProcess] ✅ Processus ${process.process_id} mis à jour. Vérification des connexions...`);
await this.ensureConnections(result.updated_process.current_process);
return result;
} else {
throw new Error('[Services:updateProcess] 💥 updated_process vide dans updateProcessReturn');
}
} catch (e) {
throw new Error(`[Services:updateProcess] 💥 Échec: ${e}`);
}
}
public async createPrdUpdate(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`[Services:createPrdUpdate] 📤 Création d'une mise à jour PRD pour ${processId}:${stateId}`);
const process = await this.getProcess(processId);
if (!process) {
throw new Error('[Services:createPrdUpdate] 💥 Processus inconnu');
} else {
await this.ensureConnections(process);
}
try {
return this.sdkClient.create_update_message(process, stateId, this.getAllMembers());
} catch (e) {
throw new Error(`[Services:createPrdUpdate] 💥 Échec: ${e}`);
}
}
public async createPrdResponse(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`[Services:createPrdResponse] 📥 Création d'une réponse PRD pour ${processId}:${stateId}`);
const process = await this.getProcess(processId);
if (!process) {
throw new Error('[Services:createPrdResponse] 💥 Processus inconnu');
}
try {
return this.sdkClient.create_response_prd(process, stateId, this.getAllMembers());
} catch (e) {
throw new Error(`[Services:createPrdResponse] 💥 Échec: ${e}`);
}
}
public async approveChange(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`[Services:approveChange] 👍 Approbation du changement ${processId}:${stateId}`);
const process = await this.getProcess(processId);
if (!process) {
throw new Error("[Services:approveChange] 💥 Échec de l'obtention du processus depuis la BDD");
}
try {
const result = this.sdkClient.validate_state(process, stateId, this.getAllMembers());
if (result.updated_process) {
await this.ensureConnections(result.updated_process.current_process);
return result;
} else {
throw new Error('[Services:approveChange] 💥 updated_process vide dans approveChangeReturn');
}
} catch (e) {
throw new Error(`[Services:approveChange] 💥 Échec: ${e}`);
}
}
public async rejectChange(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`[Services:rejectChange] 👎 Rejet du changement ${processId}:${stateId}`);
const process = await this.getProcess(processId);
if (!process) {
throw new Error("[Services:rejectChange] 💥 Échec de l'obtention du processus depuis la BDD");
}
try {
return this.sdkClient.refuse_state(process, stateId);
} catch (e) {
throw new Error(`[Services:rejectChange] 💥 Échec: ${e}`);
}
}
async resetDevice() {
console.warn("[Services:resetDevice] ⚠️ RÉINITIALISATION COMPLÈTE de l'appareil et de la BDD...");
this.sdkClient.reset_device();
// Clear all stores
const db = await Database.getInstance();
await db.clearStore('wallet');
await db.clearStore('shared_secrets');
await db.clearStore('unconfirmed_secrets');
await db.clearStore('processes');
await db.clearStore('diffs');
console.warn('[Services:resetDevice] ✅ Réinitialisation terminée.');
}
sendNewTxMessage(message: string) {
console.log('[Services:sendNewTxMessage] ✉️ Envoi de NewTx...');
sendMessage('NewTx', message);
}
sendCommitMessage(message: string) {
console.log('[Services:sendCommitMessage] ✉️ Envoi de Commit...');
sendMessage('Commit', message);
}
sendCipherMessages(ciphers: string[]) {
console.log(`[Services:sendCipherMessages] ✉️ Envoi de ${ciphers.length} cipher(s)...`);
for (let i = 0; i < ciphers.length; i++) {
const cipher = ciphers[i];
sendMessage('Cipher', cipher);
}
}
sendFaucetMessage(message: string): void {
console.log('[Services:sendFaucetMessage] ✉️ Envoi de Faucet...');
sendMessage('Faucet', message);
}
// --- AMÉLIORATION: Ajout de la solution "bombe" pour casser la boucle ---
async parseCipher(message: string) {
const membersList = this.getAllMembers();
const processes = await this.getProcesses();
try {
console.debug('[Services:parseCipher] 🤫 Tentative de déchiffrement du message...');
const apiReturn = this.sdkClient.parse_cipher(message, membersList, processes);
console.debug('[Services:parseCipher] ✅ Message déchiffré, traitement...');
await this.handleApiReturn(apiReturn);
// Si le déchiffrement réussit, c'est que nos secrets sont bons.
// On réinitialise le drapeau (au cas où il était levé).
if (this.secretsAreCompromised) {
console.log("[Services:parseCipher] ✅ Le déchiffrement a réussi. Réinitialisation du drapeau 'secretsAreCompromised'.");
this.secretsAreCompromised = false;
}
} catch (e) {
console.error(`[Services:parseCipher] 💥 Échec critique du déchiffrement: ${e}`);
console.warn(`[Services:parseCipher] Contrainte d'anonymat: L'expéditeur est inconnu.`);
// On ne supprime rien. On lève juste un drapeau pour
// forcer 'ensureConnections' à se méfier de la BDD.
console.warn(`[Services:parseCipher] 🚩 ACTION: Levée du drapeau 'secretsAreCompromised'.`);
this.secretsAreCompromised = true;
}
}
async parseNewTx(newTxMsg: string) {
console.log('[Services:parseNewTx] 📄 Nouveau message NewTx reçu.');
const parsedMsg: NewTxMessage = JSON.parse(newTxMsg);
if (parsedMsg.error !== null) {
console.error('[Services:parseNewTx] 💥 Erreur dans le message NewTx:', parsedMsg.error);
return;
}
const membersList = this.getAllMembers();
// 1. Mettre à jour les processus affectés par cette transaction
await this.updateProcessesFromNewTx(parsedMsg.transaction);
// 2. Mettre à jour le portefeuille et l'état de l'appareil
await this.updateWalletFromNewTx(newTxMsg, membersList);
}
/**
* Sous-fonction de parseNewTx: Met à jour les processus en cache.
*/
private async updateProcessesFromNewTx(transaction: any) {
try {
const prevouts = this.sdkClient.get_prevouts(transaction);
// console.debug('[Services:updateProcessesFromNewTx] Prevouts de la tx:', prevouts);
for (const process of Object.values(this.processesCache)) {
const tip = process.states[process.states.length - 1].commited_in;
if (prevouts.includes(tip)) {
const processId = process.process_id; // Utilisation de l'ID stocké
const newTip = this.sdkClient.get_txid(transaction);
console.log(`[Services:updateProcessesFromNewTx] 🔗 La Tx ${newTip} dépense le tip du processus ${processId}`);
const newStateId = this.sdkClient.get_opreturn(transaction);
console.log('[Services:updateProcessesFromNewTx] 📄 Nouvel stateId (op_return):', newStateId);
const updatedProcess = this.sdkClient.process_commit_new_state(process, newStateId, newTip);
this.processesCache[processId] = updatedProcess;
console.log('[Services:updateProcessesFromNewTx] ✅ Processus mis à jour en cache:', updatedProcess);
break; // On suppose qu'une tx ne met à jour qu'un seul processus
}
}
} catch (e) {
console.error("[Services:updateProcessesFromNewTx] 💥 Échec de l'analyse NewTx pour les commitments:", e);
}
}
/**
* Sous-fonction de parseNewTx: Met à jour le portefeuille.
*/
private async updateWalletFromNewTx(newTxMsg: string, membersList: Record<string, Member>) {
try {
const parsedTx = this.sdkClient.parse_new_tx(newTxMsg, 0, membersList);
if (parsedTx && (parsedTx.partial_tx || parsedTx.new_tx_to_send || parsedTx.secrets || parsedTx.updated_process)) {
console.log('[Services:updateWalletFromNewTx] La Tx contient des données pertinentes. Traitement par handleApiReturn...');
try {
await this.handleApiReturn(parsedTx);
const newDevice = this.dumpDeviceFromMemory();
// Preserve pairing_process_commitment from existing device
const existingDevice = await this.getDeviceFromDatabase();
if (existingDevice && existingDevice.pairing_process_commitment) {
newDevice.pairing_process_commitment = existingDevice.pairing_process_commitment;
}
await this.saveDeviceInDatabase(newDevice);
console.log('[Services:updateWalletFromNewTx] ✅ Appareil mis à jour et sauvegardé.');
} catch (e) {
console.error("[Services:updateWalletFromNewTx] 💥 Échec de la mise à jour de l'appareil après NewTx:", e);
}
} else {
// console.debug('[Services:updateWalletFromNewTx] La Tx ne contenait pas de données pertinentes pour le portefeuille.');
}
} catch (e) {
// C'est souvent normal (ex: une tx qui ne nous concerne pas)
// console.debug('[Services:updateWalletFromNewTx] sdkClient.parse_new_tx n\'a rien trouvé:', e);
}
}
// --- AMÉLIORATION: Logs ajoutés ---
public async handleApiReturn(apiReturn: ApiReturn) {
console.log("[Services:handleApiReturn] 📥 Traitement d'un nouvel objet ApiReturn...", apiReturn);
// 1. Validation initiale
if (!this.isValidApiReturn(apiReturn)) {
console.log('[Services:handleApiReturn] ⏩ ApiReturn vide ou invalide. Skip.');
return;
}
try {
// 2. Gestion de la signature de transaction
const newTxFromSigning = apiReturn.partial_tx ? await this.handlePartialTx(apiReturn.partial_tx) : null;
// 3. Gestion de l'envoi de transaction
const txData = newTxFromSigning || apiReturn.new_tx_to_send;
if (txData && txData.transaction.length != 0) {
console.log("[Services:handleApiReturn] 📤 Envoi d'une nouvelle transaction...");
await this.handleNewTx(txData);
}
// 4. Gestion des secrets
if (apiReturn.secrets) {
console.log('[Services:handleApiReturn] 🔑 Gestion des secrets...');
await this.handleSecrets(apiReturn.secrets);
}
// 5. Gestion du processus mis à jour
if (apiReturn.updated_process) {
console.log('[Services:handleApiReturn] 🔄 Gestion de la mise à jour de processus...');
await this.handleUpdatedProcess(apiReturn.updated_process);
}
// 6. Gestion du push vers le stockage
if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) {
console.log('[Services:handleApiReturn] ☁️ Poussée de données vers le stockage...');
await this.handlePushToStorage(apiReturn.push_to_storage);
}
// 7. Gestion du "commit" à envoyer
if (apiReturn.commit_to_send) {
console.log('[Services:handleApiReturn] 📤 Envoi de Commit...');
this.handleCommit(apiReturn.commit_to_send);
}
// 8. Gestion des "ciphers" à envoyer
if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) {
console.log('[Services:handleApiReturn] 📤 Envoi de Ciphers...');
this.handleCiphers(apiReturn.ciphers_to_send);
}
} catch (error) {
console.error('[Services:handleApiReturn] 💥 ERREUR CRITIQUE lors du traitement de ApiReturn:', error);
}
}
private isValidApiReturn(apiReturn: ApiReturn): boolean {
if (!apiReturn || Object.keys(apiReturn).length === 0) {
return false;
}
const hasValidData = Object.values(apiReturn).some((value) => value !== null && value !== undefined);
return hasValidData;
}
private async handlePartialTx(partialTx): Promise<any> {
console.log("[Services:handlePartialTx] ✍️ Signature d'une transaction partielle...");
try {
const res = this.sdkClient.sign_transaction(partialTx);
return res.new_tx_to_send;
} catch (e) {
console.error('[Services:handlePartialTx] 💥 Échec de la signature:', e);
return null;
}
}
private async handleNewTx(txData: any) {
this.sendNewTxMessage(JSON.stringify(txData));
// 🚨 ATTENTION: C'est un anti-pattern (code smell).
// Cette attente arbitraire doit être remplacée par un
// véritable mécanisme d'acquittement (par ex. une Promise
// retournée par sendNewTxMessage).
console.warn('[Services:handleNewTx] ⏳ Attente arbitraire de 500ms...');
await new Promise((r) => setTimeout(r, 500));
}
private async handleSecrets(secrets: any) {
const { unconfirmed_secrets, shared_secrets } = secrets;
const db = await Database.getInstance();
// Sauvegarder les secrets non confirmés
if (unconfirmed_secrets && unconfirmed_secrets.length > 0) {
console.log(`[Services:handleSecrets] 💾 Sauvegarde de ${unconfirmed_secrets.length} secret(s) non confirmé(s)`);
for (const secret of unconfirmed_secrets) {
try {
await db.addObject({
storeName: 'unconfirmed_secrets',
object: secret,
key: null,
});
} catch (e) {
console.error("[Services:handleSecrets] 💥 Échec de sauvegarde d'un secret non confirmé:", e);
}
}
}
// Sauvegarder les secrets partagés (confirmés)
if (shared_secrets && Object.keys(shared_secrets).length > 0) {
const entries = Object.entries(shared_secrets).map(([key, value]) => ({ key, value }));
console.log(`[Services:handleSecrets] 💾 Sauvegarde de ${entries.length} secret(s) partagé(s)`);
for (const entry of entries) {
try {
await db.addObject({
storeName: 'shared_secrets',
object: entry.value,
key: entry.key,
});
console.log(`[Services:handleSecrets] ✅ Secret partagé pour ${entry.key} sauvegardé.`);
} catch (e) {
console.error(`[Services:handleSecrets] 💥 Échec de l'ajout du secret partagé pour ${entry.key}:`, e);
}
}
}
}
private async handleUpdatedProcess(updatedProcess: any) {
const processId: string = updatedProcess.process_id;
console.log(`[Services:handleUpdatedProcess] 🔄 Traitement des mises à jour pour le processus ${processId}`);
// Sauvegarder les données chiffrées
if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) {
await this.saveEncryptedData(updatedProcess.encrypted_data);
}
// Sauvegarder le processus lui-même
await this.saveProcessToDb(processId, updatedProcess.current_process);
// Sauvegarder les diffs
if (updatedProcess.diffs && updatedProcess.diffs.length != 0) {
try {
await this.saveDiffsToDb(updatedProcess.diffs);
} catch (e) {
console.error('[Services:handleUpdatedProcess] 💥 Échec de la sauvegarde des diffs:', e);
}
}
// Vérifier la logique métier spécifique au pairing
await this.checkAndConfirmPairing(processId, updatedProcess);
}
private async saveEncryptedData(encryptedData: Record<string, string>) {
console.log(`[Services:saveEncryptedData] 💾 Sauvegarde de ${Object.keys(encryptedData).length} blob(s) chiffré(s)...`);
for (const [hash, cipher] of Object.entries(encryptedData)) {
const blob = this.hexToBlob(cipher);
try {
await this.saveBlobToDb(hash, blob);
} catch (e) {
console.error(`[Services:saveEncryptedData] 💥 Échec de la sauvegarde du blob pour ${hash}:`, e);
}
}
}
private async checkAndConfirmPairing(processId: string, updatedProcess: any) {
try {
const existingDevice = await this.getDeviceFromDatabase();
if (!existingDevice || existingDevice.pairing_process_commitment !== processId) {
// console.debug('[Services:checkAndConfirmPairing] Ce n\'est pas le processus de pairing de cet appareil. Skip.');
return; // Ce n'est pas le processus de pairing de cet appareil
}
// C'est notre processus de pairing, vérifions s'il est prêt
const lastState = updatedProcess.current_process.states[updatedProcess.current_process.states.length - 1];
if (lastState && lastState.public_data && lastState.public_data['pairedAddresses']) {
console.log('[Services:checkAndConfirmPairing] 🤝 Processus de pairing mis à jour avec les adresses. Confirmation automatique...');
await this.confirmPairing();
}
} catch (e) {
console.error("[Services:checkAndConfirmPairing] 💥 Échec de l'auto-confirmation du pairing:", e);
}
}
private async handlePushToStorage(hashes: string[]) {
console.log(`[Services:handlePushToStorage] ☁️ Demande de push pour ${hashes.length} hash(es)`);
for (const hash of hashes) {
try {
const blob = await this.getBlobFromDb(hash);
if (!blob) {
console.error(`[Services:handlePushToStorage] 💥 Échec: blob non trouvé en BDD pour le hash ${hash}`);
continue;
}
const diff = await this.getDiffByValueFromDb(hash);
if (!diff) {
console.error(`[Services:handlePushToStorage] 💥 Échec: diff non trouvé en BDD pour le hash ${hash}`);
continue;
}
const storages = diff.storages;
console.log(`[Services:handlePushToStorage] ☁️ Poussée de ${hash} vers ${storages.length} storage(s)...`);
await this.saveDataToStorage(storages, hash, blob, null);
} catch (e) {
console.error(`[Services:handlePushToStorage] 💥 Échec du push pour ${hash}:`, e);
}
}
}
private handleCommit(commit: any) {
this.sendCommitMessage(JSON.stringify(commit));
}
private handleCiphers(ciphers: any[]) {
this.sendCipherMessages(ciphers);
}
public async openPairingConfirmationModal(processId: string) {
console.log('[Services:openPairingConfirmationModal] 띄 Ouverture du modal de confirmation...');
const process = await this.getProcess(processId);
if (!process) {
console.error('[Services:openPairingConfirmationModal] 💥 Échec: processus de pairing non trouvé');
return;
}
const firstState = process.states[0];
const roles = firstState.roles;
const stateId = firstState.state_id;
try {
await this.routingInstance.openPairingConfirmationModal(roles, processId, stateId);
} catch (e) {
console.error(e);
}
}
public async confirmPairing() {
console.log('[Services:confirmPairing] 🤝 Confirmation du pairing...');
try {
// Get the pairing process ID from database
const existingDevice = await this.getDeviceFromDatabase();
if (!existingDevice || !existingDevice.pairing_process_commitment) {
console.error('[Services:confirmPairing] 💥 Aucun engagement de processus de pairing trouvé');
return;
}
const pairingProcessId = existingDevice.pairing_process_commitment;
console.log(`[Services:confirmPairing] Processus ID: ${pairingProcessId}`);
// Get the pairing process to extract paired addresses
const myPairingProcess = await this.getProcess(pairingProcessId);
if (!myPairingProcess) {
console.error('[Services:confirmPairing] 💥 Processus de pairing inconnu');
return;
}
// Try to get committed state first, fallback to current state
let myPairingState = this.getLastCommitedState(myPairingProcess);
if (!myPairingState && myPairingProcess.states.length > 0) {
// If no committed state, use the current state
myPairingState = myPairingProcess.states[myPairingProcess.states.length - 1];
console.log('[Services:confirmPairing] ⚠️ Utilisation de l\'état actuel au lieu de l\'état "commited"');
}
if (!myPairingState) {
console.error('[Services:confirmPairing] 💥 Aucun état trouvé dans le processus de pairing');
return;
}
const encodedSpAddressList = myPairingState.public_data['pairedAddresses'];
if (!encodedSpAddressList) {
console.error("[Services:confirmPairing] 💥 Aucune adresse d'appairage trouvée dans l'état");
return;
}
const spAddressList = this.decodeValue(encodedSpAddressList);
if (spAddressList.length === 0) {
console.error('[Services:confirmPairing] 💥 pairedAddresses est vide');
return;
}
console.log(`[Services:confirmPairing] ${spAddressList.length} adresses trouvées pour l'appairage.`);
// ... (Suppression du bloc de test 'test_process_id_parsing' pour la clarté)
this.sdkClient.unpair_device(); // Clear any existing pairing
try {
console.log('[Services:confirmPairing] 📞 Appel de sdkClient.pair_device()...');
this.sdkClient.pair_device(pairingProcessId, spAddressList);
console.log('[Services:confirmPairing] ✅ Appel de pair_device() réussi (côté SDK).');
} catch (pairError) {
console.error('[Services:confirmPairing] 💥 sdkClient.pair_device() a échoué:', pairError);
throw pairError;
}
// Verify pairing was successful
const isPairedAfterPairing = this.sdkClient.is_paired();
console.log('[Services:confirmPairing] ❓ Statut is_paired après appel:', isPairedAfterPairing);
// Save the updated device
const newDevice = this.dumpDeviceFromMemory();
console.log('[Services:confirmPairing] Appareil en mémoire après appairage:', {
pairing_process_commitment: newDevice.pairing_process_commitment,
paired_member: newDevice.paired_member,
});
// IMPORTANT: Only set pairing_process_commitment if WASM pairing succeeded
if (isPairedAfterPairing) {
console.log("[Services:confirmPairing] L'appairage WASM a réussi, conservation de l'engagement WASM");
} else {
console.warn("[Services:confirmPairing] ⚠️ L'appairage WASM a échoué, définition manuelle de l'engagement (fallback)");
newDevice.pairing_process_commitment = pairingProcessId;
}
await this.saveDeviceInDatabase(newDevice);
// Final verification
const finalIsPaired = this.sdkClient.is_paired();
console.log('[Services:confirmPairing] ✅ Statut final is_paired:', finalIsPaired);
console.log(`[Services:confirmPairing] ✅ Appareil appairé avec succès au processus: ${pairingProcessId}`);
} catch (e) {
console.error('[Services:confirmPairing] 💥 Échec global de la confirmation du pairing:', e);
return;
}
}
public async updateDevice(): Promise<void> {
console.log("[Services:updateDevice] 🔄 Mise à jour de l'appareil...");
let myPairingProcessId: string;
try {
myPairingProcessId = this.getPairingProcessId();
} catch (e) {
console.error("[Services:updateDevice] 💥 Échec de l'obtention du pairing process id");
return;
}
const myPairingProcess = await this.getProcess(myPairingProcessId);
if (!myPairingProcess) {
console.error('[Services:updateDevice] 💥 Processus de pairing inconnu');
return;
}
const myPairingState = this.getLastCommitedState(myPairingProcess);
if (myPairingState) {
const encodedSpAddressList = myPairingState.public_data['pairedAddresses'];
const spAddressList = this.decodeValue(encodedSpAddressList);
if (spAddressList.length === 0) {
console.error('[Services:updateDevice] 💥 pairedAddresses est vide');
return;
}
// We can check if our address is included and simply unpair if it's not
if (!spAddressList.includes(this.getDeviceAddress())) {
console.warn("[Services:updateDevice] ⚠️ Notre adresse n'est plus dans la liste. Dissociation...");
await this.unpairDevice();
return;
}
// We can update the device with the new addresses
console.log("[Services:updateDevice] 🔄 Ré-appairage avec la nouvelle liste d'adresses...");
this.sdkClient.unpair_device();
this.sdkClient.pair_device(myPairingProcessId, spAddressList);
const newDevice = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(newDevice);
console.log('[Services:updateDevice] ✅ Appareil mis à jour.');
}
}
public pairDevice(processId: string, spAddressList: string[]): void {
try {
this.sdkClient.pair_device(processId, spAddressList);
} catch (e) {
throw new Error(`[Services:pairDevice] 💥 Échec: ${e}`);
}
}
public getAmount(): BigInt {
const amount = this.sdkClient.get_available_amount();
return amount;
}
getDeviceAddress(): string {
try {
return this.sdkClient.get_address();
} catch (e) {
throw new Error(`[Services:getDeviceAddress] 💥 Échec: ${e}`);
}
}
public dumpDeviceFromMemory(): Device {
try {
return this.sdkClient.dump_device();
} catch (e) {
throw new Error(`[Services:dumpDeviceFromMemory] 💥 Échec: ${e}`);
}
}
public dumpNeuteredDevice(): Device | null {
try {
return this.sdkClient.dump_neutered_device();
} catch (e) {
console.error(`[Services:dumpNeuteredDevice] 💥 Échec: ${e}`);
return null;
}
}
public getPairingProcessId(): string {
try {
return this.sdkClient.get_pairing_process_id();
} catch (e) {
throw new Error(`[Services:getPairingProcessId] 💥 Échec (Probablement non appairé): ${e}`);
}
}
async saveDeviceInDatabase(device: Device): Promise<void> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
console.log("[Services:saveDeviceInDatabase] 💾 Sauvegarde de l'appareil en BDD...", {
pairing_process_commitment: device.pairing_process_commitment,
paired_member: device.paired_member,
});
const prevDevice = await this.getDeviceFromDatabase();
if (prevDevice) {
// console.debug('[Services:saveDeviceInDatabase] Appareil précédent trouvé, suppression...');
await db.deleteObject(walletStore, '1');
}
await db.addObject({
storeName: walletStore,
object: { pre_id: '1', device },
key: null,
});
console.log('[Services:saveDeviceInDatabase] ✅ Appareil sauvegardé avec succès');
// // Verify save
// const savedDevice = await this.getDeviceFromDatabase();
// console.log('[Services:saveDeviceInDatabase] 🔎 Vérification:', {
// pairing_process_commitment: savedDevice?.pairing_process_commitment,
// paired_member: savedDevice?.paired_member,
// });
} catch (e) {
console.error('[Services:saveDeviceInDatabase] 💥 Erreur lors de la sauvegarde:', e);
}
}
async getDeviceFromDatabase(): Promise<Device | null> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
const dbRes = await db.getObject(walletStore, '1');
if (dbRes) {
return dbRes['device'];
} else {
return null;
}
} catch (e) {
throw new Error(`[Services:getDeviceFromDatabase] 💥 Échec: ${e}`);
}
}
async getMemberFromDevice(): Promise<string[] | null> {
try {
const device = await this.getDeviceFromDatabase();
if (device) {
const pairedMember = device['paired_member'];
return pairedMember.sp_addresses;
} else {
return null;
}
} catch (e) {
throw new Error(`[Services:getMemberFromDevice] 💥 Échec: ${e}`);
}
}
isChildRole(parent: any, child: any): boolean {
try {
this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child));
} catch (e) {
console.error(e);
return false;
}
return true;
}
rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
let us;
try {
us = this.sdkClient.get_pairing_process_id();
} catch (e) {
// Si non appairé, nous ne pouvons être dans aucun rôle
return false;
}
return this.rolesContainsMember(roles, us);
}
rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean {
for (const roleDef of Object.values(roles)) {
if (roleDef.members.includes(pairingProcessId)) {
return true;
}
}
return false;
}
async dumpWallet() {
const wallet = await this.sdkClient.dump_wallet();
return wallet;
}
public createFaucetMessage() {
const message = this.sdkClient.create_faucet_msg();
return message;
}
async createNewDevice() {
let spAddress = '';
try {
console.log("[Services:createNewDevice] ✨ Création d'un nouvel appareil...");
// We set birthday later when we have the chain tip from relay
spAddress = await this.sdkClient.create_new_device(0, 'signet');
const device = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(device);
console.log('[Services:createNewDevice] ✅ Appareil créé et sauvegardé.');
} catch (e) {
console.error('[Services:createNewDevice] 💥 Erreur:', e);
}
return spAddress;
}
public restoreDevice(device: Device) {
try {
console.log("[Services:restoreDevice] 🔄 Restauration de l'appareil en mémoire...");
this.sdkClient.restore_device(device);
} catch (e) {
console.error(e);
}
}
public async updateDeviceBlockHeight(): Promise<void> {
if (this.currentBlockHeight === -1) {
console.warn('[Services:updateDeviceBlockHeight] ⚠️ Hauteur de bloc actuelle non définie. Skip.');
return;
}
let device: Device | null = null;
try {
device = await this.getDeviceFromDatabase();
} catch (e) {
throw new Error(`[Services:updateDeviceBlockHeight] 💥 Échec de l'obtention de l'appareil depuis la BDD: ${e}`);
}
if (!device) {
console.error('[Services:updateDeviceBlockHeight] 💥 Appareil non trouvé. Skip.');
return;
}
const birthday = device.sp_wallet.birthday;
if (birthday === undefined || birthday === null) {
console.error('[Services:updateDeviceBlockHeight] 💥 "Birthday" non trouvé. Skip.');
return;
}
if (birthday === 0) {
console.log(`[Services:updateDeviceBlockHeight] 🎂 C'est un nouvel appareil. Définition du "birthday" à ${this.currentBlockHeight}`);
// This is a new device, so current chain tip is its birthday
device.sp_wallet.birthday = this.currentBlockHeight;
// We also set last_scan, impossible that we need to scan earlier than this
device.sp_wallet.last_scan = this.currentBlockHeight;
try {
// First set the updated device in memory
this.sdkClient.restore_device(device);
// Then save it to database
await this.saveDeviceInDatabase(device);
} catch (e) {
throw new Error(`[Services:updateDeviceBlockHeight] 💥 Échec de la sauvegarde de l'appareil mis à jour: ${e}`);
}
} else {
// This is existing device, we need to catch up if last_scan is lagging behind chain_tip
if (device.sp_wallet.last_scan < this.currentBlockHeight) {
console.log(`[Services:updateDeviceBlockHeight] 🏃 Rattrapage... Scan des blocs de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`);
try {
await this.sdkClient.scan_blocks(this.currentBlockHeight, BLINDBITURL);
} catch (e) {
console.error(`[Services:updateDeviceBlockHeight] 💥 Échec du scan des blocs: ${e}`);
return;
}
// If everything went well, we can update our storage
try {
const device = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(device);
console.log('[Services:updateDeviceBlockHeight] ✅ Scan terminé et appareil sauvegardé.');
} catch (e) {
console.error(`[Services:updateDeviceBlockHeight] 💥 Échec de la sauvegarde de l'appareil après scan: ${e}`);
}
} else {
// console.debug('[Services:updateDeviceBlockHeight] Portefeuille à jour. Rien à faire.');
return;
}
}
}
private async removeProcess(processId: string): Promise<void> {
const db = await Database.getInstance();
const storeName = 'processes';
try {
console.log(`[Services:removeProcess] 🗑️ Suppression du processus ${processId}`);
await db.deleteObject(storeName, processId);
} catch (e) {
console.error(e);
}
}
public async batchSaveProcessesToDb(processes: Record<string, Process>) {
if (Object.keys(processes).length === 0) {
return;
}
console.log(`[Services:batchSaveProcessesToDb] 💾 Sauvegarde de ${Object.keys(processes).length} processus en BDD...`);
const db = await Database.getInstance();
const storeName = 'processes';
try {
await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
this.processesCache = { ...this.processesCache, ...processes };
} catch (e) {
console.error('[Services:batchSaveProcessesToDb] 💥 Échec:', e);
throw e;
}
}
public async saveProcessToDb(processId: string, process: Process) {
const db = await Database.getInstance();
const storeName = 'processes';
try {
await db.addObject({
storeName,
object: process,
key: processId,
});
// Update the process in the cache
this.processesCache[processId] = process;
} catch (e) {
console.error(`[Services:saveProcessToDb] 💥 Échec de la sauvegarde du processus ${processId}: ${e}`);
}
}
public async saveBlobToDb(hash: string, data: Blob) {
const db = await Database.getInstance();
try {
await db.addObject({
storeName: 'data',
object: data,
key: hash,
});
} catch (e) {
console.error(`[Services:saveBlobToDb] 💥 Échec de la sauvegarde du blob ${hash}: ${e}`);
}
}
public async getBlobFromDb(hash: string): Promise<Blob | null> {
const db = await Database.getInstance();
try {
return await db.getObject('data', hash);
} catch (e) {
return null;
}
}
public async saveDataToStorage(storages: string[], hash: string, data: Blob, ttl: number | null) {
try {
await storeData(storages, hash, data, ttl);
} catch (e) {
console.error(`[Services:saveDataToStorage] 💥 Échec du stockage du hash ${hash}: ${e}`);
}
}
public async fetchValueFromStorage(hash: string): Promise<ArrayBuffer | null> {
const storages = [STORAGEURL];
return await retrieveData(storages, hash);
}
public async getDiffByValueFromDb(hash: string): Promise<UserDiff | null> {
const db = await Database.getInstance();
const diff = await db.getObject('diffs', hash);
return diff;
}
public async saveDiffsToDb(diffs: UserDiff[]) {
const db = await Database.getInstance();
try {
for (const diff of diffs) {
await db.addObject({
storeName: 'diffs',
object: diff,
key: null,
});
}
} catch (e) {
throw new Error(`[Services:saveDiffsToDb] 💥 Échec: ${e}`);
}
}
public async getProcess(processId: string): Promise<Process | null> {
// 1. Essayer le cache en mémoire
if (this.processesCache[processId]) {
return this.processesCache[processId];
}
// 2. Si non trouvé, essayer la BDD
try {
const db = await Database.getInstance();
const process = await db.getObject('processes', processId);
if (process) {
this.processesCache[processId] = process; // Mettre en cache
}
return process;
} catch (e) {
console.error(`[Services:getProcess] 💥 Échec de récupération du processus ${processId}:`, e);
return null;
}
}
public async getProcesses(): Promise<Record<string, Process>> {
// 1. Essayer le cache en mémoire
if (Object.keys(this.processesCache).length > 0) {
return this.processesCache;
}
// 2. Si non trouvé, charger depuis la BDD
try {
console.log('[Services:getProcesses] Cache de processus vide. Chargement depuis la BDD...');
const db = await Database.getInstance();
this.processesCache = await db.dumpStore('processes');
console.log(`[Services:getProcesses] ✅ ${Object.keys(this.processesCache).length} processus chargés en cache.`);
return this.processesCache;
} catch (e) {
console.error('[Services:getProcesses] 💥 Échec du chargement des processus:', e);
throw e;
}
}
public async restoreProcessesFromBackUp(processes: Record<string, Process>) {
console.log(`[Services:restoreProcessesFromBackUp] 💾 Restauration de ${Object.keys(processes).length} processus depuis un backup...`);
const db = await Database.getInstance();
const storeName = 'processes';
try {
await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
} catch (e) {
throw e;
}
await this.restoreProcessesFromDB();
}
// Restore processes cache from persistent storage
public async restoreProcessesFromDB() {
const db = await Database.getInstance();
try {
const processes: Record<string, Process> = await db.dumpStore('processes');
if (processes && Object.keys(processes).length != 0) {
console.log(`[Services:restoreProcessesFromDB] 🔄 Restauration de ${Object.keys(processes).length} processus depuis la BDD vers le cache...`);
this.processesCache = processes;
} else {
console.log('[Services:restoreProcessesFromDB] Aucun processus à restaurer.');
}
} catch (e) {
throw e;
}
}
public async restoreSecretsFromBackUp(secretsStore: SecretsStore) {
console.log('[Services:restoreSecretsFromBackUp] 💾 Restauration des secrets depuis un backup...');
const db = await Database.getInstance();
for (const secret of secretsStore.unconfirmed_secrets) {
await db.addObject({
storeName: 'unconfirmed_secrets',
object: secret,
key: null,
});
}
const entries = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value }));
for (const entry of entries) {
await db.addObject({
storeName: 'shared_secrets',
object: entry.value,
key: entry.key,
});
}
// Now we can transfer them to memory
await this.restoreSecretsFromDB();
}
public async restoreSecretsFromDB() {
console.log('[Services:restoreSecretsFromDB] 🔄 Restauration des secrets depuis la BDD vers la mémoire SDK...');
const db = await Database.getInstance();
try {
const sharedSecrets: Record<string, string> = await db.dumpStore('shared_secrets');
const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets');
const secretsStore = {
shared_secrets: sharedSecrets,
unconfirmed_secrets: Object.values(unconfirmedSecrets),
};
this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore));
console.log(`[Services:restoreSecretsFromDB] ✅ ${Object.keys(sharedSecrets).length} secrets partagés restaurés.`);
} catch (e) {
throw e;
}
}
decodeValue(value: number[]): any | null {
try {
return this.sdkClient.decode_value(value);
} catch (e) {
console.error(`[Services:decodeValue] 💥 Échec: ${e}`);
return null;
}
}
async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise<any | null> {
// Le groupe principal est "collapsed" pour ne pas polluer la console par défaut
console.groupCollapsed(`[Services:decryptAttribute] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`);
try {
let hash = state.pcd_commitment[attribute];
if (!hash) {
console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`);
return null; // Le 'finally' s'exécutera
}
let key = state.keys[attribute];
const pairingProcessId = this.getPairingProcessId();
// If key is missing, request an update and then retry
if (!key) {
// On crée un sous-groupe pour la logique de récupération de la clé
console.group(`🔐 Gestion de la clé manquante pour '${attribute}'`);
console.warn(`Vérification de l'accès et demande aux pairs...`);
const roles = state.roles;
let hasAccess = false;
// If we're not supposed to have access to this attribute, ignore
for (const role of Object.values(roles)) {
for (const rule of Object.values(role.validation_rules)) {
if (rule.fields.includes(attribute)) {
if (role.members.includes(pairingProcessId)) {
// We have access to this attribute
hasAccess = true;
break;
}
}
}
}
if (!hasAccess) {
console.log(`⛔ Accès non autorisé. Abandon.`);
console.groupEnd(); // Ferme le sous-groupe "Gestion de la clé manquante"
return null; // Le 'finally' principal s'exécutera
}
const process = await this.getProcess(processId);
if (!process) {
console.error(`💥 Impossible de trouver le processus ${processId} pour ensureConnections.`);
console.groupEnd(); // Ferme le sous-groupe "Gestion de la clé manquante"
return null;
}
await this.ensureConnections(process);
// We should have the key, so we're going to ask other members for it
console.log(`🗣️ Demande de données aux pairs...`);
await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
const maxRetries = 5;
const retryDelay = 500; // delay in milliseconds
let retries = 0;
// On crée un sous-groupe replié pour la boucle de réessai (potentiellement verbeuse)
console.groupCollapsed(`⏳ Boucle d'attente de la clé (max ${maxRetries} tentatives)`);
while ((!hash || !key) && retries < maxRetries) {
console.log(`(Tentative ${retries + 1}/${maxRetries})...`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
// Re-read hash and key after waiting
const updatedProcess = await this.getProcess(processId);
const updatedState = this.getStateFromId(updatedProcess, state.state_id);
if (updatedState) {
hash = updatedState.pcd_commitment[attribute];
key = updatedState.keys[attribute];
}
retries++;
}
console.groupEnd(); // Ferme le sous-groupe "Boucle d'attente"
console.groupEnd(); // Ferme le sous-groupe "Gestion de la clé manquante"
} // Fin de if (!key)
if (hash && key) {
console.log(` Clé et hash trouvés. Tentative de déchiffrement...`);
const blob = await this.getBlobFromDb(hash);
if (blob) {
// Decrypt the data
const buf = await blob.arrayBuffer();
const cipher = new Uint8Array(buf);
const keyUIntArray = this.hexToUInt8Array(key);
try {
const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher);
if (clear) {
// deserialize the result to get the actual data
const decoded = this.sdkClient.decode_value(clear);
console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`);
return decoded; // Le 'finally' s'exécutera
} else {
throw new Error('decrypt_data returned null');
}
} catch (e) {
console.error(`💥 Échec du déchiffrement (decrypt_data): ${e}`);
}
} else {
console.error(`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`);
}
} else {
console.error(`💥 Échec: Clé ou hash manquant après ${maxRetries} tentatives pour '${attribute}'.`);
}
return null; // Le 'finally' s'exécutera
} catch (error) {
// Intercepte les erreurs inattendues non gérées
console.error(`💥 Erreur inattendue dans decryptAttribute:`, error);
return null;
} finally {
// Ce bloc est TOUJOURS exécuté, assurant que le groupe est fermé.
console.groupEnd();
}
}
getNotifications(): any[] | null {
// ... (Logique inchangée)
return this.notifications;
}
setNotifications(notifications: any[]) {
this.notifications = notifications;
}
async importJSON(backup: BackUp): Promise<void> {
console.log("[Services:importJSON] 📥 Importation d'un backup JSON...");
const device = backup.device;
// Reset current device
await this.resetDevice();
await this.saveDeviceInDatabase(device);
this.restoreDevice(device);
// TODO restore secrets and processes from file
const secretsStore = backup.secrets;
await this.restoreSecretsFromBackUp(secretsStore);
const processes = backup.processes;
await this.restoreProcessesFromBackUp(processes);
console.log('[Services:importJSON] ✅ Backup importé avec succès.');
}
public async createBackUp(): Promise<BackUp | null> {
console.log("[Services:createBackUp] 📤 Création d'un backup...");
// Get the device from indexedDB
const device = await this.getDeviceFromDatabase();
if (!device) {
console.error('[Services:createBackUp] 💥 Aucun appareil chargé');
return null;
}
// Get the processes
const processes = await this.getProcesses();
// Get the shared secrets
const secrets = await this.getAllSecrets();
// Create a backup object
const backUp = {
device: device,
secrets: secrets,
processes: processes,
};
console.log('[Services:createBackUp] ✅ Backup créé.');
return backUp;
}
// Device 1 wait Device 2
public device1: boolean = false;
public device2Ready: boolean = false;
public resetState() {
this.device1 = false;
this.device2Ready = false;
}
// --- AMÉLIORATION: Refactorisé en sous-fonctions ---
public async handleHandshakeMsg(url: string, parsedMsg: any) {
try {
const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg);
// 1. Mettre à jour les infos du relais (SP Address, Block Height)
this.updateRelayInfo(url, handshakeMsg);
// 2. Mettre à jour la liste globale des membres
this.updateGlobalMembersList(handshakeMsg.peers_list);
// 3. Lancer la synchronisation (lourde) des processus en arrière-plan
// (Anciennement dans un setTimeout, maintenant dans une fonction dédiée)
this.syncProcessesFromHandshake(handshakeMsg.processes_list, url).catch((e) => console.error(`[Services:syncProcessesFromHandshake] 💥 Échec de la synchro des processus pour ${url}:`, e));
} catch (e) {
console.error(`[Services:handleHandshakeMsg] 💥 Échec de l'analyse du message init:`, e);
}
}
/**
* Sous-fonction de handleHandshakeMsg: Met à jour les infos du relais.
*/
private updateRelayInfo(url: string, handshakeMsg: HandshakeMessage) {
if (handshakeMsg.sp_address) {
this.updateRelay(url, handshakeMsg.sp_address);
this.relayAddresses[url] = handshakeMsg.sp_address;
this.resolveRelayReady();
}
console.log(`[Services:updateRelayInfo] Handshake reçu de ${url}:`, handshakeMsg);
this.currentBlockHeight = handshakeMsg.chain_tip;
console.log(`[Services:updateRelayInfo] ⛓️ Hauteur de bloc actuelle: ${this.currentBlockHeight}`);
this.updateDeviceBlockHeight();
}
/**
* Sous-fonction de handleHandshakeMsg: Met à jour la liste globale des membres.
*/
private updateGlobalMembersList(peers_list: Record<string, Member> | null) {
console.log('[Services:updateGlobalMembersList] 🔄 Mise à jour de la liste des membres...');
if (!peers_list || Object.keys(peers_list).length === 0) {
console.log('[Services:updateGlobalMembersList] Aucune liste de pairs reçue.');
return;
}
if (this.membersList && Object.keys(this.membersList).length === 0) {
console.log('[Services:updateGlobalMembersList] ⚠️ Écrasement de la membersList vide avec la liste des pairs reçue.');
this.membersList = peers_list;
} else {
console.log('[Services:updateGlobalMembersList] Incrémentation de la membersList existante avec les nouveaux pairs.');
for (const [processId, member] of Object.entries(peers_list)) {
this.membersList[processId] = member as Member;
}
}
console.log(`[Services:updateGlobalMembersList] ✅ Liste des membres mise à jour. Total: ${Object.keys(this.membersList).length}`);
}
/**
* Sous-fonction de handleHandshakeMsg: Gère la logique complexe de synchro des processus.
*/
private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap, url: string) {
if (!newProcesses || Object.keys(newProcesses).length === 0) {
console.debug(`[Services:syncProcesses] Reçu une liste de processus vide de ${url}.`);
return;
}
console.log(`[Services:syncProcesses] 🔄 Synchronisation de ${Object.keys(newProcesses).length} processus depuis ${url}...`);
if (this.processesCache && Object.keys(this.processesCache).length === 0) {
// Cas simple: BDD vide, on sauvegarde tout
console.log('[Services:syncProcesses] Cache vide, sauvegarde en batch...');
try {
await this.batchSaveProcessesToDb(newProcesses);
} catch (e) {
console.error('[Services:syncProcesses] 💥 Échec de la sauvegarde en batch:', e);
}
} else {
// Cas complexe: Mise à jour différentielle
const toSave: Record<string, Process> = {};
for (const [processId, process] of Object.entries(newProcesses)) {
const existing = await this.getProcess(processId);
if (existing) {
// --- Logique de mise à jour d'un processus existant ---
let newStates: string[] = [];
let newRoles: Record<string, RoleDefinition>[] = [];
for (const state of process.states) {
if (!state || !state.state_id) continue;
if (state.state_id === EMPTY32BYTES) {
// ... (Logique du 'tip') ...
const existingTip = existing.states[existing.states.length - 1].commited_in;
if (existingTip !== state.commited_in) {
console.log(`[Services:syncProcesses] Nouveau 'tip' trouvé pour ${processId}`);
existing.states.pop();
existing.states.push(state);
toSave[processId] = existing;
}
} else if (!this.lookForStateId(existing, state.state_id)) {
// ... (Logique d'ajout de nouvel état) ...
console.log(`[Services:syncProcesses] Nouvel état ${state.state_id} trouvé pour ${processId}`);
const existingLastState = existing.states.pop();
if (!existingLastState) {
console.error(`[Services:syncProcesses] 💥 Échec: impossible de trouver le dernier état pour ${processId}`);
break;
}
existing.states.push(state);
existing.states.push(existingLastState);
toSave[processId] = existing;
if (this.rolesContainsUs(state.roles)) {
newStates.push(state.state_id);
newRoles.push(state.roles);
}
} else {
// ... (Logique de vérification des clés pour un état existant) ...
const existingState = this.getStateFromId(existing, state.state_id);
if (existingState!.keys && Object.keys(existingState!.keys).length != 0) {
continue; // On a déjà les clés
} else {
if (this.rolesContainsUs(state.roles)) {
console.log(`[Services:syncProcesses] État ${state.state_id} (processus ${processId}) existe, mais clés manquantes. Demande...`);
newStates.push(state.state_id);
newRoles.push(state.roles);
} else {
continue; // Pas notre état
}
}
}
}
if (newStates.length != 0) {
console.log(`[Services:syncProcesses] 📞 Demande de données pour ${newStates.length} nouvel(s) état(s) dans ${processId}`);
await this.ensureConnections(existing);
await this.requestDataFromPeers(processId, newStates, newRoles);
}
} else {
// Processus totalement nouveau, on l'ajoute
console.log(`[Services:syncProcesses] Nouveau processus ${processId} trouvé. Ajout.`);
toSave[processId] = process;
}
}
if (toSave && Object.keys(toSave).length > 0) {
console.log('[Services:syncProcesses] 💾 Sauvegarde en batch des processus mis à jour/nouveaux...', Object.keys(toSave));
await this.batchSaveProcessesToDb(toSave);
}
}
console.log('[Services:syncProcesses] ✅ Synchronisation terminée. Émission de l\'événement "processes-updated".');
document.dispatchEvent(new CustomEvent('processes-updated'));
}
private lookForStateId(process: Process, stateId: string): boolean {
for (const state of process.states) {
if (state.state_id === stateId) {
return true;
}
}
return false;
}
/**
* Waits for at least one handshake message to be received from any connected relay.
*/
private async waitForHandshakeMessage(timeoutMs: number = 10000): Promise<void> {
const startTime = Date.now();
const pollInterval = 100; // Check every 100ms
return new Promise<void>((resolve, reject) => {
const checkForHandshake = () => {
// Check if we have any members or any relays (indicating handshake was received)
if (Object.keys(this.membersList).length > 0 || Object.values(this.relayAddresses).some((addr) => addr !== '')) {
console.log('[Services:waitForHandshakeMessage] ✅ Handshake reçu (membres ou relais présents)');
resolve();
return;
}
// Check timeout
if (Date.now() - startTime >= timeoutMs) {
reject(new Error(`[Services:waitForHandshakeMessage] ❌ Aucun handshake reçu après ${timeoutMs}ms`));
return;
}
// Continue polling
setTimeout(checkForHandshake, pollInterval);
};
checkForHandshake();
});
}
/**
* Retourne la liste de tous les membres ordonnés par leur process id
* @returns Un tableau contenant tous les membres
*/
public getAllMembersSorted(): Record<string, Member> {
return Object.fromEntries(Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)));
}
public getAllMembers(): Record<string, Member> {
return this.membersList;
}
public getAddressesForMemberId(memberId: string): string[] | null {
try {
// console.debug('[Services:getAddressesForMemberId] 🔍 Recherche d\'adresses pour:', memberId);
if (Object.keys(this.membersList).length === 0) {
console.error('[Services:getAddressesForMemberId] ❌ ERREUR: membersList est VIDE. Impossible de trouver le membre:', memberId);
return null;
}
if (!this.membersList[memberId]) {
console.error(`[Services:getAddressesForMemberId] ❌ ERREUR: Membre non trouvé dans membersList: ${memberId}`);
// console.debug('Membres actuels:', this.membersList);
return null;
}
return this.membersList[memberId].sp_addresses;
} catch (e) {
console.error('[Services:getAddressesForMemberId] 💥 Erreur inattendue:', e);
return null;
}
}
public compareMembers(memberA: string[], memberB: string[]): boolean {
if (!memberA || !memberB) {
return false;
}
if (memberA.length !== memberB.length) {
return false;
}
const res = memberA.every((item) => memberB.includes(item)) && memberB.every((item) => memberA.includes(item));
return res;
}
public async handleCommitError(response: string) {
const content = JSON.parse(response);
const error = content.error;
const errorMsg = error['GenericError'];
console.warn(`[Services:handleCommitError] ⚠️ Erreur de Commit reçue: ${errorMsg}`);
const dontRetry = ['State is identical to the previous state', 'Not enough valid proofs', 'Not enough members to validate'];
if (dontRetry.includes(errorMsg)) {
console.warn('[Services:handleCommitError] ⏩ Erreur non récupérable. Pas de nouvelle tentative.');
return;
}
// Wait and retry
console.warn('[Services:handleCommitError] 🔄 Tentative de renvoi du Commit dans 1s...');
setTimeout(async () => {
this.sendCommitMessage(JSON.stringify(content));
}, 1000);
}
public getRoles(process: Process): Record<string, RoleDefinition> | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) {
return lastCommitedState!.roles;
} else if (process.states.length === 2) {
// Cas spécial pour les processus venant d'être créés ?
const firstState = process.states[0];
if (firstState && firstState.roles && Object.keys(firstState.roles).length != 0) {
return firstState!.roles;
}
}
return null;
}
public getPublicData(process: Process): Record<string, any> | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data && Object.keys(lastCommitedState.public_data).length != 0) {
return lastCommitedState!.public_data;
} else if (process.states.length === 2) {
const firstState = process.states[0];
if (firstState && firstState.public_data && Object.keys(firstState.public_data).length != 0) {
return firstState!.public_data;
}
}
return null;
}
public getProcessName(process: Process): string | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data) {
const processName = lastCommitedState!.public_data['processName'];
if (processName) {
return this.decodeValue(processName);
} else {
return null;
}
} else {
return null;
}
}
public async getMyProcesses(): Promise<string[] | null> {
// If we're not paired yet, just skip it
let pairingProcessId = null;
try {
pairingProcessId = this.getPairingProcessId();
} catch (e) {
return null; // Pas appairé
}
if (!pairingProcessId) {
return null;
}
try {
const processes = await this.getProcesses();
const newMyProcesses = new Set<string>(this.myProcesses || []);
// MyProcesses automatically contains pairing process
newMyProcesses.add(pairingProcessId);
for (const [processId, process] of Object.entries(processes)) {
if (newMyProcesses.has(processId)) {
continue;
}
try {
const roles = this.getRoles(process);
if (roles && this.rolesContainsUs(roles)) {
newMyProcesses.add(processId);
}
} catch (e) {
console.error(e);
}
}
this.myProcesses = newMyProcesses; // atomic update
return Array.from(this.myProcesses);
} catch (e) {
console.error('[Services:getMyProcesses] 💥 Échec:', e);
return null;
}
}
public async requestDataFromPeers(processId: string, stateIds: string[], roles: Record<string, RoleDefinition>[]) {
console.log(`[Services:requestDataFromPeers] 🗣️ Demande de données pour ${processId} (états: ${stateIds.join(', ')})`);
const membersList = this.getAllMembers();
try {
const res = this.sdkClient.request_data(processId, stateIds, roles, membersList);
await this.handleApiReturn(res);
} catch (e) {
console.error(e);
}
}
public hexToBlob(hexString: string): Blob {
const uint8Array = this.hexToUInt8Array(hexString);
return new Blob([uint8Array], { type: 'application/octet-stream' });
}
public hexToUInt8Array(hexString: string): Uint8Array {
if (hexString.length % 2 !== 0) {
throw new Error('Invalid hex string: length must be even');
}
const uint8Array = new Uint8Array(hexString.length / 2);
for (let i = 0; i < hexString.length; i += 2) {
uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
}
return uint8Array;
}
public async blobToHex(blob: Blob): Promise<string> {
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
return Array.from(bytes)
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}
public getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): string {
return this.sdkClient.hash_value(fileBlob, commitedIn, label);
}
public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult {
return this.sdkClient.get_merkle_proof(processState, attributeName);
}
public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean {
try {
return this.sdkClient.validate_merkle_proof(proof, hash);
} catch (e) {
throw new Error(`[Services:validateMerkleProof] 💥 Échec: ${e}`);
}
}
public getLastCommitedState(process: Process): ProcessState | null {
if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in;
const lastCommitedState = process.states.findLast((state) => state.commited_in !== processTip);
if (lastCommitedState) {
return lastCommitedState;
} else {
return null;
}
}
public getLastCommitedStateIndex(process: Process): number | null {
if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in;
for (let i = process.states.length - 1; i >= 0; i--) {
if (process.states[i].commited_in !== processTip) {
return i;
}
}
return null;
}
public getUncommitedStates(process: Process): ProcessState[] {
if (process.states.length === 0) return [];
const processTip = process.states[process.states.length - 1].commited_in;
const res = process.states.filter((state) => state.commited_in === processTip);
return res.filter((state) => state.state_id !== EMPTY32BYTES);
}
public getStateFromId(process: Process, stateId: string): ProcessState | null {
if (process.states.length === 0) return null;
const state = process.states.find((state) => state.state_id === stateId);
if (state) {
return state;
} else {
return null;
}
}
public getNextStateAfterId(process: Process, stateId: string): ProcessState | null {
if (process.states.length === 0) return null;
const index = process.states.findIndex((state) => state.state_id === stateId);
if (index !== -1 && index < process.states.length - 1) {
return process.states[index + 1];
}
return null;
}
public isPairingProcess(roles: Record<string, RoleDefinition>): boolean {
if (Object.keys(roles).length != 1) {
return false;
}
const pairingRole = roles['pairing'];
if (pairingRole) {
// For now that's enough, we should probably test more things
return true;
} else {
return false;
}
}
public async updateMemberPublicName(process: Process, newName: string): Promise<ApiReturn> {
const publicData = {
memberPublicName: newName,
};
return await this.updateProcess(process, {}, publicData, null);
}
}