refactor(core): découpage du service principal en modules métier et centralisation de la configuration globale
This commit is contained in:
parent
3eeef3fc9a
commit
04996ceaa5
31
src/config/constants.ts
Normal file
31
src/config/constants.ts
Normal 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`,
|
||||
},
|
||||
};
|
||||
|
||||
85
src/services/core/network.service.ts
Normal file
85
src/services/core/network.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
26
src/services/core/sdk.service.ts
Normal file
26
src/services/core/sdk.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
58
src/services/domain/crypto.service.ts
Normal file
58
src/services/domain/crypto.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
100
src/services/domain/process.service.ts
Normal file
100
src/services/domain/process.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
101
src/services/domain/wallet.service.ts
Normal file
101
src/services/domain/wallet.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
@ -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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user