refactor(core): découpage du service principal en modules métier et centralisation de la configuration globale

This commit is contained in:
NicolasCantu 2025-11-26 12:40:36 +01:00
parent 3eeef3fc9a
commit 04996ceaa5
11 changed files with 1077 additions and 2182 deletions

31
src/config/constants.ts Normal file
View File

@ -0,0 +1,31 @@
export const APP_CONFIG = {
// --- Cryptographie & Limites ---
U32_MAX: 4294967295,
EMPTY_32_BYTES: String('').padStart(64, '0'),
// --- Économie ---
DEFAULT_AMOUNT: 1000n,
FEE_RATE: 1, // Sat/vByte ou unité arbitraire selon le SDK
// --- Délais & Timeouts (ms) ---
TIMEOUTS: {
POLLING_INTERVAL: 100, // Vérification rapide (ex: handshake)
API_DELAY: 500, // Petit délai pour laisser respirer le réseau (hack)
RETRY_DELAY: 1000, // Délai avant de réessayer une action
FAUCET_WAIT: 2000, // Attente après appel faucet
WORKER_CHECK: 5000, // Vérification périodique du worker
HANDSHAKE: 10000, // Timeout max pour le handshake
KEY_REQUEST: 15000, // Timeout pour recevoir une clé d'un pair
WS_RECONNECT_MAX: 30000, // Délai max entre deux tentatives de reco WS
WS_HEARTBEAT: 30000, // Ping WebSocket
},
// --- URLs (Environnement) ---
URLS: {
BASE: import.meta.env.VITE_BASEURL || 'http://localhost',
BOOTSTRAP: [import.meta.env.VITE_BOOTSTRAPURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8090`],
STORAGE: import.meta.env.VITE_STORAGEURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8081`,
BLINDBIT: import.meta.env.VITE_BLINDBITURL || `${import.meta.env.VITE_BASEURL || 'http://localhost'}:8000`,
},
};

View File

@ -0,0 +1,85 @@
import { initWebsocket, sendMessage } from '../websockets.service.ts';
import { AnkFlag } from '../../../pkg/sdk_client';
export class NetworkService {
private relayAddresses: { [wsurl: string]: string } = {};
private relayReadyResolver: (() => void) | null = null;
private relayReadyPromise: Promise<void> | null = null;
constructor(private bootstrapUrls: string[]) {}
public async connectAllRelays(): Promise<void> {
const connectedUrls: string[] = [];
for (const wsurl of Object.keys(this.relayAddresses)) {
try {
await this.addWebsocketConnection(wsurl);
connectedUrls.push(wsurl);
} catch (error) {
console.error(`[Network] ❌ Échec connexion ${wsurl}:`, error);
}
}
}
// --- AJOUT ---
public async addWebsocketConnection(url: string): Promise<void> {
console.log(`[Network] 🔌 Connexion à: ${url}`);
await initWebsocket(url);
}
// -------------
public initRelays() {
for (const wsurl of this.bootstrapUrls) {
this.updateRelay(wsurl, '');
}
}
public updateRelay(url: string, spAddress: string) {
console.log(`[Network] Mise à jour relais ${url} -> ${spAddress}`);
this.relayAddresses[url] = spAddress;
if (spAddress) this.resolveRelayReady();
}
public getAvailableRelayAddress(): Promise<string> {
let relayAddress = Object.values(this.relayAddresses).find((addr) => addr !== '');
if (relayAddress) return Promise.resolve(relayAddress);
console.log("[Network] ⏳ Attente d'un relais disponible...");
return this.getRelayReadyPromise().then(() => {
const addr = Object.values(this.relayAddresses).find((a) => a !== '');
if (!addr) throw new Error('Aucun relais disponible');
return addr;
});
}
public printAllRelays(): void {
console.log('[Network] Adresses relais actuelles:');
for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) {
console.log(`${wsurl} -> ${spAddress}`);
}
}
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 getAllRelays() {
return this.relayAddresses;
}
public sendMessage(flag: AnkFlag, message: string) {
sendMessage(flag, message);
}
}

View File

@ -0,0 +1,26 @@
import { ApiReturn, Device } from '../../../pkg/sdk_client';
export class SdkService {
private client: any;
async init() {
this.client = await import('../../../pkg/sdk_client');
this.client.setup();
}
public getClient(): any {
if (!this.client) throw new Error('SDK not initialized');
return this.client;
}
// Méthodes utilitaires directes du SDK
public encodeJson(data: any): any {
return this.client.encode_json(data);
}
public encodeBinary(data: any): any {
return this.client.encode_binary(data);
}
public decodeValue(value: number[]): any {
return this.client.decode_value(value);
}
}

