Compare commits

..

No commits in common. "1db9ab8537f04b578d3db3bbe85b2fffa63bd961" and "77dcb8b9ae18c5003231a05d0ba62cd8e2f8c7b8" have entirely different histories.

16 changed files with 951 additions and 1136 deletions

View File

@ -6,7 +6,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build_wasm": "wasm-pack build --out-dir ../ihm_client_dev3/pkg ../sdk_client --target bundler --dev", "build_wasm": "wasm-pack build --out-dir ../ihm_client_dev2/pkg ../sdk_client --target bundler --dev",
"start": "vite --host 0.0.0.0", "start": "vite --host 0.0.0.0",
"build": "tsc && vite build", "build": "tsc && vite build",
"deploy": "sudo cp -r dist/* /var/www/html/", "deploy": "sudo cp -r dist/* /var/www/html/",

202
public/data.worker.js Normal file
View File

@ -0,0 +1,202 @@
// public/data.worker.js
const DB_NAME = "4nk";
const DB_VERSION = 1;
const EMPTY32BYTES = String("").padStart(64, "0");
// ============================================
// SERVICE WORKER LIFECYCLE
// ============================================
self.addEventListener("install", (event) => {
event.waitUntil(self.skipWaiting());
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
// ============================================
// INDEXEDDB DIRECT ACCESS (READ-ONLY)
// ============================================
/**
* Ouvre une connexion à la BDD directement depuis le Service Worker
*/
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
/**
* Récupère un objet spécifique (équivalent à GET_OBJECT)
*/
function getObject(db, storeName, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
/**
* Récupère plusieurs objets d'un coup (équivalent à GET_MULTIPLE_OBJECTS)
* Optimisé pour utiliser une seule transaction.
*/
function getMultipleObjects(db, storeName, keys) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const results = [];
let completed = 0;
if (keys.length === 0) resolve([]);
keys.forEach((key) => {
const request = store.get(key);
request.onsuccess = () => {
if (request.result) results.push(request.result);
completed++;
if (completed === keys.length) resolve(results);
};
request.onerror = () => {
console.warn(`[SW] Erreur lecture clé ${key}`);
completed++;
if (completed === keys.length) resolve(results);
};
});
});
}
// ============================================
// SCAN LOGIC
// ============================================
async function scanMissingData(processesToScan) {
let db;
try {
db = await openDB();
} catch (e) {
console.error("[SW] Impossible d'ouvrir la BDD:", e);
return { toDownload: [], diffsToCreate: [] };
}
// 1. Récupération directe des processus
const myProcesses = await getMultipleObjects(
db,
"processes",
processesToScan
);
let toDownload = new Set();
let diffsToCreate = [];
if (myProcesses && myProcesses.length !== 0) {
for (const process of myProcesses) {
if (!process || !process.states) continue;
const firstState = process.states[0];
if (!firstState) continue;
const processId = firstState.commited_in;
for (const state of process.states) {
if (state.state_id === EMPTY32BYTES) continue;
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
if (
(state.public_data && state.public_data[field] !== undefined) ||
field === "roles"
)
continue;
// 2. Vérification directe dans 'data'
const existingData = await getObject(db, "data", hash);
if (!existingData) {
toDownload.add(hash);
// 3. Vérification directe dans 'diffs'
const existingDiff = await getObject(db, "diffs", hash);
if (!existingDiff) {
diffsToCreate.push({
process_id: processId,
state_id: state.state_id,
value_commitment: hash,
roles: state.roles,
field: field,
description: null,
previous_value: null,
new_value: null,
notify_user: false,
need_validation: false,
validation_status: "None",
});
}
} else {
if (toDownload.has(hash)) {
toDownload.delete(hash);
}
}
}
}
}
}
// On ferme la connexion BDD
db.close();
// ✅ LOG PERTINENT UNIQUEMENT : On n'affiche que si on a trouvé quelque chose
if (toDownload.size > 0 || diffsToCreate.length > 0) {
console.log("[Service Worker] 🔄 Scan found items:", {
toDownload: toDownload.size,
diffsToCreate: diffsToCreate.length,
});
}
return {
toDownload: Array.from(toDownload),
diffsToCreate: diffsToCreate,
};
}
// ============================================
// MESSAGE HANDLER
// ============================================
self.addEventListener("message", async (event) => {
const data = event.data;
if (data.type === "SCAN") {
try {
const myProcessesId = data.payload;
if (myProcessesId && myProcessesId.length !== 0) {
// Appel direct de la nouvelle fonction optimisée
const scanResult = await scanMissingData(myProcessesId);
if (scanResult.toDownload.length !== 0) {
event.source.postMessage({
type: "TO_DOWNLOAD",
data: scanResult.toDownload,
});
}
if (scanResult.diffsToCreate.length > 0) {
event.source.postMessage({
type: "DIFFS_TO_CREATE",
data: scanResult.diffsToCreate,
});
}
}
} catch (error) {
console.error("[Service Worker] Scan error:", error);
// On évite de spammer l'UI avec des erreurs internes du worker
}
}
});

View File

@ -1,243 +0,0 @@
// Service Worker for Network Management
// Handles WebSocket connections to backend relays
/// <reference lib="webworker" />
type AnkFlag = 'Handshake' | 'NewTx' | 'Cipher' | 'Commit' | 'Faucet' | 'Ping';
interface ServiceWorkerMessage {
type: string;
payload?: any;
id?: string;
}
// State management
const sockets: Map<string, WebSocket> = new Map();
const relayAddresses: Map<string, string> = new Map(); // wsUrl -> spAddress
const messageQueue: string[] = [];
const reconnectTimers: Map<string, any> = new Map();
let heartbeatInterval: any = null;
// ==========================================
// SERVICE WORKER LIFECYCLE
// ==========================================
(self as unknown as ServiceWorkerGlobalScope).addEventListener('install', (event: ExtendableEvent) => {
console.log('[NetworkSW] Installing...');
event.waitUntil((self as unknown as ServiceWorkerGlobalScope).skipWaiting());
});
(self as unknown as ServiceWorkerGlobalScope).addEventListener('activate', (event: ExtendableEvent) => {
console.log('[NetworkSW] Activating...');
event.waitUntil((self as unknown as ServiceWorkerGlobalScope).clients.claim());
startHeartbeat();
});
// ==========================================
// MESSAGE HANDLING
// ==========================================
(self as unknown as ServiceWorkerGlobalScope).addEventListener('message', async (event: ExtendableMessageEvent) => {
const { type, payload, id } = event.data;
console.log(`[NetworkSW] Received message: ${type} (id: ${id})`);
// Get the client to respond to
const client = event.source as Client | null;
if (!client) {
console.error('[NetworkSW] No client source available');
return;
}
switch (type) {
case 'CONNECT':
connect(payload.url).then(() => {
respondToClient(client, { type: 'CONNECTED', id, payload: { url: payload.url } });
}).catch((error) => {
respondToClient(client, { type: 'ERROR', id, payload: { error: error.message } });
});
break;
case 'SEND_MESSAGE':
sendMessage(payload.flag, payload.content);
respondToClient(client, { type: 'MESSAGE_SENT', id, payload: {} });
break;
case 'GET_AVAILABLE_RELAY':
const relay = getAvailableRelay();
respondToClient(client, { type: 'AVAILABLE_RELAY', id, payload: { relay } });
break;
case 'GET_ALL_RELAYS':
const relays = getAllRelays();
respondToClient(client, { type: 'ALL_RELAYS', id, payload: { relays } });
break;
default:
console.warn('[NetworkSW] Unknown message type:', type);
}
});
// ==========================================
// WEBSOCKET MANAGEMENT
// ==========================================
async function connect(url: string): Promise<void> {
if (sockets.has(url) && sockets.get(url)?.readyState === WebSocket.OPEN) {
return;
}
console.log(`[NetworkSW] 🔌 Connexion à ${url}...`);
const ws = new WebSocket(url);
ws.onopen = () => {
console.log(`[NetworkSW] ✅ Connecté à ${url}`);
sockets.set(url, ws);
// Reset reconnect timer if exists
if (reconnectTimers.has(url)) {
clearTimeout(reconnectTimers.get(url));
reconnectTimers.delete(url);
}
// Flush message queue
flushQueue();
// Notify all clients
broadcastToClients({ type: 'STATUS_CHANGE', payload: { url, status: 'OPEN' } });
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
// If it's a Handshake, update the local map
if (msg.flag === 'Handshake' && msg.content) {
const handshake = JSON.parse(msg.content);
if (handshake.sp_address) {
relayAddresses.set(url, handshake.sp_address);
broadcastToClients({
type: 'STATUS_CHANGE',
payload: { url, status: 'OPEN', spAddress: handshake.sp_address }
});
}
}
// Forward all messages to clients
broadcastToClients({
type: 'MESSAGE_RECEIVED',
payload: { flag: msg.flag, content: msg.content, url }
});
} catch (e) {
console.error('[NetworkSW] Erreur parsing message:', e);
}
};
ws.onerror = (e) => {
// Silently handle errors (reconnection will be handled by onclose)
};
ws.onclose = () => {
console.warn(`[NetworkSW] ❌ Déconnecté de ${url}.`);
sockets.delete(url);
relayAddresses.set(url, ''); // Reset spAddress
broadcastToClients({ type: 'STATUS_CHANGE', payload: { url, status: 'CLOSED' } });
scheduleReconnect(url);
};
}
function sendMessage(flag: AnkFlag, content: string) {
const msgStr = JSON.stringify({ flag, content });
// Broadcast to all connected relays
let sent = false;
for (const [url, ws] of sockets) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(msgStr);
sent = true;
}
}
if (!sent) {
messageQueue.push(msgStr);
}
}
function getAvailableRelay(): string | null {
for (const sp of relayAddresses.values()) {
if (sp && sp !== '') return sp;
}
return null;
}
function getAllRelays(): Record<string, string> {
return Object.fromEntries(relayAddresses);
}
// ==========================================
// INTERNAL HELPERS
// ==========================================
function flushQueue() {
while (messageQueue.length > 0) {
const msg = messageQueue.shift();
if (!msg) break;
for (const ws of sockets.values()) {
if (ws.readyState === WebSocket.OPEN) ws.send(msg);
}
}
}
function scheduleReconnect(url: string) {
if (reconnectTimers.has(url)) return;
console.log(`[NetworkSW] ⏳ Reconnexion à ${url} dans 3s...`);
const timer = setTimeout(() => {
reconnectTimers.delete(url);
connect(url);
}, 3000);
reconnectTimers.set(url, timer);
}
function startHeartbeat() {
if (heartbeatInterval) clearInterval(heartbeatInterval);
heartbeatInterval = setInterval(() => {
// Heartbeat logic can be added here if needed
// sendMessage('Ping', '');
}, 30000);
}
// ==========================================
// CLIENT COMMUNICATION
// ==========================================
function respondToClient(client: Client | null, message: any) {
if (!client) {
console.error('[NetworkSW] Cannot respond: client is null');
return;
}
try {
console.log(`[NetworkSW] Sending response: ${message.type} (id: ${message.id})`);
client.postMessage(message);
} catch (error) {
console.error('[NetworkSW] Error sending message to client:', error);
// Fallback: try to get the client by ID or use broadcast
if (client.id) {
(self as unknown as ServiceWorkerGlobalScope).clients.get(client.id).then((c: Client | undefined) => {
if (c) c.postMessage(message);
}).catch((err: any) => {
console.error('[NetworkSW] Failed to get client by ID:', err);
});
}
}
}
async function broadcastToClients(message: any) {
const clients = await (self as unknown as ServiceWorkerGlobalScope).clients.matchAll({ includeUncontrolled: true });
clients.forEach((client: Client) => {
client.postMessage(message);
});
}

