// import { WebSocketClient } from '../websockets'; import { INotification } from '~/models/notification.model'; import { IProcess } from '~/models/process.model'; // import Database from './database'; import { initWebsocket, sendMessage } from '../websockets'; import { ApiReturn, Member, Process, RoleDefinition, UserDiff } from '../../pkg/sdk_client'; import ModalService from './modal.service'; import Database from './database.service'; import { storeData, retrieveData } from './storage.service'; export const U32_MAX = 4294967295; const storageUrl = `http://localhost:8080`; const BOOTSTRAPURL = [`http://localhost:8090`]; export default class Services { private static initializing: Promise | null = null; private static instance: Services; private currentProcess: string | null = null; private pendingUpdates: any | null = null; private currentUpdateMerkleRoot: string | null = null; private localAddress: string | null = null; private pairedAddresses: string[] = []; private sdkClient: any; private processes: IProcess[] | null = null; private notifications: any[] | null = null; private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; private database: any; private routingInstance!: ModalService; private relayAddresses: { [wsurl: string]: string } = {}; // Private constructor to prevent direct instantiation from outside private constructor() {} // Method to access the singleton instance of Services public static async getInstance(): Promise { if (Services.instance) { return Services.instance; } if (!Services.initializing) { Services.initializing = (async () => { const instance = new Services(); await instance.init(); instance.routingInstance = await ModalService.getInstance(); return instance; })(); } console.log('initializing services'); Services.instance = await Services.initializing; Services.initializing = null; // Reset for potential future use return Services.instance; } public async init(): Promise { this.notifications = this.getNotifications(); this.sdkClient = await import('../../pkg/sdk_client'); this.sdkClient.setup(); for (const wsurl of Object.values(BOOTSTRAPURL)) { this.updateRelay(wsurl, ''); } await this.connectAllRelays(); } /** * Calls `this.addWebsocketConnection` for each `wsurl` in relayAddresses. */ public async connectAllRelays(): Promise { for (const wsurl of Object.keys(this.relayAddresses)) { try { console.log(`Connecting to: ${wsurl}`); await this.addWebsocketConnection(wsurl); console.log(`Successfully connected to: ${wsurl}`); } catch (error) { console.error(`Failed to connect to ${wsurl}:`, error); } } } public async addWebsocketConnection(url: string): Promise { console.log('Opening new websocket connection'); await initWebsocket(url); } /** * Add or update a key/value pair in relayAddresses. * @param wsurl - The WebSocket URL (key). * @param spAddress - The SP Address (value). */ public updateRelay(wsurl: string, spAddress: string): void { this.relayAddresses[wsurl] = spAddress; console.log(`Updated: ${wsurl} -> ${spAddress}`); } /** * Retrieve the spAddress for a given wsurl. * @param wsurl - The WebSocket URL to look up. * @returns The SP Address if found, or undefined if not. */ public getSpAddress(wsurl: string): string | undefined { return this.relayAddresses[wsurl]; } /** * Get all key/value pairs from relayAddresses. * @returns An array of objects containing wsurl and spAddress. */ public getAllRelays(): { wsurl: string; spAddress: string }[] { return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({ wsurl, spAddress, })); } /** * Print all key/value pairs for debugging. */ public printAllRelays(): void { console.log("Current relay addresses:"); for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) { console.log(`${wsurl} -> ${spAddress}`); } } public isPaired(): boolean { try { return this.sdkClient.is_paired(); } catch (e) { throw new Error(`isPaired ~ Error: ${e}`); } } public async unpairDevice(): Promise { try { this.sdkClient.unpair_device(); const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); } catch (e) { throw new Error(`Failed to unpair device: ${e}`); } } public async getSecretForAddress(address: string): Promise { const db = await Database.getInstance(); return await db.getObject('shared_secrets', address); } public async connectMember(members: Member[]): Promise { if (members.length === 0) { throw new Error('Trying to connect to empty members list'); } const members_str = members.map((member) => JSON.stringify(member)); const waitForAmount = async (): Promise => { let attempts = 3; while (attempts > 0) { const amount = this.getAmount(); if (amount !== 0n) { return amount; } attempts--; if (attempts > 0) { await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second } } throw new Error('Amount is still 0 after 3 attempts'); }; let availableAmt = this.getAmount(); if (availableAmt === 0n) { const faucetMsg = this.createFaucetMessage(); this.sendFaucetMessage(faucetMsg); try { availableAmt = await waitForAmount(); } catch (e) { console.error('Failed to retrieve amount:', e); throw e; // Rethrow the error if needed } } try { return this.sdkClient.create_connect_transaction(members_str, 1); } catch (e) { throw e; } } public async createPairingProcess(pairWith: string[], relayAddress: string, feeRate: number): Promise { if (this.sdkClient.is_paired()) { throw new Error('Device already paired'); } const myAddress: string = this.sdkClient.get_address(); pairWith.push(myAddress); const newKey = this.sdkClient.get_new_keypair(); const pairingTemplate = { description: 'pairing', roles: { owner: { members: [{ sp_addresses: pairWith }], validation_rules: [ { quorum: 1.0, fields: ['description', 'roles', 'session_privkey', 'session_pubkey', 'key_parity'], min_sig_member: 1.0, }, ], storages: [storageUrl] }, }, session_privkey: newKey['private_key'], session_pubkey: newKey['x_only_public_key'], key_parity: newKey['key_parity'], }; try { return this.sdkClient.create_new_process(JSON.stringify(pairingTemplate), null, relayAddress, feeRate); } catch (e) { throw new Error(`Creating process failed:, ${e}`); } } // Create prd update for current process and update public createPrdUpdate(pcdMerkleRoot: string): ApiReturn { if (!this.currentProcess) { throw new Error('No current process defined'); } try { return this.sdkClient.create_update_message(this.currentProcess, pcdMerkleRoot); } catch (e) { throw new Error(`Failed to create prd update: ${e}`); } } public createPrdResponse(pcdMerkleRoot: string): ApiReturn { if (!this.currentProcess) { throw new Error('No current process defined'); } try { return this.sdkClient.create_response_prd(this.currentProcess, pcdMerkleRoot); } catch (e) { throw e; } } public approveChange(currentPcdMerkleRoot: string): ApiReturn { if (!this.currentProcess) { throw new Error('No current process defined'); } try { return this.sdkClient.validate_state(this.currentProcess, currentPcdMerkleRoot); } catch (e) { throw new Error(`Failed to create prd response: ${e}`); } } public rejectChange(): ApiReturn { if (!this.currentProcess || !this.currentUpdateMerkleRoot) { throw new Error('No current process and/or current update defined'); } try { return this.sdkClient.refuse_state(this.currentProcess, this.currentUpdateMerkleRoot); } catch (e) { throw new Error(`Failed to create prd response: ${e}`); } } async resetDevice() { await this.sdkClient.reset_device(); } async sendNewTxMessage(message: string) { sendMessage('NewTx', message); } async sendCommitMessage(message: string) { sendMessage('Commit', message); } async sendCipherMessages(ciphers: string[]) { for (let i = 0; i < ciphers.length; i++) { const cipher = ciphers[i]; sendMessage('Cipher', cipher); } } sendFaucetMessage(message: string): void { sendMessage('Faucet', message); } async parseCipher(message: string) { try { // console.log('parsing new cipher'); const apiReturn = this.sdkClient.parse_cipher(message); console.log('🚀 ~ Services ~ parseCipher ~ apiReturn:', apiReturn); await this.handleApiReturn(apiReturn); } catch (e) { console.error(`Parsed cipher with error: ${e}`); } // await this.saveCipherTxToDb(parsedTx) } async parseNewTx(tx: string) { try { const parsedTx = this.sdkClient.parse_new_tx(tx, 0); if (parsedTx) { try { await this.handleApiReturn(parsedTx); const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); } catch (e) { console.error('Failed to update device with new tx'); } } } catch (e) { console.trace(e); } } public async handleApiReturn(apiReturn: ApiReturn) { if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) { await this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)); } if (apiReturn.secrets) { const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets; const confirmedSecrets = apiReturn.secrets.shared_secrets; const db = await Database.getInstance(); for (const secret of unconfirmedSecrets) { await db.addObject({ storeName: 'unconfirmed_secrets', object: secret, key: null, }); } const entries = Object.entries(confirmedSecrets).map(([key, value]) => ({ key, value })); for (const entry of entries) { try { await db.addObject({ storeName: 'shared_secrets', object: entry.value, key: entry.key, }); } catch (e) { throw e; } // We don't want to throw an error, it could simply be that we registered directly the shared secret // this.removeUnconfirmedSecret(entry.value); } } setTimeout(async () => { if (apiReturn.updated_process) { const updatedProcess = apiReturn.updated_process; // Save process to storage try { await this.saveProcess(updatedProcess.commitment_tx, updatedProcess.current_process); } catch (e) { throw e; } if (updatedProcess.new_diffs.length != 0) { try { this.saveDiffs(updatedProcess.new_diffs); } catch (e) { throw e; } } if (updatedProcess.modified_state) { // For now it can only mean we added a validation token to an existing state // Not sure what action to take } } if (apiReturn.commit_to_send) { const commit = apiReturn.commit_to_send; await this.sendCommitMessage(JSON.stringify(commit)); } if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) { await this.sendCipherMessages(apiReturn.ciphers_to_send); } }, 0); } public async evaluatePendingUpdates() { if (!this.currentProcess) { throw new Error('No current process'); } try { await this.openConfirmationModal(); } catch (e) { throw new Error(`Error while evaluating pending updates for process ${this.currentProcess}: ${e}`); } } private async openConfirmationModal() { if (!this.pendingUpdates || this.pendingUpdates.modified_values.length === 0) { console.log('No pending updates to validate'); } for (const value of this.pendingUpdates!.modified_values) { if (value.notify_user) { // TODO notification pop up } if (!value.need_validation) { continue; } if (value.proof) { // It seems we already validated that, check the proof and if valid just notify user continue; } const actualProposal: Record = JSON.parse(value.new_value); const merkleRoot: string = value.new_state_merkle_root; try { await this.routingInstance.openPairingConfirmationModal(actualProposal, this.currentProcess!, merkleRoot); } catch (e) { throw new Error(`${e}`); } } } pairDevice(spAddressList: string[]) { if (this.currentProcess) { try { this.sdkClient.pair_device(this.currentProcess, spAddressList); } catch (e) { throw new Error(`Failed to pair device: ${e}`); } } } public getAmount(): BigInt { const amount = this.sdkClient.get_available_amount(); return amount; } async getDeviceAddress() { return await this.sdkClient.get_address(); } public dumpDeviceFromMemory(): string { try { return this.sdkClient.dump_device(); } catch (e) { throw new Error(`Failed to dump device: ${e}`); } } async saveDeviceInDatabase(device: any): Promise { const db = await Database.getInstance(); const walletStore = 'wallet'; try { const prevDevice = await this.getDeviceFromDatabase(); if (prevDevice) { await db.deleteObject(walletStore, "1"); } await db.addObject({ storeName: walletStore, object: { pre_id: '1', device }, key: null, }); } catch (e) { console.error(e); } } async getDeviceFromDatabase(): Promise { const db = await Database.getInstance(); const walletStore = 'wallet'; try { const dbRes = await db.getObject(walletStore, '1'); if (dbRes) { const wallet = dbRes['device']; return wallet; } else { return null; } } catch (e) { throw new Error(`Failed to retrieve device from db: ${e}`); } } async dumpWallet() { const wallet = await this.sdkClient.dump_wallet(); console.log('🚀 ~ Services ~ dumpWallet ~ wallet:', wallet); return wallet; } public createFaucetMessage() { const message = this.sdkClient.create_faucet_msg(); console.log('🚀 ~ Services ~ createFaucetMessage ~ message:', message); return message; } async createNewDevice() { let spAddress = ''; try { spAddress = await this.sdkClient.create_new_device(0, 'regtest'); const device = this.dumpDeviceFromMemory(); console.log('🚀 ~ Services ~ device:', device); await this.saveDeviceInDatabase(device); } catch (e) { console.error('Services ~ Error:', e); } return spAddress; } async restoreDevice(device: string) { try { await this.sdkClient.restore_device(device); const spAddress = this.sdkClient.get_address(); } catch (e) { console.error(e); } } public async saveProcess(commitedIn: string, process: Process) { const db = await Database.getInstance(); try { await db.addObject({ storeName: 'processes', object: process, key: commitedIn, }); } catch (e) { throw new Error(`Failed to save process: ${e}`); } // We check how many copies in storage nodes // We check the storage nodes in the process itself // this.sdkClient.get_storages(commitedIn); const storages = [storageUrl]; for (const state of process.states) { if (state.merkle_root === "") { continue; } if (!state.encrypted_pcd) { console.warn('Empty encrypted pcd, skipping...'); continue; } for (const [field, hash] of Object.entries(state.pcd_commitment)) { // get the encrypted value with the field name const value = state.encrypted_pcd[field]; await storeData(storages, hash, value, null); } } } public async fetchValueFromStorage(hash: string): Promise { const storages = [storageUrl]; return await retrieveData(storages, hash); } public async saveDiffs(diffs: UserDiff[]) { const db = await Database.getInstance(); try { for (const diff of diffs) { await db.addObject({ storeName: 'diffs', object: diff, key: null, }); } } catch (e) { throw new Error(`Failed to save process: ${e}`); } } public async getProcess(commitedIn: string): Promise { const db = await Database.getInstance(); return await db.getObject('processes', commitedIn); } public async getProcesses(): Promise> { const db = await Database.getInstance(); const processes: Record = await db.dumpStore('processes'); return processes; } // Restore process in wasm with persistent storage public async restoreProcesses() { const db = await Database.getInstance(); try { const processes: Record = await db.dumpStore('processes'); if (processes && Object.keys(processes).length != 0) { console.log(`Restoring ${Object.keys(processes).length} processes`); this.sdkClient.set_process_cache(JSON.stringify(processes)); } else { console.log('No processes to restore!'); } } catch (e) { throw e; } } public async restoreSecrets() { const db = await Database.getInstance(); try { const sharedSecrets: Record = await db.dumpStore('shared_secrets'); const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); const secretsStore = { shared_secrets: sharedSecrets, unconfirmed_secrets: Object.values(unconfirmedSecrets), }; this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore)); } catch (e) { throw e; } } getNotifications(): any[] | null { // return [ // { // id: 1, // title: 'Notif 1', // description: 'A normal notification', // sendToNotificationPage: false, // path: '/notif1', // }, // { // id: 2, // title: 'Notif 2', // description: 'A normal notification', // sendToNotificationPage: false, // path: '/notif2', // }, // { // id: 3, // title: 'Notif 3', // description: 'A normal notification', // sendToNotificationPage: false, // path: '/notif3', // }, // ]; return this.notifications; } setNotifications(notifications: any[]) { this.notifications = notifications; } async importJSON(content: any): Promise { return Promise.resolve(); } }