View File

@ -1,4 +1,5 @@
import Services from './service';
import { APP_CONFIG } from '../config/constants';
export class Database {
private static instance: Database;
@ -161,7 +162,7 @@ export class Database {
if (payload && payload.length != 0) {
activeWorker?.postMessage({ type: 'SCAN', payload });
}
}, 5000);
}, APP_CONFIG.TIMEOUTS.WORKER_CHECK);
} catch (error) {
console.error('[Database] 💥 Erreur critique Service Worker:', error);
}

View File

@ -0,0 +1,58 @@
import { MerkleProofResult, ProcessState } from '../../../pkg/sdk_client';
import { SdkService } from '../core/sdk.service';
export class CryptoService {
constructor(private sdk: SdkService) {}
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');
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.sdk.getClient().hash_value(fileBlob, commitedIn, label);
}
public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult {
return this.sdk.getClient().get_merkle_proof(processState, attributeName);
}
public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean {
return this.sdk.getClient().validate_merkle_proof(proof, hash);
}
public 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 };
}
private isFileBlob(value: any): value is { type: string; data: Uint8Array } {
return typeof value === 'object' && value !== null && typeof value.type === 'string' && value.data instanceof Uint8Array;
}
}

View File

@ -0,0 +1,100 @@
import { Process, ProcessState, RoleDefinition } from '../../../pkg/sdk_client';
import { SdkService } from '../core/sdk.service';
import Database from '../database.service';
const EMPTY32BYTES = String('').padStart(64, '0');
export class ProcessService {
private processesCache: Record<string, Process> = {};
private myProcesses: Set<string> = new Set();
constructor(private sdk: SdkService) {}
public async getProcess(processId: string): Promise<Process | null> {
if (this.processesCache[processId]) return this.processesCache[processId];
const db = await Database.getInstance();
const process = await db.getObject('processes', processId);
if (process) this.processesCache[processId] = process;
return process;
}
public async getProcesses(): Promise<Record<string, Process>> {
if (Object.keys(this.processesCache).length > 0) return this.processesCache;
const db = await Database.getInstance();
this.processesCache = await db.dumpStore('processes');
return this.processesCache;
}
public async saveProcessToDb(processId: string, process: Process) {
const db = await Database.getInstance();
await db.addObject({ storeName: 'processes', object: process, key: processId });
this.processesCache[processId] = process;
}
public async batchSaveProcesses(processes: Record<string, Process>) {
if (Object.keys(processes).length === 0) return;
const db = await Database.getInstance();
await db.batchWriting({ storeName: 'processes', objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
this.processesCache = { ...this.processesCache, ...processes };
}
public getLastCommitedState(process: Process): ProcessState | null {
if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in;
return process.states.findLast((state) => state.commited_in !== processTip) || null;
}
public getUncommitedStates(process: Process): ProcessState[] {
if (process.states.length === 0) return [];
const processTip = process.states[process.states.length - 1].commited_in;
return process.states.filter((state) => state.commited_in === processTip).filter((state) => state.state_id !== EMPTY32BYTES);
}
public getStateFromId(process: Process, stateId: string): ProcessState | null {
return process.states.find((state) => state.state_id === stateId) || null;
}
public getRoles(process: Process): Record<string, RoleDefinition> | null {
const last = this.getLastCommitedState(process);
if (last?.roles && Object.keys(last.roles).length > 0) return last.roles;
const first = process.states[0];
if (first?.roles && Object.keys(first.roles).length > 0) return first.roles;
return null;
}
public rolesContainsMember(roles: Record<string, RoleDefinition>, memberId: string): boolean {
return Object.values(roles).some((role) => role.members.includes(memberId));
}
public async getMyProcesses(pairingProcessId: string): Promise<string[]> {
const processes = await this.getProcesses();
const newMyProcesses = new Set<string>(this.myProcesses);
if (pairingProcessId) newMyProcesses.add(pairingProcessId);
for (const [processId, process] of Object.entries(processes)) {
if (newMyProcesses.has(processId)) continue;
const roles = this.getRoles(process);
if (roles && this.rolesContainsMember(roles, pairingProcessId)) {
newMyProcesses.add(processId);
}
}
this.myProcesses = newMyProcesses;
return Array.from(this.myProcesses);
}
// --- AJOUT : Méthode manquante ---
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;
}
}

View File

@ -0,0 +1,101 @@
import { Device } from '../../../pkg/sdk_client';
import { SdkService } from '../core/sdk.service';
import Database from '../database.service';
export class WalletService {
constructor(private sdk: SdkService) {}
public isPaired(): boolean {
try {
return this.sdk.getClient().is_paired();
} catch (e) {
return false;
}
}
public getAmount(): BigInt {
return this.sdk.getClient().get_available_amount();
}
public getDeviceAddress(): string {
return this.sdk.getClient().get_address();
}
public getPairingProcessId(): string {
return this.sdk.getClient().get_pairing_process_id();
}
public async createNewDevice(chainTip: number): Promise<string> {
const spAddress = await this.sdk.getClient().create_new_device(0, 'signet');
const device = this.dumpDeviceFromMemory();
if (device.sp_wallet.birthday === 0) {
device.sp_wallet.birthday = chainTip;
device.sp_wallet.last_scan = chainTip;
this.sdk.getClient().restore_device(device);
}
await this.saveDeviceInDatabase(device);
return spAddress;
}
public dumpDeviceFromMemory(): Device {
return this.sdk.getClient().dump_device();
}
public dumpNeuteredDevice(): Device | null {
try {
return this.sdk.getClient().dump_neutered_device();
} catch (e) {
return null;
}
}
// --- AJOUTS (Manquants) ---
public async dumpWallet(): Promise<any> {
return await this.sdk.getClient().dump_wallet();
}
public 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(`[WalletService] Échec: ${e}`);
}
}
// -------------------------
public async saveDeviceInDatabase(device: Device): Promise<void> {
const db = await Database.getInstance();
await db.deleteObject('wallet', '1').catch(() => {});
await db.addObject({
storeName: 'wallet',
object: { pre_id: '1', device },
key: null,
});
}
public async getDeviceFromDatabase(): Promise<Device | null> {
const db = await Database.getInstance();
const res = await db.getObject('wallet', '1');
return res ? res['device'] : null;
}
public restoreDevice(device: Device) {
this.sdk.getClient().restore_device(device);
}
public pairDevice(processId: string, spAddressList: string[]): void {
this.sdk.getClient().pair_device(processId, spAddressList);
}
public async unpairDevice(): Promise<void> {
this.sdk.getClient().unpair_device();
const newDevice = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(newDevice);
}
}

