2356 lines
92 KiB
TypeScript
Executable File
2356 lines
92 KiB
TypeScript
Executable File
// @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);
|
||
}
|
||
}
|