1234 lines
41 KiB
TypeScript
1234 lines
41 KiB
TypeScript
import * as Comlink from "comlink";
|
|
import {
|
|
ApiReturn,
|
|
Device,
|
|
HandshakeMessage,
|
|
Member,
|
|
MerkleProofResult,
|
|
OutPointProcessMap,
|
|
Process,
|
|
ProcessState,
|
|
RoleDefinition,
|
|
SecretsStore,
|
|
UserDiff,
|
|
} from "../../pkg/sdk_client";
|
|
import Database from "../services/database.service";
|
|
import { storeData, retrieveData } from "../services/storage.service";
|
|
import { BackUp } from "../types/index";
|
|
import { APP_CONFIG } from "../config/constants";
|
|
import { splitPrivateData } from "../utils/service.utils";
|
|
|
|
// Services internes au worker
|
|
import { WalletService } from "../services/wallet.service";
|
|
import { ProcessService } from "../services/process.service";
|
|
import { CryptoService } from "../services/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 {
|
|
// Services (passed from main thread via Comlink)
|
|
private wasmService!: WasmServiceProxy;
|
|
private db!: DatabaseServiceProxy;
|
|
|
|
// Domain services
|
|
private walletService!: WalletService;
|
|
private processService!: ProcessService;
|
|
private cryptoService!: CryptoService;
|
|
|
|
// État (State)
|
|
private processId: string | null = null;
|
|
private stateId: string | null = null;
|
|
private membersList: Record<string, Member> = {};
|
|
private notifications: any[] | null = null;
|
|
private currentBlockHeight: number = -1;
|
|
private pendingKeyRequests: Map<string, (key: string) => void> = new Map();
|
|
|
|
// Flags publics (State)
|
|
public device1: boolean = false;
|
|
public device2Ready: boolean = false;
|
|
|
|
private isInitialized = false;
|
|
|
|
// Callbacks vers le Main Thread
|
|
private notifier: ((event: string, data?: any) => void) | null = null;
|
|
private networkSender: ((flag: string, content: string) => void) | null =
|
|
null;
|
|
private relayUpdater: ((url: string, sp: string) => void) | null = null;
|
|
private relayGetter: (() => Promise<string>) | null = null;
|
|
|
|
constructor() {
|
|
// Services will be set via setServices() from main thread
|
|
}
|
|
|
|
public async init(): Promise<void> {
|
|
if (this.isInitialized) return;
|
|
|
|
console.log("[CoreWorker] ⚙️ Initialisation du Backend...");
|
|
|
|
// Services must be set before init
|
|
if (!this.wasmService || !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.isInitialized = true;
|
|
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 ---
|
|
public setCallbacks(
|
|
notifier: (event: string, data?: any) => void,
|
|
networkSender: (flag: string, content: string) => void,
|
|
relayUpdater: (url: string, sp: string) => void,
|
|
relayGetter: () => Promise<string>
|
|
) {
|
|
this.notifier = notifier;
|
|
this.networkSender = networkSender;
|
|
this.relayUpdater = relayUpdater;
|
|
this.relayGetter = relayGetter;
|
|
}
|
|
|
|
// ==========================================
|
|
// GETTERS & SETTERS (STATE)
|
|
// ==========================================
|
|
public setProcessId(id: string | null) {
|
|
this.processId = id;
|
|
}
|
|
public setStateId(id: string | null) {
|
|
this.stateId = id;
|
|
}
|
|
public getProcessId() {
|
|
return this.processId;
|
|
}
|
|
public getStateId() {
|
|
return this.stateId;
|
|
}
|
|
|
|
public getDevice1() {
|
|
return this.device1;
|
|
} // Ajouté
|
|
public getDevice2Ready() {
|
|
return this.device2Ready;
|
|
} // Ajouté
|
|
|
|
public resetState() {
|
|
this.device1 = false;
|
|
this.device2Ready = false;
|
|
}
|
|
|
|
// ==========================================
|
|
// WALLET PROXY
|
|
// ==========================================
|
|
public async isPaired() {
|
|
return await this.walletService.isPaired();
|
|
}
|
|
public async getAmount() {
|
|
return await this.walletService.getAmount();
|
|
}
|
|
public async getDeviceAddress() {
|
|
return await this.walletService.getDeviceAddress();
|
|
}
|
|
public async dumpDeviceFromMemory() {
|
|
return await this.walletService.dumpDeviceFromMemory();
|
|
}
|
|
public async dumpNeuteredDevice() {
|
|
return await this.walletService.dumpNeuteredDevice();
|
|
}
|
|
public async getPairingProcessId() {
|
|
return await this.walletService.getPairingProcessId();
|
|
}
|
|
public async getDeviceFromDatabase() {
|
|
return this.walletService.getDeviceFromDatabase();
|
|
}
|
|
public async restoreDevice(d: Device) {
|
|
await this.walletService.restoreDevice(d);
|
|
}
|
|
public async pairDevice(pid: string, list: string[]) {
|
|
await this.walletService.pairDevice(pid, list);
|
|
}
|
|
public async unpairDevice() {
|
|
await this.walletService.unpairDevice();
|
|
}
|
|
public async saveDeviceInDatabase(d: Device) {
|
|
await this.walletService.saveDeviceInDatabase(d);
|
|
}
|
|
public async createNewDevice() {
|
|
return this.walletService.createNewDevice(
|
|
this.currentBlockHeight > 0 ? this.currentBlockHeight : 0
|
|
);
|
|
}
|
|
public async dumpWallet() {
|
|
return this.walletService.dumpWallet();
|
|
}
|
|
public async getMemberFromDevice() {
|
|
return this.walletService.getMemberFromDevice();
|
|
}
|
|
|
|
// ==========================================
|
|
// PROCESS PROXY
|
|
// ==========================================
|
|
public async getProcess(id: string) {
|
|
return this.processService.getProcess(id);
|
|
}
|
|
public async getProcesses() {
|
|
return this.processService.getProcesses();
|
|
}
|
|
public async restoreProcessesFromDB() {
|
|
await this.processService.getProcesses();
|
|
}
|
|
public getLastCommitedState(p: Process) {
|
|
return this.processService.getLastCommitedState(p);
|
|
}
|
|
public getUncommitedStates(p: Process) {
|
|
return this.processService.getUncommitedStates(p);
|
|
}
|
|
public getStateFromId(p: Process, id: string) {
|
|
return this.processService.getStateFromId(p, id);
|
|
}
|
|
public getRoles(p: Process) {
|
|
return this.processService.getRoles(p);
|
|
}
|
|
public getLastCommitedStateIndex(p: Process) {
|
|
return this.processService.getLastCommitedStateIndex(p);
|
|
}
|
|
public async batchSaveProcessesToDb(p: Record<string, Process>) {
|
|
return this.processService.batchSaveProcesses(p);
|
|
}
|
|
|
|
// ==========================================
|
|
// CRYPTO HELPERS
|
|
// ==========================================
|
|
public async decodeValue(val: number[]) {
|
|
return await this.wasmService.decodeValue(val);
|
|
}
|
|
public hexToBlob(hex: string) {
|
|
return this.cryptoService.hexToBlob(hex);
|
|
}
|
|
public hexToUInt8Array(hex: string) {
|
|
return this.cryptoService.hexToUInt8Array(hex);
|
|
}
|
|
public async blobToHex(blob: Blob) {
|
|
return this.cryptoService.blobToHex(blob);
|
|
}
|
|
public async getHashForFile(c: string, l: string, f: any) {
|
|
return await this.cryptoService.getHashForFile(c, l, f);
|
|
}
|
|
public async getMerkleProofForFile(s: ProcessState, a: string) {
|
|
return await this.cryptoService.getMerkleProofForFile(s, a);
|
|
}
|
|
public async validateMerkleProof(p: MerkleProofResult, h: string) {
|
|
return await this.cryptoService.validateMerkleProof(p, h);
|
|
}
|
|
private splitData(obj: Record<string, any>) {
|
|
return this.cryptoService.splitData(obj);
|
|
}
|
|
|
|
// ==========================================
|
|
// MEMBERS
|
|
// ==========================================
|
|
public getAllMembers() {
|
|
return this.membersList;
|
|
}
|
|
public getAllMembersSorted() {
|
|
return Object.fromEntries(
|
|
Object.entries(this.membersList).sort(([keyA], [keyB]) =>
|
|
keyA.localeCompare(keyB)
|
|
)
|
|
);
|
|
}
|
|
public async ensureMembersAvailable(): Promise<void> {
|
|
if (Object.keys(this.membersList).length > 0) return;
|
|
console.warn("[CoreWorker] Tentative de récupération des membres...");
|
|
}
|
|
public getAddressesForMemberId(memberId: string): string[] | null {
|
|
if (!this.membersList[memberId]) return null;
|
|
return this.membersList[memberId].sp_addresses;
|
|
}
|
|
public compareMembers(memberA: string[], memberB: string[]): boolean {
|
|
if (!memberA || !memberB) return false;
|
|
if (memberA.length !== memberB.length) return false;
|
|
return (
|
|
memberA.every((item) => memberB.includes(item)) &&
|
|
memberB.every((item) => memberA.includes(item))
|
|
);
|
|
}
|
|
|
|
// ==========================================
|
|
// UTILITAIRES DIVERS
|
|
// ==========================================
|
|
public async createFaucetMessage() {
|
|
return await this.wasmService.createFaucetMessage();
|
|
}
|
|
|
|
public async isChildRole(parent: any, child: any): Promise<boolean> {
|
|
try {
|
|
return await this.wasmService.isChildRole(parent, child);
|
|
} catch (e) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// LOGIQUE HANDSHAKE
|
|
// ==========================================
|
|
public async handleHandshakeMsg(url: string, parsedMsg: any) {
|
|
try {
|
|
const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg);
|
|
|
|
if (handshakeMsg.sp_address && this.relayUpdater) {
|
|
await this.relayUpdater(url, handshakeMsg.sp_address);
|
|
}
|
|
this.currentBlockHeight = handshakeMsg.chain_tip;
|
|
|
|
if (!(await this.isPaired())) {
|
|
console.log(`[CoreWorker] ⏳ Non pairé. Attente appairage...`);
|
|
}
|
|
|
|
this.updateDeviceBlockHeight();
|
|
|
|
if (handshakeMsg.peers_list) {
|
|
this.membersList = {
|
|
...this.membersList,
|
|
...(handshakeMsg.peers_list as Record<string, Member>),
|
|
};
|
|
}
|
|
|
|
if (handshakeMsg.processes_list) {
|
|
await this.syncProcessesFromHandshake(handshakeMsg.processes_list);
|
|
}
|
|
} catch (e) {
|
|
console.error("Handshake Error", e);
|
|
}
|
|
}
|
|
|
|
public async updateDeviceBlockHeight() {
|
|
if (this.currentBlockHeight <= 0) return;
|
|
const device = await this.walletService.getDeviceFromDatabase();
|
|
if (!device) return;
|
|
|
|
if (device.sp_wallet.birthday === 0) {
|
|
device.sp_wallet.birthday = this.currentBlockHeight;
|
|
device.sp_wallet.last_scan = this.currentBlockHeight;
|
|
await this.walletService.saveDeviceInDatabase(device);
|
|
await this.walletService.restoreDevice(device);
|
|
} else if (device.sp_wallet.last_scan < this.currentBlockHeight) {
|
|
console.log(
|
|
`[CoreWorker] Scan requis de ${device.sp_wallet.last_scan} à ${this.currentBlockHeight}`
|
|
);
|
|
try {
|
|
await this.wasmService.scanBlocks(this.currentBlockHeight, APP_CONFIG.URLS.BLINDBIT);
|
|
const updatedDevice = await this.walletService.dumpDeviceFromMemory();
|
|
await this.walletService.saveDeviceInDatabase(updatedDevice);
|
|
} catch (e) {
|
|
console.error("Scan error", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async syncProcessesFromHandshake(newProcesses: OutPointProcessMap) {
|
|
if (!newProcesses || Object.keys(newProcesses).length === 0) return;
|
|
|
|
const toSave: Record<string, Process> = {};
|
|
const currentProcesses = await this.getProcesses();
|
|
let updatesCount = 0; // Compteur de changements réels
|
|
|
|
if (Object.keys(currentProcesses).length === 0) {
|
|
await this.processService.batchSaveProcesses(newProcesses);
|
|
updatesCount = Object.keys(newProcesses).length;
|
|
} else {
|
|
for (const [processId, process] of Object.entries(newProcesses)) {
|
|
const existing = currentProcesses[processId];
|
|
if (existing) {
|
|
let hasChanged = false; // On tracke si ce process a bougé
|
|
let newStates: string[] = [];
|
|
let newRoles: Record<string, RoleDefinition>[] = [];
|
|
|
|
for (const state of process.states) {
|
|
if (!state || !state.state_id) continue;
|
|
|
|
if (state.state_id === APP_CONFIG.EMPTY_32_BYTES) {
|
|
const existingTip =
|
|
existing.states[existing.states.length - 1].commited_in;
|
|
if (existingTip !== state.commited_in) {
|
|
existing.states.pop();
|
|
existing.states.push(state);
|
|
toSave[processId] = existing;
|
|
hasChanged = true;
|
|
}
|
|
} else if (
|
|
!this.processService.getStateFromId(existing, state.state_id)
|
|
) {
|
|
const existingLast = existing.states.pop();
|
|
if (existingLast) {
|
|
existing.states.push(state);
|
|
existing.states.push(existingLast);
|
|
toSave[processId] = existing;
|
|
hasChanged = true;
|
|
if (await this.rolesContainsUs(state.roles)) {
|
|
newStates.push(state.state_id);
|
|
newRoles.push(state.roles);
|
|
}
|
|
}
|
|
} else {
|
|
// Logique existante pour les clés manquantes
|
|
const existingState = this.processService.getStateFromId(
|
|
existing,
|
|
state.state_id
|
|
);
|
|
if (
|
|
existingState &&
|
|
(!existingState.keys ||
|
|
Object.keys(existingState.keys).length === 0)
|
|
) {
|
|
if (await this.rolesContainsUs(state.roles)) {
|
|
newStates.push(state.state_id);
|
|
newRoles.push(state.roles);
|
|
// Ici on ne marque pas forcément hasChanged pour la DB, mais on demande les clés
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasChanged) updatesCount++;
|
|
|
|
if (newStates.length > 0) {
|
|
await this.ensureConnections(existing);
|
|
await this.requestDataFromPeers(processId, newStates, newRoles);
|
|
}
|
|
} else {
|
|
// C'est un nouveau process qu'on ne connaissait pas
|
|
toSave[processId] = process;
|
|
updatesCount++;
|
|
}
|
|
}
|
|
if (Object.keys(toSave).length > 0) {
|
|
await this.processService.batchSaveProcesses(toSave);
|
|
}
|
|
}
|
|
|
|
// ✅ ON LOG ET NOTIFIE SEULEMENT SI QUELQUE CHOSE A CHANGÉ
|
|
if (updatesCount > 0) {
|
|
console.log(
|
|
`[CoreWorker] 🔄 Synchro effectuée : ${updatesCount} processus mis à jour.`
|
|
);
|
|
if (this.notifier) this.notifier("processes-updated");
|
|
} else {
|
|
// Silence radio si les données reçues sont déjà connues (idempotence)
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// LOGIQUE MÉTIER
|
|
// ==========================================
|
|
public async getMyProcesses(): Promise<string[] | null> {
|
|
try {
|
|
if (!(await this.isPaired())) {
|
|
return null;
|
|
}
|
|
const pid = await this.getPairingProcessId();
|
|
return await this.processService.getMyProcesses(pid);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async ensureConnections(
|
|
process: Process,
|
|
stateId: string | null = null
|
|
): Promise<void> {
|
|
if (!process) return;
|
|
|
|
let state: ProcessState | null = null;
|
|
if (stateId) state = this.processService.getStateFromId(process, stateId);
|
|
if (!state && process.states.length >= 2)
|
|
state = process.states[process.states.length - 2];
|
|
if (!state) return;
|
|
|
|
await this.ensureMembersAvailable();
|
|
const members = new Set<Member>();
|
|
|
|
if (state.roles) {
|
|
for (const role of Object.values(state.roles)) {
|
|
for (const memberId of role.members) {
|
|
const addrs = this.getAddressesForMemberId(memberId);
|
|
if (addrs) members.add({ sp_addresses: addrs });
|
|
}
|
|
}
|
|
}
|
|
|
|
if (members.size === 0) {
|
|
let publicData: Record<string, any> | null = null;
|
|
for (let i = process.states.length - 1; i >= 0; i--) {
|
|
const s = process.states[i];
|
|
if (s.public_data && s.public_data["pairedAddresses"]) {
|
|
publicData = s.public_data;
|
|
break;
|
|
}
|
|
}
|
|
if (publicData && publicData["pairedAddresses"]) {
|
|
const decoded = await this.decodeValue(publicData["pairedAddresses"]);
|
|
if (decoded) members.add({ sp_addresses: decoded });
|
|
}
|
|
}
|
|
|
|
if (members.size === 0) return;
|
|
|
|
const unconnected = new Set<string>();
|
|
const myAddress = await this.getDeviceAddress();
|
|
for (const member of Array.from(members)) {
|
|
if (!member.sp_addresses) continue;
|
|
for (const address of member.sp_addresses) {
|
|
if (address === myAddress) continue;
|
|
if ((await this.getSecretForAddress(address)) === null)
|
|
unconnected.add(address);
|
|
}
|
|
}
|
|
|
|
if (unconnected.size > 0) {
|
|
console.log(
|
|
`[CoreWorker] 📡 ${unconnected.size} non connectés. Connexion...`
|
|
);
|
|
await this.connectAddresses(Array.from(unconnected));
|
|
}
|
|
}
|
|
|
|
public async connectAddresses(
|
|
addresses: string[]
|
|
): Promise<ApiReturn | null> {
|
|
if (addresses.length === 0) return null;
|
|
const feeRate = APP_CONFIG.FEE_RATE;
|
|
try {
|
|
return await this.wasmService.createTransaction(addresses, feeRate);
|
|
} catch (error: any) {
|
|
if (String(error).includes("Insufficient funds")) {
|
|
await this.getTokensFromFaucet();
|
|
return await this.wasmService.createTransaction(addresses, feeRate);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async getTokensFromFaucet(): Promise<void> {
|
|
console.log("[CoreWorker] 🚰 Demande Faucet...");
|
|
const availableAmt = await this.getAmount();
|
|
const target: BigInt = APP_CONFIG.DEFAULT_AMOUNT * BigInt(10);
|
|
if (availableAmt < target) {
|
|
const msg = await this.wasmService.createFaucetMessage();
|
|
if (this.networkSender) this.networkSender("Faucet", msg);
|
|
|
|
let attempts = 3;
|
|
while (attempts > 0) {
|
|
if ((await this.getAmount()) >= target) return;
|
|
attempts--;
|
|
await new Promise((r) =>
|
|
setTimeout(r, APP_CONFIG.TIMEOUTS.RETRY_DELAY)
|
|
);
|
|
}
|
|
throw new Error("Montant insuffisant après faucet");
|
|
}
|
|
}
|
|
|
|
public async createPairingProcess(
|
|
userName: string,
|
|
pairWith: string[]
|
|
): Promise<ApiReturn> {
|
|
if (await this.isPaired()) throw new Error("Déjà appairé");
|
|
const myAddress = await this.getDeviceAddress();
|
|
pairWith.push(myAddress);
|
|
const privateData = { description: "pairing", counter: 0 };
|
|
const publicData = {
|
|
memberPublicName: userName,
|
|
pairedAddresses: pairWith,
|
|
};
|
|
const validation_fields = [
|
|
...Object.keys(privateData),
|
|
...Object.keys(publicData),
|
|
"roles",
|
|
];
|
|
const roles = {
|
|
pairing: {
|
|
members: [],
|
|
validation_rules: [
|
|
{ quorum: 1.0, fields: validation_fields, min_sig_member: 1.0 },
|
|
],
|
|
storages: [APP_CONFIG.URLS.STORAGE],
|
|
},
|
|
};
|
|
return this.createProcess(privateData, publicData, roles);
|
|
}
|
|
|
|
public async createProcess(
|
|
privateData: any,
|
|
publicData: any,
|
|
roles: any,
|
|
feeRate = APP_CONFIG.FEE_RATE
|
|
): Promise<ApiReturn> {
|
|
// Appel au main thread pour avoir l'adresse du relais
|
|
const relay = this.relayGetter ? await this.relayGetter() : "";
|
|
if (!relay) throw new Error("Aucun relais disponible");
|
|
|
|
const { encodedPrivateData, encodedPublicData } =
|
|
await this.prepareProcessData(privateData, publicData);
|
|
const members = this.membersList;
|
|
try {
|
|
return await this.attemptCreateProcess(
|
|
encodedPrivateData,
|
|
roles,
|
|
encodedPublicData,
|
|
relay,
|
|
feeRate,
|
|
members
|
|
);
|
|
} catch (e: any) {
|
|
if (String(e).includes("Insufficient funds")) {
|
|
await this.getTokensFromFaucet();
|
|
return await this.attemptCreateProcess(
|
|
encodedPrivateData,
|
|
roles,
|
|
encodedPublicData,
|
|
relay,
|
|
feeRate,
|
|
members
|
|
);
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private async attemptCreateProcess(
|
|
priv: any,
|
|
roles: any,
|
|
pub: any,
|
|
relay: string,
|
|
fee: number,
|
|
members: any
|
|
): Promise<ApiReturn> {
|
|
const res = await this.wasmService.createNewProcess(priv, roles, pub, relay, fee, members);
|
|
if (res.updated_process) {
|
|
await this.ensureConnections(res.updated_process.current_process);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
public async updateProcess(
|
|
processId: string,
|
|
newData: any,
|
|
privateFields: string[],
|
|
roles: any
|
|
): Promise<ApiReturn> {
|
|
const process = await this.processService.getProcess(processId);
|
|
if (!process) throw new Error("Process not found");
|
|
|
|
let lastState = this.processService.getLastCommitedState(process);
|
|
let currentProcess = process;
|
|
|
|
if (!lastState) {
|
|
const first = process.states[0];
|
|
if (await this.rolesContainsUs(first.roles)) {
|
|
const appRes = await this.approveChange(processId, first.state_id);
|
|
await this.handleApiReturn(appRes);
|
|
const prdRes = await this.createPrdUpdate(processId, first.state_id);
|
|
await this.handleApiReturn(prdRes);
|
|
} else if (first.validation_tokens.length > 0) {
|
|
const res = await this.createPrdUpdate(processId, first.state_id);
|
|
await this.handleApiReturn(res);
|
|
}
|
|
const updated = await this.processService.getProcess(processId);
|
|
if (updated) currentProcess = updated;
|
|
lastState = this.processService.getLastCommitedState(currentProcess);
|
|
if (!lastState) throw new Error("Still no commited state");
|
|
}
|
|
|
|
const lastStateIndex = this.getLastCommitedStateIndex(currentProcess);
|
|
if (lastStateIndex === null) throw new Error("Index commited introuvable");
|
|
|
|
const privateData: any = {};
|
|
const publicData: any = {};
|
|
|
|
for (const field of Object.keys(newData)) {
|
|
if (lastState.public_data[field]) {
|
|
publicData[field] = newData[field];
|
|
continue;
|
|
}
|
|
if (privateFields.includes(field)) {
|
|
privateData[field] = newData[field];
|
|
continue;
|
|
}
|
|
let isPrivate = false;
|
|
for (let i = lastStateIndex; i >= 0; i--) {
|
|
if (currentProcess.states[i].pcd_commitment[field]) {
|
|
privateData[field] = newData[field];
|
|
isPrivate = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!isPrivate) publicData[field] = newData[field];
|
|
}
|
|
|
|
const finalRoles = roles || this.processService.getRoles(currentProcess);
|
|
const { encodedPrivateData, encodedPublicData } =
|
|
await this.prepareProcessData(privateData, publicData);
|
|
|
|
const res = await this.wasmService.updateProcess(
|
|
currentProcess,
|
|
encodedPrivateData,
|
|
encodedPublicData,
|
|
finalRoles,
|
|
this.membersList
|
|
);
|
|
if (res.updated_process)
|
|
await this.ensureConnections(res.updated_process.current_process);
|
|
return res;
|
|
}
|
|
|
|
private async prepareProcessData(priv: any, pub: any) {
|
|
const p1 = this.splitData(priv);
|
|
const p2 = this.splitData(pub);
|
|
return {
|
|
encodedPrivateData: {
|
|
...(await this.wasmService.encodeJson(p1.jsonCompatibleData)),
|
|
...(await this.wasmService.encodeBinary(p1.binaryData)),
|
|
},
|
|
encodedPublicData: {
|
|
...(await this.wasmService.encodeJson(p2.jsonCompatibleData)),
|
|
...(await this.wasmService.encodeBinary(p2.binaryData)),
|
|
},
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// API METHODS (Actions)
|
|
// ==========================================
|
|
public async createPrdUpdate(pid: string, sid: string) {
|
|
const p = await this.getProcess(pid);
|
|
await this.ensureConnections(p!);
|
|
return await this.wasmService.createUpdateMessage(p, sid, this.membersList);
|
|
}
|
|
public async createPrdResponse(pid: string, sid: string) {
|
|
const p = await this.getProcess(pid);
|
|
return await this.wasmService.createPrdResponse(p, sid, this.membersList);
|
|
}
|
|
public async approveChange(pid: string, sid: string) {
|
|
const p = await this.getProcess(pid);
|
|
const res = await this.wasmService.validateState(p, sid, this.membersList);
|
|
if (res.updated_process)
|
|
await this.ensureConnections(res.updated_process.current_process);
|
|
return res;
|
|
}
|
|
public async rejectChange(pid: string, sid: string) {
|
|
const p = await this.getProcess(pid);
|
|
return await this.wasmService.refuseState(p, sid);
|
|
}
|
|
public async requestDataFromPeers(pid: string, sids: string[], roles: any) {
|
|
const res = await this.wasmService.requestData(pid, sids, roles, this.membersList);
|
|
await this.handleApiReturn(res);
|
|
}
|
|
|
|
public async resetDevice() {
|
|
await this.wasmService.resetDevice();
|
|
await this.db.clearMultipleStores([
|
|
"wallet",
|
|
"shared_secrets",
|
|
"unconfirmed_secrets",
|
|
"processes",
|
|
"diffs",
|
|
]);
|
|
}
|
|
|
|
public async handleApiReturn(res: ApiReturn) {
|
|
if (!res || Object.keys(res).length === 0) return;
|
|
try {
|
|
const txData =
|
|
(res.partial_tx ? await this.handlePartialTx(res.partial_tx) : null) ||
|
|
res.new_tx_to_send;
|
|
if (txData && txData.transaction.length != 0) {
|
|
if (this.networkSender)
|
|
this.networkSender("NewTx", JSON.stringify(txData));
|
|
await new Promise((r) => setTimeout(r, APP_CONFIG.TIMEOUTS.API_DELAY));
|
|
}
|
|
if (res.secrets) await this.handleSecrets(res.secrets);
|
|
if (res.updated_process)
|
|
await this.handleUpdatedProcess(res.updated_process);
|
|
if (res.push_to_storage)
|
|
await this.handlePushToStorage(res.push_to_storage);
|
|
|
|
if (res.commit_to_send && this.networkSender)
|
|
this.networkSender("Commit", JSON.stringify(res.commit_to_send));
|
|
if (res.ciphers_to_send && this.networkSender)
|
|
for (const c of res.ciphers_to_send) this.networkSender("Cipher", c);
|
|
} catch (e) {
|
|
console.error("ApiReturn Error:", e);
|
|
}
|
|
}
|
|
|
|
private async handlePartialTx(partialTx: any): Promise<any> {
|
|
try {
|
|
const result = await this.wasmService.signTransaction(partialTx);
|
|
return result.new_tx_to_send;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private async handleSecrets(secrets: any) {
|
|
const { unconfirmed_secrets, shared_secrets } = secrets;
|
|
const unconfirmedList =
|
|
unconfirmed_secrets && unconfirmed_secrets.length > 0
|
|
? unconfirmed_secrets
|
|
: [];
|
|
const sharedList =
|
|
shared_secrets && Object.keys(shared_secrets).length > 0
|
|
? Object.entries(shared_secrets).map(([key, value]) => ({ key, value }))
|
|
: [];
|
|
|
|
if (unconfirmedList.length > 0 || sharedList.length > 0) {
|
|
await this.db.saveSecretsBatch(unconfirmedList, sharedList);
|
|
}
|
|
}
|
|
|
|
private async handleUpdatedProcess(updated: any) {
|
|
const pid = updated.process_id;
|
|
if (updated.encrypted_data) {
|
|
for (const [h, c] of Object.entries(
|
|
updated.encrypted_data as Record<string, string>
|
|
))
|
|
await this.saveBlobToDb(h, this.hexToBlob(c));
|
|
}
|
|
await this.processService.saveProcessToDb(pid, updated.current_process);
|
|
if (updated.diffs) await this.saveDiffsToDb(updated.diffs);
|
|
|
|
this._resolvePendingKeyRequests(pid, updated.current_process);
|
|
// Notification UI
|
|
if (this.notifier) this.notifier("processes-updated");
|
|
}
|
|
|
|
public async saveDiffsToDb(diffs: UserDiff[]) {
|
|
await this.db.saveDiffs(diffs);
|
|
}
|
|
|
|
private _resolvePendingKeyRequests(processId: string, process: Process) {
|
|
if (this.pendingKeyRequests.size === 0) return;
|
|
for (const state of process.states) {
|
|
if (!state.keys) continue;
|
|
for (const [attr, key] of Object.entries(state.keys)) {
|
|
const rid = `${processId}_${state.state_id}_${attr}`;
|
|
if (this.pendingKeyRequests.has(rid)) {
|
|
this.pendingKeyRequests.get(rid)?.(key as string);
|
|
this.pendingKeyRequests.delete(rid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async handlePushToStorage(hashes: string[]) {
|
|
for (const hash of hashes) {
|
|
try {
|
|
const blob = await this.getBlobFromDb(hash);
|
|
const diff = await this.getDiffByValue(hash);
|
|
if (blob && diff)
|
|
await this.saveDataToStorage(diff.storages, hash, blob, null);
|
|
} catch (e) {
|
|
console.error("Push error", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async handleCommitError(response: string) {
|
|
const content = JSON.parse(response);
|
|
const errorMsg = content.error["GenericError"];
|
|
if (
|
|
![
|
|
"State is identical to the previous state",
|
|
"Not enough valid proofs",
|
|
].includes(errorMsg)
|
|
) {
|
|
// Retry via network callback
|
|
if (this.networkSender) {
|
|
setTimeout(
|
|
() => this.networkSender!("Commit", JSON.stringify(content)),
|
|
APP_CONFIG.TIMEOUTS.RETRY_DELAY
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async rolesContainsUs(roles: any) {
|
|
try {
|
|
if (!(await this.isPaired())) {
|
|
return false;
|
|
}
|
|
return this.processService.rolesContainsMember(
|
|
roles,
|
|
await this.getPairingProcessId()
|
|
);
|
|
} catch (e) {
|
|
console.error("RolesContainsUs Error:", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async getSecretForAddress(address: string): Promise<string | null> {
|
|
return await this.db.getSharedSecret(address);
|
|
}
|
|
|
|
public async getAllDiffs(): Promise<Record<string, UserDiff>> {
|
|
return await this.db.getAllDiffs();
|
|
}
|
|
|
|
public async getDiffByValue(value: string): Promise<UserDiff | null> {
|
|
return await this.db.getDiff(value);
|
|
}
|
|
|
|
public async getAllSecrets(): Promise<SecretsStore> {
|
|
return await this.db.getAllSecrets();
|
|
}
|
|
|
|
// ==========================================
|
|
// STORAGE & DB
|
|
// ==========================================
|
|
public async saveBlobToDb(h: string, d: Blob) {
|
|
await this.db.saveBlob(h, d);
|
|
}
|
|
public async getBlobFromDb(h: string) {
|
|
return await this.db.getBlob(h);
|
|
}
|
|
public async fetchValueFromStorage(h: string) {
|
|
return retrieveData([APP_CONFIG.URLS.STORAGE], h);
|
|
}
|
|
public async saveDataToStorage(
|
|
s: string[],
|
|
h: string,
|
|
d: Blob,
|
|
ttl: number | null
|
|
) {
|
|
return storeData(s, h, d, ttl);
|
|
}
|
|
|
|
// ==========================================
|
|
// HELPERS
|
|
// ==========================================
|
|
public getProcessName(p: Process) {
|
|
const pub = this.getPublicData(p);
|
|
if (pub && pub["processName"]) return this.decodeValue(pub["processName"]);
|
|
return null;
|
|
}
|
|
public getPublicData(p: Process) {
|
|
const last = this.getLastCommitedState(p);
|
|
return last ? last.public_data : p.states[0]?.public_data || null;
|
|
}
|
|
public updateMemberPublicName(pid: string, name: string) {
|
|
return this.updateProcess(pid, { memberPublicName: name }, [], null);
|
|
}
|
|
|
|
// ==========================================
|
|
// UI HELPERS
|
|
// ==========================================
|
|
public getNotifications() {
|
|
return this.notifications;
|
|
}
|
|
public setNotifications(n: any[]) {
|
|
this.notifications = n;
|
|
}
|
|
|
|
// ==========================================
|
|
// PARSING & RESEAU ENTRANT
|
|
// ==========================================
|
|
async parseCipher(msg: string) {
|
|
try {
|
|
const res = await this.wasmService.parseCipher(msg, this.membersList, await this.getProcesses());
|
|
await this.handleApiReturn(res);
|
|
} catch (e) {
|
|
console.error("Cipher Error", e);
|
|
}
|
|
}
|
|
|
|
async parseNewTx(msg: string) {
|
|
const parsed = JSON.parse(msg);
|
|
if (parsed.error) return;
|
|
|
|
const prevouts = await this.wasmService.getPrevouts(parsed.transaction);
|
|
for (const p of Object.values(await this.getProcesses())) {
|
|
const tip = p.states[p.states.length - 1].commited_in;
|
|
if (prevouts.includes(tip)) {
|
|
const newTip = await this.wasmService.getTxid(parsed.transaction);
|
|
const newStateId = await this.wasmService.getOpReturn(parsed.transaction);
|
|
await this.wasmService.processCommitNewState(p, newStateId, newTip);
|
|
break;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const res = await this.wasmService.parseNewTx(msg, 0, this.membersList);
|
|
if (
|
|
res &&
|
|
(res.partial_tx ||
|
|
res.new_tx_to_send ||
|
|
res.secrets ||
|
|
res.updated_process)
|
|
) {
|
|
await this.handleApiReturn(res);
|
|
const d = await this.dumpDeviceFromMemory();
|
|
const old = await this.getDeviceFromDatabase();
|
|
if (old && old.pairing_process_commitment)
|
|
d.pairing_process_commitment = old.pairing_process_commitment;
|
|
await this.saveDeviceInDatabase(d);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
// ==========================================
|
|
// BACKUP & RESTORE
|
|
// ==========================================
|
|
public async importJSON(backup: BackUp) {
|
|
await this.resetDevice();
|
|
await this.walletService.saveDeviceInDatabase(backup.device);
|
|
await this.walletService.restoreDevice(backup.device);
|
|
await this.processService.batchSaveProcesses(backup.processes);
|
|
await this.restoreSecretsFromBackUp(backup.secrets);
|
|
}
|
|
public async restoreSecretsFromBackUp(secretsStore: SecretsStore) {
|
|
const sharedList = Object.entries(secretsStore.shared_secrets).map(
|
|
([key, value]) => ({ key, value })
|
|
);
|
|
await this.db.saveSecretsBatch(
|
|
secretsStore.unconfirmed_secrets,
|
|
sharedList
|
|
);
|
|
await this.restoreSecretsFromDB();
|
|
}
|
|
public async restoreSecretsFromDB() {
|
|
const secretsStore = await this.db.getAllSecrets();
|
|
await this.wasmService.setSharedSecrets(JSON.stringify(secretsStore));
|
|
console.log("[CoreWorker] 🔐 Secrets restaurés depuis la DB");
|
|
}
|
|
public async createBackUp() {
|
|
const device = await this.walletService.getDeviceFromDatabase();
|
|
if (!device) return null;
|
|
return {
|
|
device,
|
|
processes: await this.processService.getProcesses(),
|
|
secrets: await this.getAllSecrets(),
|
|
};
|
|
}
|
|
|
|
// ==========================================
|
|
// DECRYPT ATTRIBUTE
|
|
// ==========================================
|
|
public async decryptAttribute(
|
|
processId: string,
|
|
state: ProcessState,
|
|
attribute: string
|
|
): Promise<any | null> {
|
|
console.groupCollapsed(
|
|
`[CoreWorker] 🔑 Déchiffrement de '${attribute}' (Process: ${processId})`
|
|
);
|
|
|
|
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 key: string | null | undefined = state.keys[attribute];
|
|
const pairingProcessId = await this.getPairingProcessId();
|
|
|
|
if (!hash) {
|
|
console.warn(`⚠️ L'attribut n'existe pas (pas de hash).`);
|
|
return null;
|
|
}
|
|
|
|
if (!key) {
|
|
if (!this._checkAccess(state, attribute, pairingProcessId)) {
|
|
console.log(`⛔ Accès non autorisé. Abandon.`);
|
|
return null;
|
|
}
|
|
const result = await this._fetchMissingKey(processId, state, attribute);
|
|
hash = result.hash;
|
|
key = result.key;
|
|
}
|
|
|
|
if (hash && key) {
|
|
const blob = await this.getBlobFromDb(hash);
|
|
if (!blob) {
|
|
console.error(
|
|
`💥 Échec: Blob non trouvé en BDD pour le hash ${hash}`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const buf = await blob.arrayBuffer();
|
|
const cipher = new Uint8Array(buf);
|
|
const keyUIntArray = this.hexToUInt8Array(key);
|
|
|
|
const clear = await this.wasmService.decryptData(keyUIntArray, cipher);
|
|
if (!clear) throw new Error("decrypt_data returned null");
|
|
|
|
const decoded = await this.wasmService.decodeValue(Array.from(clear));
|
|
console.log(`✅ Attribut '${attribute}' déchiffré avec succès.`);
|
|
return decoded;
|
|
} catch (e) {
|
|
console.error(`💥 Échec du déchiffrement: ${e}`);
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error(`💥 Erreur:`, error);
|
|
return null;
|
|
} finally {
|
|
console.groupEnd();
|
|
}
|
|
}
|
|
|
|
private _checkAccess(
|
|
state: ProcessState,
|
|
attribute: string,
|
|
pairingProcessId: string
|
|
): boolean {
|
|
const roles = state.roles;
|
|
return Object.values(roles).some((role) => {
|
|
const isMember = role.members.includes(pairingProcessId);
|
|
if (!isMember) return false;
|
|
return Object.values(role.validation_rules).some((rule) =>
|
|
rule.fields.includes(attribute)
|
|
);
|
|
});
|
|
}
|
|
|
|
private async _fetchMissingKey(
|
|
processId: string,
|
|
state: ProcessState,
|
|
attribute: string
|
|
): Promise<{ hash: string | null; key: string | null }> {
|
|
try {
|
|
const process = await this.getProcess(processId);
|
|
if (!process) return { hash: null, key: null };
|
|
|
|
await this.ensureConnections(process);
|
|
await this.requestDataFromPeers(
|
|
processId,
|
|
[state.state_id],
|
|
[state.roles]
|
|
);
|
|
|
|
const requestId = `${processId}_${state.state_id}_${attribute}`;
|
|
const keyRequestPromise = new Promise<string>((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
this.pendingKeyRequests.delete(requestId);
|
|
reject(new Error(`Timeout waiting for key: ${attribute}`));
|
|
}, APP_CONFIG.TIMEOUTS.KEY_REQUEST);
|
|
|
|
this.pendingKeyRequests.set(requestId, (key: string) => {
|
|
clearTimeout(timeout);
|
|
resolve(key);
|
|
});
|
|
});
|
|
|
|
const receivedKey = await keyRequestPromise;
|
|
const updatedProcess = await this.getProcess(processId);
|
|
if (!updatedProcess) return { hash: null, key: null };
|
|
|
|
const updatedState = this.getStateFromId(updatedProcess, state.state_id);
|
|
const updatedHash = updatedState
|
|
? updatedState.pcd_commitment[attribute]
|
|
: state.pcd_commitment[attribute];
|
|
|
|
return { hash: updatedHash, key: receivedKey };
|
|
} catch (e) {
|
|
return { hash: null, key: null };
|
|
}
|
|
}
|
|
}
|
|
|
|
Comlink.expose(new CoreBackend());
|