View File

@ -0,0 +1,111 @@
import * as Comlink from "comlink";
import type { NetworkBackend } from "../../workers/network.worker";
import Services from "../service";
export class NetworkService {
private worker: Comlink.Remote<NetworkBackend>;
private workerInstance: Worker;
// Cache local
private localRelays: Record<string, string> = {};
// Mécanisme d'attente (Events)
private relayReadyResolver: ((addr: string) => void) | null = null;
private relayReadyPromise: Promise<string> | null = null;
constructor(private bootstrapUrls: string[]) {
this.workerInstance = new Worker(
new URL("../../workers/network.worker.ts", import.meta.url),
{ type: "module" }
);
this.worker = Comlink.wrap<NetworkBackend>(this.workerInstance);
}
public async initRelays() {
await this.worker.setCallbacks(
Comlink.proxy(this.onMessageReceived.bind(this)),
Comlink.proxy(this.onStatusChange.bind(this))
);
for (const url of this.bootstrapUrls) {
this.addWebsocketConnection(url);
}
}
public async addWebsocketConnection(url: string) {
await this.worker.connect(url);
}
public async connectAllRelays() {
for (const url of this.bootstrapUrls) {
this.addWebsocketConnection(url);
}
}
public async sendMessage(flag: string, content: string) {
await this.worker.sendMessage(flag as any, content);
}
// Cette méthode est appelée par le Worker (via Services.ts) ou par onStatusChange
public updateRelay(url: string, spAddress: string) {
this.localRelays[url] = spAddress;
// ✨ EVENT TRIGGER : Si quelqu'un attendait un relais, on le débloque !
if (spAddress && spAddress !== "" && this.relayReadyResolver) {
this.relayReadyResolver(spAddress);
this.relayReadyResolver = null;
this.relayReadyPromise = null;
}
}
public getAllRelays() {
return this.localRelays;
}
public async getAvailableRelayAddress(): Promise<string> {
// 1. Vérification immédiate (Fast path)
const existing = Object.values(this.localRelays).find(
(addr) => addr && addr !== ""
);
if (existing) return existing;
// 2. Si pas encore là, on crée une "barrière" (Promise)
if (!this.relayReadyPromise) {
console.log("[NetworkService] ⏳ Attente d'un événement Handshake...");
this.relayReadyPromise = new Promise<string>((resolve, reject) => {
this.relayReadyResolver = resolve;
// Timeout de sécurité (10s) pour ne pas bloquer indéfiniment
setTimeout(() => {
if (this.relayReadyResolver) {
reject(new Error("Timeout: Aucun relais reçu après 10s"));
this.relayReadyResolver = null;
this.relayReadyPromise = null;
}
}, 10000);
});
}
return this.relayReadyPromise;
}
// --- INTERNES ---
private async onMessageReceived(flag: string, content: string, url: string) {
const services = await Services.getInstance();
await services.dispatchToWorker(flag, content, url);
}
private onStatusChange(
url: string,
status: "OPEN" | "CLOSED",
spAddress?: string
) {
if (status === "OPEN" && spAddress) {
// Met à jour et déclenche potentiellement le resolve()
this.updateRelay(url, spAddress);
} else if (status === "CLOSED") {
this.localRelays[url] = "";
}
}
}

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,14 +1,8 @@
import { MerkleProofResult, ProcessState } from '../../pkg/sdk_client'; import { MerkleProofResult, ProcessState } from '../../../pkg/sdk_client';
import { SdkService } from '../core/sdk.service';
// Type for WasmService proxy (passed from Core Worker)
type WasmServiceProxy = {
hashValue(fileBlob: { type: string; data: Uint8Array }, commitedIn: string, label: string): Promise<string>;
getMerkleProof(processState: any, attributeName: string): Promise<any>;
validateMerkleProof(proof: any, hash: string): Promise<boolean>;
};
export class CryptoService { export class CryptoService {
constructor(private wasm: WasmServiceProxy) {} constructor(private sdk: SdkService) {}
public hexToBlob(hexString: string): Blob { public hexToBlob(hexString: string): Blob {
const uint8Array = this.hexToUInt8Array(hexString); const uint8Array = this.hexToUInt8Array(hexString);
@ -32,16 +26,16 @@ export class CryptoService {
.join(''); .join('');
} }
public async getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): Promise<string> { public getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): string {
return await this.wasm.hashValue(fileBlob, commitedIn, label); return this.sdk.getClient().hash_value(fileBlob, commitedIn, label);
} }
public async getMerkleProofForFile(processState: ProcessState, attributeName: string): Promise<MerkleProofResult> { public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult {
return await this.wasm.getMerkleProof(processState, attributeName); return this.sdk.getClient().get_merkle_proof(processState, attributeName);
} }
public async validateMerkleProof(proof: MerkleProofResult, hash: string): Promise<boolean> { public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean {
return await this.wasm.validateMerkleProof(proof, hash); return this.sdk.getClient().validate_merkle_proof(proof, hash);
} }
public splitData(obj: Record<string, any>) { public splitData(obj: Record<string, any>) {

View File

@ -1,12 +1,6 @@
import { Process, ProcessState, RoleDefinition } from '../../pkg/sdk_client'; import { Process, ProcessState, RoleDefinition } from '../../../pkg/sdk_client';
import { SdkService } from '../core/sdk.service';
// Type for Database proxy (passed from Core Worker) import Database from '../database.service';
type DatabaseServiceProxy = {
getProcess(processId: string): Promise<Process | null>;
getAllProcesses(): Promise<Record<string, Process>>;
saveProcess(processId: string, process: Process): Promise<void>;
saveProcessesBatch(processes: Record<string, Process>): Promise<void>;
};
const EMPTY32BYTES = String('').padStart(64, '0'); const EMPTY32BYTES = String('').padStart(64, '0');
@ -14,7 +8,7 @@ export class ProcessService {
private processesCache: Record<string, Process> = {}; private processesCache: Record<string, Process> = {};
private myProcesses: Set<string> = new Set(); private myProcesses: Set<string> = new Set();
constructor(private db: any) {} constructor(private sdk: SdkService, private db: Database) {}
public async getProcess(processId: string): Promise<Process | null> { public async getProcess(processId: string): Promise<Process | null> {
if (this.processesCache[processId]) return this.processesCache[processId]; if (this.processesCache[processId]) return this.processesCache[processId];
@ -45,17 +39,17 @@ export class ProcessService {
public getLastCommitedState(process: Process): ProcessState | null { public getLastCommitedState(process: Process): ProcessState | null {
if (process.states.length === 0) return null; if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in; const processTip = process.states[process.states.length - 1].commited_in;
return process.states.findLast((state: ProcessState) => state.commited_in !== processTip) || null; return process.states.findLast((state) => state.commited_in !== processTip) || null;
} }
public getUncommitedStates(process: Process): ProcessState[] { public getUncommitedStates(process: Process): ProcessState[] {
if (process.states.length === 0) return []; if (process.states.length === 0) return [];
const processTip = process.states[process.states.length - 1].commited_in; const processTip = process.states[process.states.length - 1].commited_in;
return process.states.filter((state: ProcessState) => state.commited_in === processTip).filter((state: ProcessState) => state.state_id !== EMPTY32BYTES); return process.states.filter((state) => state.commited_in === processTip).filter((state) => state.state_id !== EMPTY32BYTES);
} }
public getStateFromId(process: Process, stateId: string): ProcessState | null { public getStateFromId(process: Process, stateId: string): ProcessState | null {
return process.states.find((state: ProcessState) => state.state_id === stateId) || null; return process.states.find((state) => state.state_id === stateId) || null;
} }
public getRoles(process: Process): Record<string, RoleDefinition> | null { public getRoles(process: Process): Record<string, RoleDefinition> | null {

View File

@ -0,0 +1,93 @@
import { Device } from '../../../pkg/sdk_client';
import { SdkService } from '../core/sdk.service';
import Database from '../database.service';
export class WalletService {
constructor(private sdk: SdkService, private db: Database) {}
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;
}
}
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> {
await this.db.saveDevice(device);
}
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,316 +0,0 @@
import Services from "./service";
interface ServiceWorkerMessage {
type: string;
payload?: any;
id?: string;
}
export class NetworkService {
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private messageIdCounter: number = 0;
private pendingMessages: Map<string, { resolve: (value: any) => void; reject: (error: any) => void }> = new Map();
// Relay ready promise mechanism (waits for first relay to become available)
private relayReadyResolver: ((addr: string) => void) | null = null;
private relayReadyPromise: Promise<string> | null = null;
constructor(private bootstrapUrls: string[]) {
this.setupMessageListener();
}
public async initRelays() {
try {
// Register Service Worker
console.log("[NetworkService] Registering Service Worker...");
await this.registerServiceWorker();
// Wait for Service Worker to be ready
console.log("[NetworkService] Waiting for Service Worker to be ready...");
await this.waitForServiceWorkerReady();
console.log("[NetworkService] Service Worker is ready");
// Connect to bootstrap URLs
console.log("[NetworkService] Connecting to bootstrap URLs...");
for (const url of this.bootstrapUrls) {
await this.addWebsocketConnection(url);
}
console.log("[NetworkService] Initialization complete");
} catch (error) {
console.error("[NetworkService] Initialization failed:", error);
throw error;
}
}
public async addWebsocketConnection(url: string) {
await this.sendToServiceWorker({ type: 'CONNECT', payload: { url } });
}
public async connectAllRelays() {
for (const url of this.bootstrapUrls) {
this.addWebsocketConnection(url);
}
}
public async sendMessage(flag: string, content: string) {
await this.sendToServiceWorker({
type: 'SEND_MESSAGE',
payload: { flag, content }
});
}
// Called by onStatusChange when a relay becomes available
// Triggers the relay ready promise if someone is waiting
public updateRelay(url: string, spAddress: string) {
// Trigger relay ready promise if someone is waiting
if (spAddress && spAddress !== "" && this.relayReadyResolver) {
this.relayReadyResolver(spAddress);
this.relayReadyResolver = null;
this.relayReadyPromise = null;
}
}
public async getAllRelays(): Promise<Record<string, string>> {
const response = await this.sendToServiceWorker({ type: 'GET_ALL_RELAYS' });
return response?.relays || {};
}
public async getAvailableRelayAddress(): Promise<string> {
// 1. Query Service Worker first (fast path if relay already available)
const response = await this.sendToServiceWorker({ type: 'GET_AVAILABLE_RELAY' });
if (response?.relay) return response.relay;
// 2. If no relay yet, wait for one to become available
if (!this.relayReadyPromise) {
console.log("[NetworkService] ⏳ Waiting for relay Handshake...");
this.relayReadyPromise = new Promise<string>((resolve, reject) => {
this.relayReadyResolver = resolve;
// Timeout after 10s to avoid blocking indefinitely
setTimeout(() => {
if (this.relayReadyResolver) {
reject(new Error("Timeout: No relay received after 10s"));
this.relayReadyResolver = null;
this.relayReadyPromise = null;
}
}, 10000);
});
}
return this.relayReadyPromise;
}
// --- INTERNES ---
private async registerServiceWorker(): Promise<void> {
if (!("serviceWorker" in navigator)) {
throw new Error("Service Workers are not supported");
}
try {
// Check if already registered
const registrations = await navigator.serviceWorker.getRegistrations();
const existing = registrations.find((r) => {
const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL;
return url && url.includes("network.sw.js");
});
if (existing) {
console.log("[NetworkService] Found existing Service Worker registration");
this.serviceWorkerRegistration = existing;
// Listen for controller change in case it activates
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log("[NetworkService] Service Worker controller changed");
});
// Try to update
try {
await existing.update();
} catch (updateError) {
console.warn("[NetworkService] Service Worker update failed:", updateError);
}
} else {
// Register new Service Worker
console.log("[NetworkService] Registering new Service Worker at /network.sw.js");
this.serviceWorkerRegistration = await navigator.serviceWorker.register(
"/network.sw.js",
{ type: "module", scope: "/" }
);
console.log("[NetworkService] Service Worker registered:", this.serviceWorkerRegistration);
}
// Listen for registration errors
this.serviceWorkerRegistration.addEventListener('error', (event) => {
console.error("[NetworkService] Service Worker error:", event);
});
} catch (error) {
console.error("[NetworkService] Failed to register Service Worker:", error);
// Check if it's a 404 error
if (error instanceof Error && error.message.includes('404')) {
throw new Error("Service Worker file not found at /network.sw.js. Check Vite configuration.");
}
throw error;
}
}
private async waitForServiceWorkerReady(): Promise<void> {
if (!this.serviceWorkerRegistration) {
throw new Error("Service Worker registration is null");
}
// Wait for the Service Worker to be ready (installed and activated)
if (this.serviceWorkerRegistration.installing) {
const installing = this.serviceWorkerRegistration.installing;
await new Promise<void>((resolve) => {
installing.addEventListener('statechange', () => {
if (installing.state === 'installed') {
resolve();
}
});
});
}
// Also ensure it's active
if (this.serviceWorkerRegistration.active) {
// Wait a bit for it to become the controller
await new Promise(resolve => setTimeout(resolve, 100));
return;
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Service Worker activation timeout after 10 seconds"));
}, 10000);
const checkState = () => {
if (this.serviceWorkerRegistration?.active) {
clearTimeout(timeout);
// Wait a bit for it to become the controller
setTimeout(resolve, 100);
} else if (this.serviceWorkerRegistration?.installing) {
// Service Worker is installing, wait for it
this.serviceWorkerRegistration.installing.addEventListener('statechange', () => {
if (this.serviceWorkerRegistration?.active) {
clearTimeout(timeout);
setTimeout(resolve, 100);
}
});
} else if (this.serviceWorkerRegistration?.waiting) {
// Service Worker is waiting, skip waiting
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
setTimeout(checkState, 100);
} else {
setTimeout(checkState, 100);
}
};
checkState();
});
}
private setupMessageListener(): void {
if (!navigator.serviceWorker) {
console.warn("[NetworkService] Service Workers not supported, message listener not set up");
return;
}
const messageHandler = (event: MessageEvent<ServiceWorkerMessage>) => {
const { type, payload, id } = event.data;
console.log(`[NetworkService] Received message from SW: ${type} (id: ${id})`);
// Handle response messages
if (id && this.pendingMessages.has(id)) {
const { resolve, reject } = this.pendingMessages.get(id)!;
this.pendingMessages.delete(id);
if (type === "ERROR") {
reject(new Error(payload?.error || "Unknown error"));
} else {
resolve(payload);
}
return;
}
// Handle event messages (not responses to requests)
switch (type) {
case "MESSAGE_RECEIVED":
this.onMessageReceived(payload.flag, payload.content, payload.url);
break;
case "STATUS_CHANGE":
this.onStatusChange(payload.url, payload.status, payload.spAddress);
break;
}
};
// Listen on both the serviceWorker and controller
navigator.serviceWorker.addEventListener("message", messageHandler as EventListener);
// Also listen on the controller if it exists
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.addEventListener("message", messageHandler as EventListener);
}
// Listen for controller changes
navigator.serviceWorker.addEventListener("controllerchange", () => {
console.log("[NetworkService] Service Worker controller changed");
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.addEventListener("message", messageHandler as EventListener);
}
});
}
private async sendToServiceWorker(message: ServiceWorkerMessage): Promise<any> {
if (!this.serviceWorkerRegistration) {
throw new Error("Service Worker is not registered");
}
// Use the controller if available, otherwise use active
const target = navigator.serviceWorker.controller || this.serviceWorkerRegistration.active;
if (!target) {
throw new Error(`Service Worker is not active. State: ${this.serviceWorkerRegistration.installing ? 'installing' : this.serviceWorkerRegistration.waiting ? 'waiting' : 'unknown'}`);
}
const id = `msg_${++this.messageIdCounter}`;
message.id = id;
return new Promise((resolve, reject) => {
this.pendingMessages.set(id, { resolve, reject });
// Timeout after 10 seconds (reduced from 30)
const timeout = setTimeout(() => {
if (this.pendingMessages.has(id)) {
this.pendingMessages.delete(id);
reject(new Error(`Service Worker message timeout after 10s. Message type: ${message.type}`));
}
}, 10000);
try {
target.postMessage(message);
console.log(`[NetworkService] Sent message to SW via ${target === navigator.serviceWorker.controller ? 'controller' : 'active'}: ${message.type} (id: ${id})`);
} catch (error) {
clearTimeout(timeout);
this.pendingMessages.delete(id);
reject(new Error(`Failed to send message to Service Worker: ${error instanceof Error ? error.message : String(error)}`));
}
});
}
private async onMessageReceived(flag: string, content: string, url: string) {
const services = await Services.getInstance();
await services.dispatchToWorker(flag, content, url);
}
private onStatusChange(
url: string,
status: "OPEN" | "CLOSED",
spAddress?: string
) {
if (status === "OPEN" && spAddress) {
// Trigger relay ready promise if someone is waiting
this.updateRelay(url, spAddress);
}
// Note: Service worker is the source of truth for relay state
// We don't need to track CLOSED here since we query the SW directly
}
}

View File

@ -11,10 +11,10 @@ import {
} from "../../pkg/sdk_client"; } from "../../pkg/sdk_client";
import { BackUp } from "../types/index"; import { BackUp } from "../types/index";
import { APP_CONFIG } from "../config/constants"; import { APP_CONFIG } from "../config/constants";
import { NetworkService } from "./network.service"; import { NetworkService } from "./core/network.service";
import type { CoreBackend } from "../workers/core.worker"; import type { CoreBackend } from "../workers/core.worker";
import { SWController } from "./sw-controller.service";
import Database from "./database.service"; import Database from "./database.service";
import WasmService from "./wasm.service";
export default class Services { export default class Services {
private static instance: Services; private static instance: Services;
@ -54,19 +54,16 @@ export default class Services {
public async init(): Promise<void> { public async init(): Promise<void> {
console.log("[Services] 🚀 Démarrage Proxy..."); console.log("[Services] 🚀 Démarrage Proxy...");
// 1. Initialiser les Services dans le Main Thread (Hub) // 1. Initialiser le Core Worker
const db = await Database.getInstance();
const wasmService = await WasmService.getInstance();
// 2. Passer les Services au Core Worker via Comlink proxy (BEFORE init)
await this.coreWorker.setServices(
Comlink.proxy(wasmService),
Comlink.proxy(db)
);
// 3. Initialiser le Core Worker (after services are set)
await this.coreWorker.init(); await this.coreWorker.init();
// 2. Initialiser la Database (Main Thread)
await Database.getInstance();
// 3. Initialiser le Service Worker Controller
const swController = await SWController.getInstance();
await swController.init();
// 4. Configurer les Callbacks // 4. Configurer les Callbacks
await this.coreWorker.setCallbacks( await this.coreWorker.setCallbacks(
Comlink.proxy(this.handleWorkerNotification.bind(this)), Comlink.proxy(this.handleWorkerNotification.bind(this)),
@ -79,7 +76,7 @@ export default class Services {
await this.networkService.initRelays(); await this.networkService.initRelays();
console.log( console.log(
"[Services] ✅ Proxy connecté au CoreWorker, WASM Service, Database Service et NetworkService." "[Services] ✅ Proxy connecté au CoreWorker, SWController et NetworkService."
); );
} }
@ -158,15 +155,14 @@ export default class Services {
public async addWebsocketConnection(url: string) { public async addWebsocketConnection(url: string) {
await this.networkService.addWebsocketConnection(url); await this.networkService.addWebsocketConnection(url);
} }
public async getAllRelays() { public getAllRelays() {
return await this.networkService.getAllRelays(); return this.networkService.getAllRelays();
} }
public updateRelay(url: string, sp: string) { public updateRelay(url: string, sp: string) {
this.networkService.updateRelay(url, sp); this.networkService.updateRelay(url, sp);
} }
public async getSpAddress(url: string) { public getSpAddress(url: string) {
const relays = await this.networkService.getAllRelays(); return this.networkService.getAllRelays()[url];
return relays[url];
} }
// ========================================== // ==========================================
@ -184,6 +180,9 @@ export default class Services {
public async dumpDeviceFromMemory() { public async dumpDeviceFromMemory() {
return await this.coreWorker.dumpDeviceFromMemory(); return await this.coreWorker.dumpDeviceFromMemory();
} }
public async dumpNeuteredDevice() {
return await this.coreWorker.dumpNeuteredDevice();
}
public async getPairingProcessId() { public async getPairingProcessId() {
return await this.coreWorker.getPairingProcessId(); return await this.coreWorker.getPairingProcessId();
} }
@ -344,6 +343,9 @@ export default class Services {
public async resetDevice() { public async resetDevice() {
await this.coreWorker.resetDevice(); await this.coreWorker.resetDevice();
} }
public async handleApiReturn(res: ApiReturn) {
await this.coreWorker.handleApiReturn(res);
}
public async saveDiffsToDb(diffs: UserDiff[]) { public async saveDiffsToDb(diffs: UserDiff[]) {
await this.coreWorker.saveDiffsToDb(diffs); await this.coreWorker.saveDiffsToDb(diffs);
} }
@ -351,10 +353,6 @@ export default class Services {
await this.coreWorker.handleCommitError(res); await this.coreWorker.handleCommitError(res);
} }
public async handleApiReturn(res: ApiReturn) {
await this.coreWorker.handleApiReturn(res);
}
public async rolesContainsUs(roles: any) { public async rolesContainsUs(roles: any) {
return await this.coreWorker.rolesContainsUs(roles); return await this.coreWorker.rolesContainsUs(roles);
} }

View File

@ -0,0 +1,194 @@
import Services from "./service";
import Database from "./database.service";
/**
* Service Worker Controller - Manages SW registration and communication
*/
export class SWController {
private static instance: SWController;
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private serviceWorkerCheckIntervalId: number | null = null;
private constructor() {
// Singleton
}
public static async getInstance(): Promise<SWController> {
if (!SWController.instance) {
SWController.instance = new SWController();
}
return SWController.instance;
}
public async init(): Promise<void> {
await this.registerServiceWorker("/data.worker.js");
}
private async registerServiceWorker(path: string): Promise<void> {
if (!("serviceWorker" in navigator)) return;
console.log("[SWController] Initializing Service Worker:", path);
try {
// Nettoyage des anciens workers si nécessaire (logique conservée)
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
const scriptURL =
registration.active?.scriptURL ||
registration.installing?.scriptURL ||
registration.waiting?.scriptURL;
const scope = registration.scope;
if (
scope.includes("/src/service-workers/") ||
(scriptURL && scriptURL.includes("/src/service-workers/"))
) {
console.warn(`[SWController] Removing old Service Worker (${scope})`);
await registration.unregister();
}
}
const existingValidWorker = registrations.find((r) => {
const url =
r.active?.scriptURL ||
r.installing?.scriptURL ||
r.waiting?.scriptURL;
return url && url.endsWith(path.replace(/^\//, ""));
});
if (!existingValidWorker) {
console.log("[SWController] Registering new Service Worker");
this.serviceWorkerRegistration = await navigator.serviceWorker.register(
path,
{ type: "module", scope: "/" }
);
} else {
console.log("[SWController] Service Worker already active");
this.serviceWorkerRegistration = existingValidWorker;
await this.serviceWorkerRegistration.update();
}
navigator.serviceWorker.addEventListener("message", async (event) => {
await this.handleServiceWorkerMessage(event.data);
});
// Boucle de scan périodique
if (this.serviceWorkerCheckIntervalId)
clearInterval(this.serviceWorkerCheckIntervalId);
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
const activeWorker =
this.serviceWorkerRegistration?.active ||
(await this.waitForServiceWorkerActivation(
this.serviceWorkerRegistration!
));
// On récupère les processus via le proxy Services
const service = await Services.getInstance();
const payload = await service.getMyProcesses();
if (payload && Object.keys(payload).length !== 0) {
activeWorker?.postMessage({ type: "SCAN", payload });
}
}, 5000);
} catch (error) {
console.error("[SWController] Service Worker error:", error);
}
}
private async waitForServiceWorkerActivation(
registration: ServiceWorkerRegistration
): Promise<ServiceWorker | null> {
return new Promise((resolve) => {
if (registration.active) {
resolve(registration.active);
} else {
const listener = () => {
if (registration.active) {
navigator.serviceWorker.removeEventListener(
"controllerchange",
listener
);
resolve(registration.active);
}
};
navigator.serviceWorker.addEventListener("controllerchange", listener);
}
});
}
// ============================================
// MESSAGE HANDLERS
// ============================================
private async handleServiceWorkerMessage(message: any) {
switch (message.type) {
case "TO_DOWNLOAD":
await this.handleDownloadList(message.data);
break;
case "DIFFS_TO_CREATE":
await this.handleDiffsToCreate(message.data);
break;
default:
console.warn("[SWController] Unknown message type received:", message);
}
}
private async handleDiffsToCreate(diffs: any[]): Promise<void> {
console.log(
`[SWController] Creating ${diffs.length} diffs from Service Worker scan`
);
try {
const db = await Database.getInstance();
await db.saveDiffs(diffs);
console.log("[SWController] Diffs created successfully");
} catch (error) {
console.error("[SWController] Error creating diffs:", error);
}
}
private async handleDownloadList(downloadList: string[]): Promise<void> {
let requestedStateId: string[] = [];
// On a besoin de Services pour la logique métier (fetch, network)
const service = await Services.getInstance();
for (const hash of downloadList) {
const diff = await service.getDiffByValue(hash);
if (!diff) {
console.warn(`[SWController] Missing a diff for hash ${hash}`);
continue;
}
const processId = diff.process_id;
const stateId = diff.state_id;
const roles = diff.roles;
try {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
const blob = new Blob([valueBytes], {
type: "application/octet-stream",
});
await service.saveBlobToDb(hash, blob);
document.dispatchEvent(
new CustomEvent("newDataReceived", {
detail: { processId, stateId, hash },
})
);
} else {
console.log(
"[SWController] Request data from managers of the process"
);
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
}
}
} catch (e) {
console.error(e);
}
}
}
}

View File

@ -1,134 +0,0 @@
import { Device } from '../../pkg/sdk_client';
import Database from './database.service';
// Type for WasmService proxy (passed from Core Worker)
type WasmServiceProxy = {
isPaired(): Promise<boolean>;
getAvailableAmount(): Promise<BigInt>;
getAddress(): Promise<string>;
getPairingProcessId(): Promise<string>;
createNewDevice(birthday: number, network: string): Promise<string>;
dumpDevice(): Promise<any>;
dumpNeuteredDevice(): Promise<any>;
dumpWallet(): Promise<any>;
restoreDevice(device: any): Promise<void>;
pairDevice(processId: string, spAddresses: string[]): Promise<void>;
unpairDevice(): Promise<void>;
};
type DatabaseServiceProxy = {
getStoreList(): Promise<{ [key: string]: string }>;
addObject(payload: { storeName: string; object: any; key: any }): Promise<void>;
batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void>;
getObject(storeName: string, key: string): Promise<any | null>;
dumpStore(storeName: string): Promise<Record<string, any>>;
deleteObject(storeName: string, key: string): Promise<void>;
clearStore(storeName: string): Promise<void>;
requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]>;
clearMultipleStores(storeNames: string[]): Promise<void>;
saveDevice(device: any): Promise<void>;
getDevice(): Promise<any | null>;
saveProcess(processId: string, process: any): Promise<void>;
saveProcessesBatch(processes: Record<string, any>): Promise<void>;
getProcess(processId: string): Promise<any | null>;
getAllProcesses(): Promise<Record<string, any>>;
saveBlob(hash: string, data: Blob): Promise<void>;
getBlob(hash: string): Promise<Blob | null>;
saveDiffs(diffs: any[]): Promise<void>;
getDiff(hash: string): Promise<any | null>;
getAllDiffs(): Promise<Record<string, any>>;
getSharedSecret(address: string): Promise<string | null>;
saveSecretsBatch(unconfirmedSecrets: any[], sharedSecrets: { key: string; value: any }[]): Promise<void>;
getAllSecrets(): Promise<{ shared_secrets: Record<string, any>; unconfirmed_secrets: any[] }>;
};
export class WalletService {
constructor(private wasm: WasmServiceProxy, private db: DatabaseServiceProxy) {}
public async isPaired(): Promise<boolean> {
try {
return await this.wasm.isPaired();
} catch (e) {
return false;
}
}
public async getAmount(): Promise<BigInt> {
return await this.wasm.getAvailableAmount();
}
public async getDeviceAddress(): Promise<string> {
return await this.wasm.getAddress();
}
public async getPairingProcessId(): Promise<string> {
return await this.wasm.getPairingProcessId();
}
public async createNewDevice(chainTip: number): Promise<string> {
const spAddress = await this.wasm.createNewDevice(0, 'signet');
const device = await this.dumpDeviceFromMemory();
if (device.sp_wallet.birthday === 0) {
device.sp_wallet.birthday = chainTip;
device.sp_wallet.last_scan = chainTip;
await this.wasm.restoreDevice(device);
}
await this.saveDeviceInDatabase(device);
return spAddress;
}
public async dumpDeviceFromMemory(): Promise<Device> {
return await this.wasm.dumpDevice();
}
public async dumpNeuteredDevice(): Promise<Device | null> {
try {
return await this.wasm.dumpNeuteredDevice();
} catch (e) {
return null;
}
}
public async dumpWallet(): Promise<any> {
return await this.wasm.dumpWallet();
}
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> {
await this.db.saveDevice(device);
}
public async getDeviceFromDatabase(): Promise<Device | null> {
const db = await Database.getInstance();
const res = await db.getObject('wallet', '1');
return res ? res['device'] : null;
}
public async restoreDevice(device: Device): Promise<void> {
await this.wasm.restoreDevice(device);
}
public async pairDevice(processId: string, spAddressList: string[]): Promise<void> {
await this.wasm.pairDevice(processId, spAddressList);
}
public async unpairDevice(): Promise<void> {
await this.wasm.unpairDevice();
const newDevice = await this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(newDevice);
}
}

View File

@ -19,93 +19,18 @@ import { APP_CONFIG } from "../config/constants";
import { splitPrivateData } from "../utils/service.utils"; import { splitPrivateData } from "../utils/service.utils";
// Services internes au worker // Services internes au worker
import { WalletService } from "../services/wallet.service"; import { SdkService } from "../services/core/sdk.service";
import { ProcessService } from "../services/process.service"; import { WalletService } from "../services/domain/wallet.service";
import { CryptoService } from "../services/crypto.service"; import { ProcessService } from "../services/domain/process.service";
import { CryptoService } from "../services/domain/crypto.service";
// Types for services passed from main thread
type WasmServiceProxy = {
callMethod(method: string, ...args: any[]): Promise<any>;
parseCipher(msg: string, membersList: any, processes: any): Promise<any>;
parseNewTx(msg: string, blockHeight: number, membersList: any): Promise<any>;
createTransaction(addresses: string[], feeRate: number): Promise<any>;
signTransaction(partialTx: any): Promise<any>;
getTxid(transaction: string): Promise<string>;
getOpReturn(transaction: string): Promise<string>;
getPrevouts(transaction: string): Promise<string[]>;
createProcess(privateData: any, publicData: any, roles: any, membersList: any): Promise<any>;
createNewProcess(privateData: any, roles: any, publicData: any, relayAddress: string, feeRate: number, membersList: any): Promise<any>;
updateProcess(process: any, privateData: any, publicData: any, roles: any, membersList: any): Promise<any>;
createUpdateMessage(process: any, stateId: string, membersList: any): Promise<any>;
createPrdResponse(process: any, stateId: string, membersList: any): Promise<any>;
validateState(process: any, stateId: string, membersList: any): Promise<any>;
refuseState(process: any, stateId: string): Promise<any>;
requestData(processId: string, stateIds: string[], roles: any, membersList: any): Promise<any>;
processCommitNewState(process: any, newStateId: string, newTip: string): Promise<void>;
encodeJson(data: any): Promise<any>;
encodeBinary(data: any): Promise<any>;
decodeValue(value: number[]): Promise<any>;
createFaucetMessage(): Promise<string>;
isChildRole(parent: any, child: any): Promise<boolean>;
// Wallet methods
isPaired(): Promise<boolean>;
getAvailableAmount(): Promise<BigInt>;
getAddress(): Promise<string>;
getPairingProcessId(): Promise<string>;
createNewDevice(birthday: number, network: string): Promise<string>;
dumpDevice(): Promise<any>;
dumpNeuteredDevice(): Promise<any>;
dumpWallet(): Promise<any>;
restoreDevice(device: any): Promise<void>;
pairDevice(processId: string, spAddresses: string[]): Promise<void>;
unpairDevice(): Promise<void>;
resetDevice(): Promise<void>;
// Crypto methods
hashValue(fileBlob: { type: string; data: Uint8Array }, commitedIn: string, label: string): Promise<string>;
getMerkleProof(processState: any, attributeName: string): Promise<any>;
validateMerkleProof(proof: any, hash: string): Promise<boolean>;
decryptData(key: Uint8Array, data: Uint8Array): Promise<Uint8Array>;
// Secrets management
setSharedSecrets(secretsJson: string): Promise<void>;
// Blockchain scanning
scanBlocks(tipHeight: number, blindbitUrl: string): Promise<void>;
};
type DatabaseServiceProxy = {
getStoreList(): Promise<{ [key: string]: string }>;
addObject(payload: { storeName: string; object: any; key: any }): Promise<void>;
batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void>;
getObject(storeName: string, key: string): Promise<any | null>;
dumpStore(storeName: string): Promise<Record<string, any>>;
deleteObject(storeName: string, key: string): Promise<void>;
clearStore(storeName: string): Promise<void>;
requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]>;
clearMultipleStores(storeNames: string[]): Promise<void>;
saveDevice(device: any): Promise<void>;
getDevice(): Promise<any | null>;
saveProcess(processId: string, process: any): Promise<void>;
saveProcessesBatch(processes: Record<string, any>): Promise<void>;
getProcess(processId: string): Promise<any | null>;
getAllProcesses(): Promise<Record<string, any>>;
saveBlob(hash: string, data: Blob): Promise<void>;
getBlob(hash: string): Promise<Blob | null>;
saveDiffs(diffs: any[]): Promise<void>;
getDiff(hash: string): Promise<any | null>;
getAllDiffs(): Promise<Record<string, any>>;
getSharedSecret(address: string): Promise<string | null>;
saveSecretsBatch(unconfirmedSecrets: any[], sharedSecrets: { key: string; value: any }[]): Promise<void>;
getAllSecrets(): Promise<{ shared_secrets: Record<string, any>; unconfirmed_secrets: any[] }>;
};
export class CoreBackend { export class CoreBackend {
// Services (passed from main thread via Comlink) // Services
private wasmService!: WasmServiceProxy; private sdkService: SdkService;
private db!: DatabaseServiceProxy;
// Domain services
private walletService!: WalletService; private walletService!: WalletService;
private processService!: ProcessService; private processService!: ProcessService;
private cryptoService!: CryptoService; private cryptoService: CryptoService;
private db!: Database;
// État (State) // État (State)
private processId: string | null = null; private processId: string | null = null;
@ -129,39 +54,28 @@ export class CoreBackend {
private relayGetter: (() => Promise<string>) | null = null; private relayGetter: (() => Promise<string>) | null = null;
constructor() { constructor() {
// Services will be set via setServices() from main thread this.sdkService = new SdkService();
this.cryptoService = new CryptoService(this.sdkService);
// Initialisation temporaire
this.walletService = new WalletService(this.sdkService, null as any);
this.processService = new ProcessService(this.sdkService, null as any);
} }
public async init(): Promise<void> { public async init(): Promise<void> {
if (this.isInitialized) return; if (this.isInitialized) return;
console.log("[CoreWorker] ⚙️ Initialisation du Backend..."); console.log("[CoreWorker] ⚙️ Initialisation du Backend...");
await this.sdkService.init(); // Charge le WASM
this.db = await Database.getInstance(); // Lance le Database Worker
// Services must be set before init this.walletService = new WalletService(this.sdkService, this.db);
if (!this.wasmService || !this.db) { this.processService = new ProcessService(this.sdkService, this.db);
throw new Error("Services must be set via setServices() before init()");
}
// Initialize domain services with WASM and Database proxies
this.cryptoService = new CryptoService(this.wasmService);
this.walletService = new WalletService(this.wasmService, this.db);
// @ts-ignore - ProcessService accepts DatabaseServiceProxy via Comlink but TypeScript sees Database type
this.processService = new ProcessService(this.db);
this.notifications = this.getNotifications(); this.notifications = this.getNotifications();
this.isInitialized = true; this.isInitialized = true;
console.log("[CoreWorker] ✅ Backend prêt."); console.log("[CoreWorker] ✅ Backend prêt.");
} }
// --- CONFIGURATION DES SERVICES (depuis Main Thread) ---
public setServices(
wasmService: WasmServiceProxy,
db: DatabaseServiceProxy
) {
this.wasmService = wasmService;
this.db = db;
}
// --- CONFIGURATION DES CALLBACKS --- // --- CONFIGURATION DES CALLBACKS ---
public setCallbacks( public setCallbacks(
notifier: (event: string, data?: any) => void, notifier: (event: string, data?: any) => void,
@ -206,32 +120,32 @@ export class CoreBackend {
// ========================================== // ==========================================
// WALLET PROXY // WALLET PROXY
// ========================================== // ==========================================
public async isPaired() { public isPaired() {
return await this.walletService.isPaired(); return this.walletService.isPaired();
} }
public async getAmount() { public getAmount() {
return await this.walletService.getAmount(); return this.walletService.getAmount();
} }
public async getDeviceAddress() { public getDeviceAddress() {
return await this.walletService.getDeviceAddress(); return this.walletService.getDeviceAddress();
} }
public async dumpDeviceFromMemory() { public dumpDeviceFromMemory() {
return await this.walletService.dumpDeviceFromMemory(); return this.walletService.dumpDeviceFromMemory();
} }
public async dumpNeuteredDevice() { public dumpNeuteredDevice() {
return await this.walletService.dumpNeuteredDevice(); return this.walletService.dumpNeuteredDevice();
} }
public async getPairingProcessId() { public getPairingProcessId() {
return await this.walletService.getPairingProcessId(); return this.walletService.getPairingProcessId();
} }
public async getDeviceFromDatabase() { public async getDeviceFromDatabase() {
return this.walletService.getDeviceFromDatabase(); return this.walletService.getDeviceFromDatabase();
} }
public async restoreDevice(d: Device) { public restoreDevice(d: Device) {
await this.walletService.restoreDevice(d); this.walletService.restoreDevice(d);
} }
public async pairDevice(pid: string, list: string[]) { public pairDevice(pid: string, list: string[]) {
await this.walletService.pairDevice(pid, list); this.walletService.pairDevice(pid, list);
} }
public async unpairDevice() { public async unpairDevice() {
await this.walletService.unpairDevice(); await this.walletService.unpairDevice();
@ -285,8 +199,8 @@ export class CoreBackend {
// ========================================== // ==========================================
// CRYPTO HELPERS // CRYPTO HELPERS
// ========================================== // ==========================================
public async decodeValue(val: number[]) { public decodeValue(val: number[]) {
return await this.wasmService.decodeValue(val); return this.sdkService.decodeValue(val);
} }
public hexToBlob(hex: string) { public hexToBlob(hex: string) {
return this.cryptoService.hexToBlob(hex); return this.cryptoService.hexToBlob(hex);
@ -297,14 +211,14 @@ export class CoreBackend {
public async blobToHex(blob: Blob) { public async blobToHex(blob: Blob) {
return this.cryptoService.blobToHex(blob); return this.cryptoService.blobToHex(blob);
} }
public async getHashForFile(c: string, l: string, f: any) { public getHashForFile(c: string, l: string, f: any) {
return await this.cryptoService.getHashForFile(c, l, f); return this.cryptoService.getHashForFile(c, l, f);
} }
public async getMerkleProofForFile(s: ProcessState, a: string) { public getMerkleProofForFile(s: ProcessState, a: string) {
return await this.cryptoService.getMerkleProofForFile(s, a); return this.cryptoService.getMerkleProofForFile(s, a);
} }
public async validateMerkleProof(p: MerkleProofResult, h: string) { public validateMerkleProof(p: MerkleProofResult, h: string) {
return await this.cryptoService.validateMerkleProof(p, h); return this.cryptoService.validateMerkleProof(p, h);
} }
private splitData(obj: Record<string, any>) { private splitData(obj: Record<string, any>) {
return this.cryptoService.splitData(obj); return this.cryptoService.splitData(obj);
@ -343,13 +257,16 @@ export class CoreBackend {
// ========================================== // ==========================================
// UTILITAIRES DIVERS // UTILITAIRES DIVERS
// ========================================== // ==========================================
public async createFaucetMessage() { public createFaucetMessage() {
return await this.wasmService.createFaucetMessage(); return this.sdkService.getClient().create_faucet_msg();
} }
public async isChildRole(parent: any, child: any): Promise<boolean> { public isChildRole(parent: any, child: any): boolean {
try { try {
return await this.wasmService.isChildRole(parent, child); this.sdkService
.getClient()
.is_child_role(JSON.stringify(parent), JSON.stringify(child));
return true;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return false; return false;
@ -368,7 +285,7 @@ export class CoreBackend {
} }
this.currentBlockHeight = handshakeMsg.chain_tip; this.currentBlockHeight = handshakeMsg.chain_tip;
if (!(await this.isPaired())) { if (!this.isPaired()) {
console.log(`[CoreWorker] ⏳ Non pairé. Attente appairage...`); console.log(`[CoreWorker] ⏳ Non pairé. Attente appairage...`);
} }
@ -398,14 +315,16 @@ export class CoreBackend {
device.sp_wallet.birthday = this.currentBlockHeight; device.sp_wallet.birthday = this.currentBlockHeight;
device.sp_wallet.last_scan = this.currentBlockHeight; device.sp_wallet.last_scan = this.currentBlockHeight;
await this.walletService.saveDeviceInDatabase(device); await this.walletService.saveDeviceInDatabase(device);
await this.walletService.restoreDevice(device); this.walletService.restoreDevice(device);
} else if (device.sp_wallet.last_scan < this.currentBlockHeight) { } else if (device.sp_wallet.last_scan < this.currentBlockHeight) {
console.log( console.log(
`[CoreWorker] Scan requis de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}` `[CoreWorker] Scan requis de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`
); );
try { try {
await this.wasmService.scanBlocks(this.currentBlockHeight, APP_CONFIG.URLS.BLINDBIT); await this.sdkService
const updatedDevice = await this.walletService.dumpDeviceFromMemory(); .getClient()
.scan_blocks(this.currentBlockHeight, APP_CONFIG.URLS.BLINDBIT);
const updatedDevice = this.walletService.dumpDeviceFromMemory();
await this.walletService.saveDeviceInDatabase(updatedDevice); await this.walletService.saveDeviceInDatabase(updatedDevice);
} catch (e) { } catch (e) {
console.error("Scan error", e); console.error("Scan error", e);
@ -452,7 +371,7 @@ export class CoreBackend {
existing.states.push(existingLast); existing.states.push(existingLast);
toSave[processId] = existing; toSave[processId] = existing;
hasChanged = true; hasChanged = true;
if (await this.rolesContainsUs(state.roles)) { if (this.rolesContainsUs(state.roles)) {
newStates.push(state.state_id); newStates.push(state.state_id);
newRoles.push(state.roles); newRoles.push(state.roles);
} }
@ -468,7 +387,7 @@ export class CoreBackend {
(!existingState.keys || (!existingState.keys ||
Object.keys(existingState.keys).length === 0) Object.keys(existingState.keys).length === 0)
) { ) {
if (await this.rolesContainsUs(state.roles)) { if (this.rolesContainsUs(state.roles)) {
newStates.push(state.state_id); newStates.push(state.state_id);
newRoles.push(state.roles); newRoles.push(state.roles);
// Ici on ne marque pas forcément hasChanged pour la DB, mais on demande les clés // Ici on ne marque pas forcément hasChanged pour la DB, mais on demande les clés
@ -510,10 +429,7 @@ export class CoreBackend {
// ========================================== // ==========================================
public async getMyProcesses(): Promise<string[] | null> { public async getMyProcesses(): Promise<string[] | null> {
try { try {
if (!(await this.isPaired())) { const pid = this.getPairingProcessId();
return null;
}
const pid = await this.getPairingProcessId();
return await this.processService.getMyProcesses(pid); return await this.processService.getMyProcesses(pid);
} catch (e) { } catch (e) {
return null; return null;
@ -554,7 +470,7 @@ export class CoreBackend {
} }
} }
if (publicData && publicData["pairedAddresses"]) { if (publicData && publicData["pairedAddresses"]) {
const decoded = await this.decodeValue(publicData["pairedAddresses"]); const decoded = this.decodeValue(publicData["pairedAddresses"]);
if (decoded) members.add({ sp_addresses: decoded }); if (decoded) members.add({ sp_addresses: decoded });
} }
} }
@ -562,7 +478,7 @@ export class CoreBackend {
if (members.size === 0) return; if (members.size === 0) return;
const unconnected = new Set<string>(); const unconnected = new Set<string>();
const myAddress = await this.getDeviceAddress(); const myAddress = this.getDeviceAddress();
for (const member of Array.from(members)) { for (const member of Array.from(members)) {
if (!member.sp_addresses) continue; if (!member.sp_addresses) continue;
for (const address of member.sp_addresses) { for (const address of member.sp_addresses) {
@ -586,11 +502,13 @@ export class CoreBackend {
if (addresses.length === 0) return null; if (addresses.length === 0) return null;
const feeRate = APP_CONFIG.FEE_RATE; const feeRate = APP_CONFIG.FEE_RATE;
try { try {
return await this.wasmService.createTransaction(addresses, feeRate); return this.sdkService.getClient().create_transaction(addresses, feeRate);
} catch (error: any) { } catch (error: any) {
if (String(error).includes("Insufficient funds")) { if (String(error).includes("Insufficient funds")) {
await this.getTokensFromFaucet(); await this.getTokensFromFaucet();
return await this.wasmService.createTransaction(addresses, feeRate); return this.sdkService
.getClient()
.create_transaction(addresses, feeRate);
} else { } else {
throw error; throw error;
} }
@ -599,15 +517,15 @@ export class CoreBackend {
private async getTokensFromFaucet(): Promise<void> { private async getTokensFromFaucet(): Promise<void> {
console.log("[CoreWorker] 🚰 Demande Faucet..."); console.log("[CoreWorker] 🚰 Demande Faucet...");
const availableAmt = await this.getAmount(); const availableAmt = this.getAmount();
const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10); const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10);
if (availableAmt < target) { if (availableAmt < target) {
const msg = await this.wasmService.createFaucetMessage(); const msg = this.sdkService.getClient().create_faucet_msg();
if (this.networkSender) this.networkSender("Faucet", msg); if (this.networkSender) this.networkSender("Faucet", msg);
let attempts = 3; let attempts = 3;
while (attempts > 0) { while (attempts > 0) {
if ((await this.getAmount()) >= target) return; if (this.getAmount() >= target) return;
attempts--; attempts--;
await new Promise((r) => await new Promise((r) =>
setTimeout(r, APP_CONFIG.TIMEOUTS.RETRY_DELAY) setTimeout(r, APP_CONFIG.TIMEOUTS.RETRY_DELAY)
@ -621,8 +539,8 @@ export class CoreBackend {
userName: string, userName: string,
pairWith: string[] pairWith: string[]
): Promise<ApiReturn> { ): Promise<ApiReturn> {
if (await this.isPaired()) throw new Error("Déjà appairé"); if (this.isPaired()) throw new Error("Déjà appairé");
const myAddress = await this.getDeviceAddress(); const myAddress = this.getDeviceAddress();
pairWith.push(myAddress); pairWith.push(myAddress);
const privateData = { description: "pairing", counter: 0 }; const privateData = { description: "pairing", counter: 0 };
const publicData = { const publicData = {
@ -692,7 +610,9 @@ export class CoreBackend {
fee: number, fee: number,
members: any members: any
): Promise<ApiReturn> { ): Promise<ApiReturn> {
const res = await this.wasmService.createNewProcess(priv, roles, pub, relay, fee, members); const res = this.sdkService
.getClient()
.create_new_process(priv, roles, pub, relay, fee, members);
if (res.updated_process) { if (res.updated_process) {
await this.ensureConnections(res.updated_process.current_process); await this.ensureConnections(res.updated_process.current_process);
} }
@ -713,7 +633,7 @@ export class CoreBackend {
if (!lastState) { if (!lastState) {
const first = process.states[0]; const first = process.states[0];
if (await this.rolesContainsUs(first.roles)) { if (this.rolesContainsUs(first.roles)) {
const appRes = await this.approveChange(processId, first.state_id); const appRes = await this.approveChange(processId, first.state_id);
await this.handleApiReturn(appRes); await this.handleApiReturn(appRes);
const prdRes = await this.createPrdUpdate(processId, first.state_id); const prdRes = await this.createPrdUpdate(processId, first.state_id);
@ -758,11 +678,13 @@ export class CoreBackend {
const { encodedPrivateData, encodedPublicData } = const { encodedPrivateData, encodedPublicData } =
await this.prepareProcessData(privateData, publicData); await this.prepareProcessData(privateData, publicData);
const res = await this.wasmService.updateProcess( const res = this.sdkService
.getClient()
.update_process(
currentProcess, currentProcess,
encodedPrivateData, encodedPrivateData,
encodedPublicData,
finalRoles, finalRoles,
encodedPublicData,
this.membersList this.membersList
); );
if (res.updated_process) if (res.updated_process)
@ -775,12 +697,12 @@ export class CoreBackend {
const p2 = this.splitData(pub); const p2 = this.splitData(pub);
return { return {
encodedPrivateData: { encodedPrivateData: {
...(await this.wasmService.encodeJson(p1.jsonCompatibleData)), ...this.sdkService.getClient().encode_json(p1.jsonCompatibleData),
...(await this.wasmService.encodeBinary(p1.binaryData)), ...this.sdkService.getClient().encode_binary(p1.binaryData),
}, },
encodedPublicData: { encodedPublicData: {
...(await this.wasmService.encodeJson(p2.jsonCompatibleData)), ...this.sdkService.getClient().encode_json(p2.jsonCompatibleData),
...(await this.wasmService.encodeBinary(p2.binaryData)), ...this.sdkService.getClient().encode_binary(p2.binaryData),
}, },
}; };
} }
@ -791,30 +713,38 @@ export class CoreBackend {
public async createPrdUpdate(pid: string, sid: string) { public async createPrdUpdate(pid: string, sid: string) {
const p = await this.getProcess(pid); const p = await this.getProcess(pid);
await this.ensureConnections(p!); await this.ensureConnections(p!);
return await this.wasmService.createUpdateMessage(p, sid, this.membersList); return this.sdkService
.getClient()
.create_update_message(p, sid, this.membersList);
} }
public async createPrdResponse(pid: string, sid: string) { public async createPrdResponse(pid: string, sid: string) {
const p = await this.getProcess(pid); const p = await this.getProcess(pid);
return await this.wasmService.createPrdResponse(p, sid, this.membersList); return this.sdkService
.getClient()
.create_response_prd(p, sid, this.membersList);
} }
public async approveChange(pid: string, sid: string) { public async approveChange(pid: string, sid: string) {
const p = await this.getProcess(pid); const p = await this.getProcess(pid);
const res = await this.wasmService.validateState(p, sid, this.membersList); const res = this.sdkService
.getClient()
.validate_state(p, sid, this.membersList);
if (res.updated_process) if (res.updated_process)
await this.ensureConnections(res.updated_process.current_process); await this.ensureConnections(res.updated_process.current_process);
return res; return res;
} }
public async rejectChange(pid: string, sid: string) { public async rejectChange(pid: string, sid: string) {
const p = await this.getProcess(pid); const p = await this.getProcess(pid);
return await this.wasmService.refuseState(p, sid); return this.sdkService.getClient().refuse_state(p, sid);
} }
public async requestDataFromPeers(pid: string, sids: string[], roles: any) { public async requestDataFromPeers(pid: string, sids: string[], roles: any) {
const res = await this.wasmService.requestData(pid, sids, roles, this.membersList); const res = this.sdkService
.getClient()
.request_data(pid, sids, roles, this.membersList);
await this.handleApiReturn(res); await this.handleApiReturn(res);
} }
public async resetDevice() { public async resetDevice() {
await this.wasmService.resetDevice(); this.sdkService.getClient().reset_device();
await this.db.clearMultipleStores([ await this.db.clearMultipleStores([
"wallet", "wallet",
"shared_secrets", "shared_secrets",
@ -852,8 +782,8 @@ export class CoreBackend {
private async handlePartialTx(partialTx: any): Promise<any> { private async handlePartialTx(partialTx: any): Promise<any> {
try { try {
const result = await this.wasmService.signTransaction(partialTx); return this.sdkService.getClient().sign_transaction(partialTx)
return result.new_tx_to_send; .new_tx_to_send;
} catch (e) { } catch (e) {
return null; return null;
} }
@ -941,19 +871,11 @@ export class CoreBackend {
} }
} }
public async rolesContainsUs(roles: any) { public rolesContainsUs(roles: any) {
try {
if (!(await this.isPaired())) {
return false;
}
return this.processService.rolesContainsMember( return this.processService.rolesContainsMember(
roles, roles,
await this.getPairingProcessId() this.getPairingProcessId()
); );
} catch (e) {
console.error("RolesContainsUs Error:", e);
return false;
}
} }
public async getSecretForAddress(address: string): Promise<string | null> { public async getSecretForAddress(address: string): Promise<string | null> {
@ -1024,7 +946,9 @@ export class CoreBackend {
// ========================================== // ==========================================
async parseCipher(msg: string) { async parseCipher(msg: string) {
try { try {
const res = await this.wasmService.parseCipher(msg, this.membersList, await this.getProcesses()); const res = this.sdkService
.getClient()
.parse_cipher(msg, this.membersList, await this.getProcesses());
await this.handleApiReturn(res); await this.handleApiReturn(res);
} catch (e) { } catch (e) {
console.error("Cipher Error", e); console.error("Cipher Error", e);
@ -1035,19 +959,27 @@ export class CoreBackend {
const parsed = JSON.parse(msg); const parsed = JSON.parse(msg);
if (parsed.error) return; if (parsed.error) return;
const prevouts = await this.wasmService.getPrevouts(parsed.transaction); const prevouts = this.sdkService
.getClient()
.get_prevouts(parsed.transaction);
for (const p of Object.values(await this.getProcesses())) { for (const p of Object.values(await this.getProcesses())) {
const tip = p.states[p.states.length - 1].commited_in; const tip = p.states[p.states.length - 1].commited_in;
if (prevouts.includes(tip)) { if (prevouts.includes(tip)) {
const newTip = await this.wasmService.getTxid(parsed.transaction); const newTip = this.sdkService.getClient().get_txid(parsed.transaction);
const newStateId = await this.wasmService.getOpReturn(parsed.transaction); const newStateId = this.sdkService
await this.wasmService.processCommitNewState(p, newStateId, newTip); .getClient()
.get_opreturn(parsed.transaction);
this.sdkService
.getClient()
.process_commit_new_state(p, newStateId, newTip);
break; break;
} }
} }
try { try {
const res = await this.wasmService.parseNewTx(msg, 0, this.membersList); const res = this.sdkService
.getClient()
.parse_new_tx(msg, 0, this.membersList);
if ( if (
res && res &&
(res.partial_tx || (res.partial_tx ||
@ -1056,7 +988,7 @@ export class CoreBackend {
res.updated_process) res.updated_process)
) { ) {
await this.handleApiReturn(res); await this.handleApiReturn(res);
const d = await this.dumpDeviceFromMemory(); const d = this.dumpDeviceFromMemory();
const old = await this.getDeviceFromDatabase(); const old = await this.getDeviceFromDatabase();
if (old && old.pairing_process_commitment) if (old && old.pairing_process_commitment)
d.pairing_process_commitment = old.pairing_process_commitment; d.pairing_process_commitment = old.pairing_process_commitment;
@ -1071,7 +1003,7 @@ export class CoreBackend {
public async importJSON(backup: BackUp) { public async importJSON(backup: BackUp) {
await this.resetDevice(); await this.resetDevice();
await this.walletService.saveDeviceInDatabase(backup.device); await this.walletService.saveDeviceInDatabase(backup.device);
await this.walletService.restoreDevice(backup.device); this.walletService.restoreDevice(backup.device);
await this.processService.batchSaveProcesses(backup.processes); await this.processService.batchSaveProcesses(backup.processes);
await this.restoreSecretsFromBackUp(backup.secrets); await this.restoreSecretsFromBackUp(backup.secrets);
} }
@ -1087,7 +1019,9 @@ export class CoreBackend {
} }
public async restoreSecretsFromDB() { public async restoreSecretsFromDB() {
const secretsStore = await this.db.getAllSecrets(); const secretsStore = await this.db.getAllSecrets();
await this.wasmService.setSharedSecrets(JSON.stringify(secretsStore)); this.sdkService
.getClient()
.set_shared_secrets(JSON.stringify(secretsStore));
console.log("[CoreWorker] 🔐 Secrets restaurés depuis la DB"); console.log("[CoreWorker] 🔐 Secrets restaurés depuis la DB");
} }
public async createBackUp() { public async createBackUp() {
@ -1113,13 +1047,9 @@ export class CoreBackend {
); );
try { try {
if (!(await this.isPaired())) {
console.warn(`⚠️ Device is not paired. Cannot decrypt attribute.`);
return null;
}
let hash: string | null | undefined = state.pcd_commitment[attribute]; let hash: string | null | undefined = state.pcd_commitment[attribute];
let key: string | null | undefined = state.keys[attribute]; let key: string | null | undefined = state.keys[attribute];
const pairingProcessId = await this.getPairingProcessId(); const pairingProcessId = this.getPairingProcessId();
if (!hash) { if (!hash) {
console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`); console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`);
@ -1150,10 +1080,12 @@ export class CoreBackend {
const cipher = new Uint8Array(buf); const cipher = new Uint8Array(buf);
const keyUIntArray = this.hexToUInt8Array(key); const keyUIntArray = this.hexToUInt8Array(key);
const clear = await this.wasmService.decryptData(keyUIntArray, cipher); const clear = this.sdkService
.getClient()
.decrypt_data(keyUIntArray, cipher);
if (!clear) throw new Error("decrypt_data returned null"); if (!clear) throw new Error("decrypt_data returned null");
const decoded = await this.wasmService.decodeValue(Array.from(clear)); const decoded = this.sdkService.getClient().decode_value(clear);
console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`); console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`);
return decoded; return decoded;
} catch (e) { } catch (e) {

View File

@ -0,0 +1,150 @@
import * as Comlink from 'comlink';
import { APP_CONFIG } from '../config/constants';
// On redéfinit le type localement pour éviter d'importer tout le SDK WASM ici
type AnkFlag = 'Handshake' | 'NewTx' | 'Cipher' | 'Commit' | 'Faucet' | 'Ping';
export class NetworkBackend {
private sockets: Map<string, WebSocket> = new Map();
private relayAddresses: Map<string, string> = new Map(); // wsUrl -> spAddress
private messageQueue: string[] = [];
// Callback pour notifier le Main Thread
private msgCallback: ((flag: string, content: string, url: string) => void) | null = null;
private statusCallback: ((url: string, status: 'OPEN' | 'CLOSED', spAddress?: string) => void) | null = null;
// Timers pour la gestion des reconnexions
private reconnectTimers: Map<string, any> = new Map();
private heartbeatInterval: any = null;
constructor() {
this.startHeartbeat();
}
public setCallbacks(
msgCb: (flag: string, content: string, url: string) => void,
statusCb: (url: string, status: 'OPEN' | 'CLOSED', spAddress?: string) => void
) {
this.msgCallback = msgCb;
this.statusCallback = statusCb;
}
public async connect(url: string) {
if (this.sockets.has(url) && this.sockets.get(url)?.readyState === WebSocket.OPEN) return;
console.log(`[NetworkWorker] 🔌 Connexion à ${url}...`);
const ws = new WebSocket(url);
ws.onopen = () => {
console.log(`[NetworkWorker] ✅ Connecté à ${url}`);
this.sockets.set(url, ws);
// Reset timer reconnexion si existant
if (this.reconnectTimers.has(url)) {
clearTimeout(this.reconnectTimers.get(url));
this.reconnectTimers.delete(url);
}
// Vider la file d'attente (si message en attente pour ce socket ou broadcast)
this.flushQueue();
if (this.statusCallback) this.statusCallback(url, 'OPEN');
};
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
// Si c'est un Handshake, on met à jour la map locale
if (msg.flag === 'Handshake' && msg.content) {
const handshake = JSON.parse(msg.content);
if (handshake.sp_address) {
this.relayAddresses.set(url, handshake.sp_address);
if (this.statusCallback) this.statusCallback(url, 'OPEN', handshake.sp_address);
}
}
// On remonte TOUT au Main Thread (qui passera au Core)
if (this.msgCallback) this.msgCallback(msg.flag, msg.content, url);
} catch (e) {
console.error('[NetworkWorker] Erreur parsing message:', e);
}
};
ws.onerror = (e) => {
// console.error(`[NetworkWorker] Erreur sur ${url}`, e);
};
ws.onclose = () => {
console.warn(`[NetworkWorker] ❌ Déconnecté de ${url}.`);
this.sockets.delete(url);
this.relayAddresses.set(url, ''); // Reset spAddress
if (this.statusCallback) this.statusCallback(url, 'CLOSED');
this.scheduleReconnect(url);
};
}
public sendMessage(flag: AnkFlag, content: string) {
const msgStr = JSON.stringify({ flag, content });
// Stratégie simple : On envoie à TOUS les relais connectés (Broadcast)
// Ou on pourrait cibler un relais spécifique si besoin.
let sent = false;
for (const [url, ws] of this.sockets) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(msgStr);
sent = true;
}
}
if (!sent) {
// console.warn(`[NetworkWorker] Pas de connexion. Message ${flag} mis en file.`);
this.messageQueue.push(msgStr);
}
}
public getAvailableRelay(): string | null {
// Retourne l'adresse SP d'un relais connecté
for (const sp of this.relayAddresses.values()) {
if (sp && sp !== '') return sp;
}
return null;
}
public getAllRelays() {
return Object.fromEntries(this.relayAddresses);
}
// --- INTERNES ---
private flushQueue() {
while (this.messageQueue.length > 0) {
const msg = this.messageQueue.shift();
if (!msg) break;
for (const ws of this.sockets.values()) {
if (ws.readyState === WebSocket.OPEN) ws.send(msg);
}
}
}
private scheduleReconnect(url: string) {
if (this.reconnectTimers.has(url)) return;
console.log(`[NetworkWorker] ⏳ Reconnexion à ${url} dans 3s...`);
const timer = setTimeout(() => {
this.reconnectTimers.delete(url);
this.connect(url);
}, 3000); // Délai fixe ou APP_CONFIG.TIMEOUTS.RETRY_DELAY
this.reconnectTimers.set(url, timer);
}
private startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
// Envoi d'un ping léger ou gestion du keep-alive
// this.sendMessage('Ping', '');
}, 30000);
}
}
Comlink.expose(new NetworkBackend());

View File

@ -1,112 +0,0 @@
/**
* WASM Worker - Handles all WASM operations in background
* Single source of truth for WASM module lifecycle and method calls
*/
/// <reference lib="webworker" />
interface WasmWorkerMessage {
id: string;
type: 'WASM_METHOD' | 'INIT';
method?: string;
args?: any[];
}
interface WasmWorkerResponse {
id: string;
type: 'SUCCESS' | 'ERROR';
result?: any;
error?: string;
}
let wasmClient: any = null;
let isInitialized = false;
// ============================================
// WASM INITIALIZATION
// ============================================
async function initWasm(): Promise<void> {
if (isInitialized && wasmClient) {
return;
}
console.log('[WasmWorker] 🔄 Initializing WASM module...');
wasmClient = await import('../../pkg/sdk_client');
wasmClient.setup();
isInitialized = true;
console.log('[WasmWorker] ✅ WASM module initialized');
}
// ============================================
// WASM METHOD CALL HANDLER
// ============================================
async function callWasmMethod(method: string, args: any[]): Promise<any> {
if (!wasmClient) {
throw new Error('WASM client not initialized');
}
if (typeof wasmClient[method] !== 'function') {
throw new Error(`WASM method '${method}' does not exist`);
}
try {
return await wasmClient[method](...args);
} catch (error) {
console.error(`[WasmWorker] Error calling WASM method '${method}':`, error);
throw error;
}
}
// ============================================
// MESSAGE HANDLER
// ============================================
self.addEventListener('message', async (event: MessageEvent<WasmWorkerMessage>) => {
const { id, type, method, args = [] } = event.data;
try {
let result: any;
switch (type) {
case 'INIT':
await initWasm();
result = { success: true };
break;
case 'WASM_METHOD':
if (!method) {
throw new Error('Method name is required');
}
if (!isInitialized) {
await initWasm();
}
result = await callWasmMethod(method, args);
break;
default:
throw new Error(`Unknown message type: ${type}`);
}
self.postMessage({
id,
type: 'SUCCESS',
result,
} as WasmWorkerResponse);
} catch (error) {
self.postMessage({
id,
type: 'ERROR',
error: error instanceof Error ? error.message : String(error),
} as WasmWorkerResponse);
}
});
// ============================================
// INITIALIZATION
// ============================================
console.log('[WasmWorker] 🔄 Worker script loaded');

View File

@ -1,94 +1,31 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import wasm from 'vite-plugin-wasm'; import wasm from 'vite-plugin-wasm';
import { fileURLToPath, URL } from 'node:url'; import { fileURLToPath, URL } from 'node:url';
import { resolve } from 'path';
import type { Plugin } from 'vite';
// Plugin to handle Service Worker in dev and build
function serviceWorkerPlugin(): Plugin {
const swPath = resolve(__dirname, 'src/service-workers/network.sw.ts');
const swUrl = '/src/service-workers/network.sw.ts';
return {
name: 'service-worker-plugin',
enforce: 'pre', // Run before other plugins
configureServer(server) {
// In dev mode, serve the Service Worker from src
server.middlewares.use('/network.sw.js', async (req, res, next) => {
console.log('[Service Worker Plugin] Request for /network.sw.js');
try {
// Try using the URL format that Vite expects
let result = await server.transformRequest(swUrl, { ssr: false });
// If that doesn't work, try with the file path
if (!result || !result.code) {
console.log('[Service Worker Plugin] Trying with file path...');
result = await server.transformRequest(swPath, { ssr: false });
}
if (result && result.code) {
console.log('[Service Worker Plugin] Successfully transformed Service Worker');
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Service-Worker-Allowed', '/');
res.setHeader('Cache-Control', 'no-cache');
res.end(result.code);
return;
}
// Final fallback: use pluginContainer directly
console.log('[Service Worker Plugin] Fallback: using pluginContainer.transform');
const { readFileSync } = await import('fs');
const code = readFileSync(swPath, 'utf-8');
const transformed = await server.pluginContainer.transform(code, swUrl);
if (transformed && transformed.code) {
console.log('[Service Worker Plugin] Successfully transformed via pluginContainer');
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Service-Worker-Allowed', '/');
res.setHeader('Cache-Control', 'no-cache');
res.end(transformed.code);
} else {
console.error('[Service Worker Plugin] Failed to transform Service Worker');
res.statusCode = 500;
res.end('Failed to load Service Worker');
}
} catch (err) {
console.error('[Service Worker Plugin] Error serving SW:', err);
if (err instanceof Error) {
console.error('[Service Worker Plugin] Error details:', err.stack);
}
res.statusCode = 500;
res.end(`Service Worker error: ${err instanceof Error ? err.message : String(err)}`);
}
});
},
};
}
export default defineConfig({ export default defineConfig({
// Configuration du serveur de développement // Configuration du serveur de développement
server: { server: {
port: 3003, port: 3003,
host: '0.0.0.0', // Permet l'accès depuis l'extérieur (Docker/Réseau) host: '0.0.0.0', // Permet l'accès depuis l'extérieur (Docker/Réseau)
allowedHosts: ['dev3.4nkweb.com'], allowedHosts: ['dev2.4nkweb.com'],
proxy: { proxy: {
// Proxy pour le stockage // Proxy pour le stockage
'/storage': { '/storage': {
target: process.env.VITE_STORAGEURL || 'https://dev3.4nkweb.com', target: process.env.VITE_STORAGEURL || 'https://dev2.4nkweb.com',
changeOrigin: true, changeOrigin: true,
secure: false, // Accepte les certificats auto-signés si besoin secure: false, // Accepte les certificats auto-signés si besoin
rewrite: (path) => path.replace(/^\/storage/, '/storage'), rewrite: (path) => path.replace(/^\/storage/, '/storage'),
}, },
// Proxy pour les websockets (si besoin de contourner CORS ou SSL) // Proxy pour les websockets (si besoin de contourner CORS ou SSL)
'/ws': { '/ws': {
target: process.env.VITE_BOOTSTRAPURL?.replace('ws', 'http') || 'https://dev3.4nkweb.com', target: process.env.VITE_BOOTSTRAPURL?.replace('ws', 'http') || 'https://dev2.4nkweb.com',
ws: true, ws: true,
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },
// Proxy pour l'API BlindBit // Proxy pour l'API BlindBit
'/blindbit': { '/blindbit': {
target: process.env.VITE_BLINDBITURL || 'https://dev3.4nkweb.com/blindbit', target: process.env.VITE_BLINDBITURL || 'https://dev2.4nkweb.com/blindbit',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
rewrite: (path) => path.replace(/^\/blindbit/, ''), rewrite: (path) => path.replace(/^\/blindbit/, ''),
@ -99,7 +36,6 @@ export default defineConfig({
// Plugins essentiels // Plugins essentiels
plugins: [ plugins: [
wasm(), // Indispensable pour ton SDK Rust wasm(), // Indispensable pour ton SDK Rust
serviceWorkerPlugin(), // Service Worker handler
], ],
// Alias pour les imports (ex: import ... from '@/services/...') // Alias pour les imports (ex: import ... from '@/services/...')
@ -116,17 +52,7 @@ export default defineConfig({
outDir: 'dist', outDir: 'dist',
assetsDir: 'assets', assetsDir: 'assets',
emptyOutDir: true, // Vide le dossier dist avant chaque build emptyOutDir: true, // Vide le dossier dist avant chaque build
rollupOptions: { // On retire la config "lib" car c'est maintenant une App autonome
input: {
main: resolve(__dirname, 'index.html'),
'network.sw': resolve(__dirname, 'src/service-workers/network.sw.ts'),
},
output: {
entryFileNames: (chunkInfo) => {
return chunkInfo.name === 'network.sw' ? 'network.sw.js' : 'assets/[name]-[hash].js';
},
},
},
}, },
// Configuration spécifique pour les Workers (Database) // Configuration spécifique pour les Workers (Database)