View File

@ -1,6 +1,6 @@
import { MessageType } from '../types/index';
import Services from './service';
import TokenService from './token';
import TokenService from './token.service';
import { cleanSubscriptions } from '../utils/subscription.utils';
import { splitPrivateData, isValid32ByteHex } from '../utils/service.utils';
import { MerkleProofResult } from '../../pkg/sdk_client';

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,11 @@
import { AnkFlag } from '../../pkg/sdk_client'; // Vérifie le chemin vers pkg
import Services from './service';
import { APP_CONFIG } from '../config/constants';
let ws: WebSocket | null = null;
let messageQueue: string[] = [];
let reconnectInterval = 1000; // Délai initial de 1s avant reconnexion
const MAX_RECONNECT_INTERVAL = 30000; // Max 30s
let reconnectInterval = APP_CONFIG.TIMEOUTS.RETRY_DELAY;
const MAX_RECONNECT_INTERVAL = APP_CONFIG.TIMEOUTS.WS_RECONNECT_MAX;
let isConnecting = false;
let urlReference: string = '';
let pingIntervalId: any = null;
@ -24,7 +25,7 @@ function connect() {
ws.onopen = async () => {
console.log('[WS] ✅ Connexion établie !');
isConnecting = false;
reconnectInterval = 1000; // Reset du délai
reconnectInterval = APP_CONFIG.TIMEOUTS.RETRY_DELAY; // Reset du délai
// Démarrer le Heartbeat (Ping pour garder la connexion vivante)
startHeartbeat();
@ -95,7 +96,7 @@ function startHeartbeat() {
// Adapter selon ce que ton serveur attend comme Ping, ou envoyer un message vide
// ws.send(JSON.stringify({ flag: 'Ping', content: '' }));
}
}, 30000);
}, APP_CONFIG.TIMEOUTS.WS_HEARTBEAT);
}
function stopHeartbeat() {