// 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 } from '../../dist/pkg/sdk_client'; import ModalService from './modal.service'; import { navigate } from '../router'; import Database from './database.service'; type ProcessesCache = { [key: string]: any; }; export const U32_MAX = 4294967295; const wsurl = `https://demo.4nkweb.com/ws/`; export default class Services { private static initializing: Promise | null = null; private static instance: Services; private current_process: string | null = null; private sdkClient: any; private processes: IProcess[] | null = null; private notifications: INotification[] | null = null; private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; private database: any; private routingInstance!: ModalService; // 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('../../dist/pkg/sdk_client'); this.sdkClient.setup(); await this.addWebsocketConnection(wsurl); } public async addWebsocketConnection(url: string): Promise { // const services = await Services.getInstance(); console.log('Opening new websocket connection'); const newClient = initWebsocket(url); } public isPaired(): boolean | undefined { try { return this.sdkClient.is_linking(); } catch (e) { console.error('isPaired ~ Error:', e); } } public async unpairDevice(): Promise { const service = await Services.getInstance(); await service.sdkClient.unpair_device(); const newDevice = await this.dumpDevice(); await this.saveDevice(newDevice); } private prepareProcessTx(myAddress: string, recipientAddress: string) { const initial_session_privkey = new Uint8Array(32); const initial_session_pubkey = new Uint8Array(32); const pairingTemplate = { description: 'AliceBob', roles: { owner: { members: [{ sp_addresses: [myAddress, recipientAddress] }], validation_rules: [ { quorum: 1.0, fields: ['description', 'roles', 'session_privkey', 'session_pubkey', 'key_parity'], min_sig_member: 1.0, }, ], }, }, session_privkey: initial_session_privkey, session_pubkey: initial_session_pubkey, key_parity: true, }; const apiReturn = this.sdkClient.create_update_transaction(undefined, JSON.stringify(pairingTemplate), 1); return apiReturn; } public async sendPairingTx(spAddress: string): Promise { const localAddress = this.sdkClient.get_address(); const emptyTxid = '0'.repeat(64); try { let commitmentOutpoint = `${emptyTxid}:${U32_MAX}`; this.sdkClient.pair_device(commitmentOutpoint, [spAddress]); } catch (e) { console.error('Services ~ Error:', e); return; } setTimeout(async () => { const apiReturn = this.prepareProcessTx(localAddress, spAddress); await this.handleApiReturn(apiReturn); }, 1500); return; } async resetDevice() { await this.sdkClient.reset_device(); } async sendNewTxMessage(message: string) { await sendMessage('NewTx', message); } async sendCommitMessage(message: string) { await sendMessage('Commit', message); } async sendCipherMessages(ciphers: string[]) { for (let i = 0; i < ciphers.length; i++) { const cipher = ciphers[i]; await sendMessage('Cipher', cipher); } } async sendFaucetMessage(message: string): Promise { await sendMessage('Faucet', message); } async parseCipher(message: string) { try { console.log('parsing new cipher'); const apiReturn = await this.sdkClient.parse_cipher(message, 0.00001); console.log('πŸš€ ~ Services ~ parseCipher ~ apiReturn:', apiReturn); await this.handleApiReturn(apiReturn); } catch (e) { console.log("Cipher isn't for us"); } // await this.saveCipherTxToDb(parsedTx) } async parseNewTx(tx: string) { try { const parsedTx = await this.sdkClient.parse_new_tx(tx, 0, 0.0001); if (parsedTx) { console.log('πŸš€ ~ Services ~ parseNewTx ~ parsedTx:', parsedTx); try { await this.handleApiReturn(parsedTx); const newDevice = await this.dumpDevice(); await this.saveDevice(newDevice); } catch (e) { console.error('Failed to update device with new tx'); } } } catch (e) { console.trace(e); } } private async handleApiReturn(apiReturn: ApiReturn) { if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length) { await this.sendCipherMessages(apiReturn.ciphers_to_send); } setTimeout(async () => { if (apiReturn.updated_process && apiReturn.updated_process.length) { const [processCommitment, process] = apiReturn.updated_process; console.debug('Updated Process Commitment:', processCommitment); console.debug('Process Details:', process); // Save process to storage localStorage.setItem(processCommitment, JSON.stringify(process)); const db = await Database.getInstance(); db.addObject({ storeName: 'process', object: { id: processCommitment, process }, key: processCommitment, }); // Check if the newly updated process reveals some new information try { const proposals: string[] = this.sdkClient.get_update_proposals(processCommitment); if (proposals && proposals.length != 0) { const actual_proposal = JSON.parse(proposals[0]); // We just don't acknowledge concurrent proposals for now console.info(actual_proposal); await this.routingInstance.openConfirmationModal(actual_proposal, processCommitment); } } catch (e) { console.error(e); } } if (apiReturn.updated_cached_msg && apiReturn.updated_cached_msg.length) { apiReturn.updated_cached_msg.forEach(async (msg, index) => { // console.debug(`CachedMessage ${index}:`, msg); // Save the message to local storage localStorage.setItem(msg.id.toString(), JSON.stringify(msg)); const db = await Database.getInstance(); db.addObject({ storeName: 'messages', object: { id: msg.id.toString(), msg }, key: msg.id.toString(), }); }); } if (apiReturn.commit_to_send) { const commit = apiReturn.commit_to_send; await this.sendCommitMessage(JSON.stringify(commit)); } if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) { await this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)); } }, 0); } async pairDevice(commitmentTx: string, spAddressList: string[]) { await this.sdkClient.pair_device(commitmentTx, spAddressList); } async validatePairingDevice(prd: any, outpointCommitment: string) { console.log('πŸš€ ~ Services ~ pairDevice ~ prd:', prd); const spAddress = (await this.getDeviceAddress()) as any; const sender = JSON.parse(prd?.sender); const senderAddress = sender?.sp_addresses?.find((address: string) => address !== spAddress); console.log('πŸš€ ~ Services ~ pairDevice ~ senderAddress:', senderAddress); if (senderAddress) { const proposal = this.sdkClient.get_update_proposals(outpointCommitment); console.log('πŸš€ ~ Services ~ pairDevice ~ proposal:', proposal); // const pairingTx = proposal.pairing_tx.replace(/^\"+|\"+$/g, '') const parsedProposal = JSON.parse(proposal[0]); console.log('πŸš€ ~ Services ~ pairDevice ~ parsedProposal:', parsedProposal); const roles = JSON.parse(parsedProposal.roles); console.log('πŸš€ ~ Services ~ pairDevice ~ roles:', roles, Array.isArray(roles), !roles.owner); if (Array.isArray(roles) || !roles.owner) return; const proposalMembers = roles?.owner?.members; const isFirstDevice = proposalMembers.some((member: Member) => member.sp_addresses.some((address) => address === spAddress)); const isSecondDevice = proposalMembers.some((member: Member) => member.sp_addresses.some((address) => address === senderAddress)); console.log('πŸš€ ~ Services ~ pairDevice ~ proposalMembers:', proposalMembers); if (proposalMembers?.length !== 2 || !isFirstDevice || !isSecondDevice) return; const pairingTx = parsedProposal?.pairing_tx?.replace(/^\"+|\"+$/g, ''); let txid = '0'.repeat(64); console.log('πŸš€ ~ Services ~ pairDevice ~ pairingTx:', pairingTx, `${txid}:4294967295`); const pairing = await this.sdkClient.pair_device(`${txid}:4294967295`, [senderAddress]); const device = this.dumpDevice(); console.log('πŸš€ ~ Services ~ pairDevice ~ device:', device); this.saveDevice(device); // await service.sdkClient.pair_device(pairingTx, [senderAddress]) console.log('πŸš€ ~ Services ~ pairDevice ~ pairing:', pairing); // const process = await this.prepareProcessTx(spAddress, senderAddress) console.log('πŸš€ ~ Services ~ pairDevice ~ process:', outpointCommitment, prd, prd.payload); const prdString = JSON.stringify(prd).trim(); console.log('πŸš€ ~ Services ~ pairDevice ~ prdString:', prdString); let tx = await this.sdkClient.response_prd(outpointCommitment, prdString, true); console.log('πŸš€ ~ Services ~ pairDevice ~ tx:', tx); if (tx.ciphers_to_send) { tx.ciphers_to_send.forEach((cipher: string) => sendMessage('Cipher', cipher)); } navigate('process'); } } async getAmount(): Promise { const amount = await this.sdkClient.get_available_amount(); return amount; } async getDeviceAddress() { return await this.sdkClient.get_address(); } async dumpDevice() { const device = await this.sdkClient.dump_device(); return device; } async saveDevice(device: any): Promise { const db = await Database.getInstance(); db.addObject({ storeName: 'wallet', object: { pre_id: '1', device }, key: '1', }); localStorage.setItem('wallet', device); } async getDevice(): Promise { const db = await Database.getInstance(); db.getObject('wallet', '1'); return localStorage.getItem('wallet'); } async dumpWallet() { const wallet = await this.sdkClient.dump_wallet(); console.log('πŸš€ ~ Services ~ dumpWallet ~ wallet:', wallet); return wallet; } async createFaucetMessage() { const message = await 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 = await this.dumpDevice(); console.log('πŸš€ ~ Services ~ device:', device); await this.saveDevice(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); } } async getProcesses(): Promise { const process = [ [ '1', { title: 'Messaging', description: 'Messagerie chiffrΓ©e', html: '
', css: '', script: '', zones: [ { id: '1', title: 'zone 1', description: 'zone 1', }, { id: '2', title: 'zone 2', description: 'zone 2', }, ], roles: { owner: { members: ['dfdsfdfdsf', 'dfdfdfdsfsfdfdsf'], validation_rules: [ { quorum: 1.0, fields: ['description', 'roles', 'session_privkey', 'session_pubkey', 'key_parity'], min_sig_member: 1.0, }, ], }, }, }, ], [ '2', { title: 'Database', description: 'Database chiffrΓ©e', html: '
', css: '', script: '', zones: [ { id: '1', title: 'zone 1', description: 'zone 1', }, { id: '2', title: 'zone 2', description: 'zone 2', }, ], roles: { owner: { members: ['dfdsfdfdsf', 'dfdfdfdsfsfdfdsf'], validation_rules: [ { quorum: 1.0, fields: ['description', 'roles', 'session_privkey', 'session_pubkey', 'key_parity'], min_sig_member: 1.0, }, ], }, }, }, ], ]; return process; } private getProcessesCache(): ProcessesCache { // Regular expression to match 64-character hexadecimal strings const hexU32KeyRegex: RegExp = /^[0-9a-fA-F]{64}:\d+$/; const hexObjects: ProcessesCache = {}; // Iterate over all keys in localStorage for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (!key) { return hexObjects; } // Check if the key matches the 32-byte hex pattern if (hexU32KeyRegex.test(key)) { const value = localStorage.getItem(key); if (!value) { continue; } hexObjects[key] = JSON.parse(value); } } return hexObjects; } private getCachedMessages(): string[] { const u32KeyRegex = /^\d+$/; const U32_MAX = 4294967295; const messages: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (!key) { return messages; } if (u32KeyRegex.test(key)) { const num = parseInt(key, 10); if (num < 0 || num > U32_MAX) { console.warn(`Key ${key} is outside the u32 range and will be ignored.`); continue; } const value = localStorage.getItem(key); if (!value) { console.warn(`No value found for key: ${key}`); continue; } messages.push(value); } } return messages; } public async restoreProcesses() { const processesCache = this.getProcessesCache(); console.log('πŸš€ ~ Services ~ restoreProcesses ~ processesCache:', processesCache); if (processesCache.length == 0) { console.debug('πŸš€ ~ Services ~ restoreProcesses ~ no processes in local storage'); return; } try { await this.sdkClient.set_process_cache(JSON.stringify(processesCache)); } catch (e) { console.error('Services ~ restoreProcesses ~ Error:', e); } } public async restoreMessages() { const cachedMessages = this.getCachedMessages(); console.log('πŸš€ ~ Services ~ restoreMessages ~ chachedMessages:', cachedMessages); if (cachedMessages && cachedMessages.length != 0) { try { await this.sdkClient.set_message_cache(cachedMessages); } catch (e) { console.error('Services ~ restoreMessages ~ Error:', e); } } } getNotifications(): INotification[] { 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', }, ]; } }