// import { WebSocketClient } from '../websockets'; import { INotification } from '~/models/notification.model'; import homePage from '../html/home.html?raw'; import homeScript from '../html/home.js?raw'; import processPage from '../html/process.html?raw'; import processScript from '../html/process.js?raw'; import { IProcess } from '~/models/process.model'; // import Database from './database'; import { WebSocketClient } from '../websockets'; import QRCode from 'qrcode' import { servicesVersion } from 'typescript'; import { ApiReturn, CachedMessage, Member } from '../../dist/pkg/sdk_client'; import Routing from './routing.service'; type ProcessesCache = { [key: string]: any; }; 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 websocketConnection: WebSocketClient | null = null; private processes: IProcess[] | null = null; private notifications: INotification[] | null = null; private subscriptions: {element: Element; event: string; eventHandler: string;}[] = [] ; private database: any // 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(); 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); await this.recoverInjectHtml(); } public async addWebsocketConnection(url: string): Promise { // const services = await Services.getInstance(); if (!this.websocketConnection) { console.log('Opening new websocket connection'); const newClient = new WebSocketClient(url, this); this.websocketConnection = newClient; } } public async recoverInjectHtml(): Promise { const container = document.getElementById('containerId'); if (!container) { console.error("No html container"); return; } container.innerHTML = homePage; const newScript = document.createElement('script') newScript.setAttribute('type', 'module') newScript.textContent = homeScript; document.head.appendChild(newScript).parentNode?.removeChild(newScript); const btn = container.querySelector('#scan-this-device') if(btn) { this.addSubscription(btn, 'click', 'injectProcessListPage') } } private generateQRCode = async (text: string) => { console.log("🚀 ~ Services ~ generateQRCode= ~ text:", text) try { const container = document.getElementById('containerId'); const currentUrl = window.location.href; const url = await QRCode.toDataURL(currentUrl + "?sp_address=" + text); const qrCode = container?.querySelector('.qr-code img'); qrCode?.setAttribute('src', url) } catch (err) { console.error(err); } } 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 amount = this.sdkClient.get_available_amount(); if (amount === 0n) { const faucetMsg = this.sdkClient.create_faucet_msg(); await this.sendFaucetMessage(faucetMsg); } 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); }, 100) return } async resetDevice() { await this.sdkClient.reset_device() } async sendNewTxMessage(message: string) { if (!this.websocketConnection) { throw new Error('No websocket connection'); } // console.log("🚀 ~ WebSocketClient ~ this.ws.onopen= ~ newTxMessage:", message) await this.websocketConnection.sendMessage('NewTx', message) } async sendCommitMessage(message: string) { // console.log("🚀 ~ WebSocketClient ~ this.ws.onopen= ~ CommitMessage:", message) await this.websocketConnection?.sendMessage('Commit', message) } async sendCipherMessages(ciphers: string[]) { for (let i = 0; i < ciphers.length; i++) { const cipher = ciphers[i]; await this.websocketConnection?.sendMessage('Cipher', cipher); } } async sendFaucetMessage(message: string): Promise { // console.log("🚀 ~ WebSocketClient ~ this.ws.onopen= ~ faucetMessage:", message) await this.websocketConnection?.sendMessage('Faucet', message); } async parseCipher(message: string) { // try { // JSON.parse(message) // const router = await Routing.getInstance(); // router.closeLoginModal() // this.injectProcessListPage() // } catch { // console.log('Not proper format for cipher') // } 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 { // console.log('==============> sending txxxxxxx parser', tx) 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)); // Check if the newly updated process reveals some new information try { const proposals: string[] = this.sdkClient.get_update_proposals(processCommitment); const actual_proposal = JSON.parse(proposals[0]); // just hacky way to solve redundant info for now console.info(actual_proposal); let router = await Routing.getInstance(); router.openConfirmationModal(actual_proposal, processCommitment); } catch (e) { console.error(e); } } if(apiReturn.updated_cached_msg && apiReturn.updated_cached_msg.length) { apiReturn.updated_cached_msg.forEach((msg, index) => { console.debug(`CachedMessage ${index}:`, msg); // Save the message to local storage localStorage.setItem(msg.id.toString(), JSON.stringify(msg)); }); } if (apiReturn.commit_to_send) { const commit = apiReturn.commit_to_send; await this.sendCommitMessage(JSON.stringify(commit)); } if (apiReturn.new_tx_to_send) { await this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)) } }, 0) } async pairDevice(prd: any, outpointCommitment: string) { console.log("🚀 ~ Services ~ pairDevice ~ prd:", prd) const service = await Services.getInstance(); 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 = service.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 service.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 service.sdkClient.response_prd(outpointCommitment, prdString, true) console.log("🚀 ~ Services ~ pairDevice ~ tx:", tx) if(tx.ciphers_to_send) { tx.ciphers_to_send.forEach((cipher: string) => service.websocketConnection?.sendMessage('Cipher', cipher)) } this.injectProcessListPage() } } // async saveTxToDb(tx: CachedMessage) { // const database = await Database.getInstance(); // const indexedDb = await database.getDb(); // await database.writeObject(indexedDb, 'messages', tx, null); // } // async saveCipherTxToDb(tx: string) { // const database = await Database.getInstance(); // const indexedDb = await database.getDb(); // if(tx) { // await database.writeObject(indexedDb, database.getStoreList().AnkCipherMessages, tx, null); // } // } async getAmount() { const amount = await this.sdkClient.get_available_amount() console.log("🚀 ~ Services ~ getAmount ~ amount:", amount) return amount } async getDeviceAddress() { return await this.sdkClient.get_address(); } async dumpDevice() { const device = await this.sdkClient.dump_device() // console.log("🚀 ~ Services ~ dumpDevice ~ device:", device) return device } async saveDevice(device: any): Promise { // console.log("🚀 ~ Services ~ saveDevice ~ device:", device) localStorage.setItem('wallet', device); } async getDevice(): Promise { 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; } private addSubscription(element: Element, event: string, eventHandler: string): void { this.subscriptions.push({ element, event, eventHandler }); element.addEventListener(event, (this as any)[eventHandler].bind(this)); } 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); } this.generateQRCode(spAddress || '') return spAddress; } async restoreDevice(device: string) { try { await this.sdkClient.restore_device(device) const spAddress = this.sdkClient.get_address(); this.generateQRCode(spAddress || '') } catch (e) { console.error(e); } } 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); } } } private cleanSubscriptions(): void { for (const sub of this.subscriptions) { const el = sub.element; const eventHandler = sub.eventHandler; el.removeEventListener(sub.event, (this as any)[eventHandler].bind(this)); } this.subscriptions = []; } async injectProcessListPage(): Promise { const container = document.getElementById('containerId'); if (!container) { console.error("No html container"); return; } this.cleanSubscriptions() // const user = services.sdkClient.create_user('Test', 'test', 10, 1, 'Messaging') // console.log("🚀 ~ Services ~ injectProcessListPage ~ user:", user) // const database = await Database.getInstance(); // const indexedDb = await database.getDb(); // await database.writeObject(indexedDb, database.getStoreList().AnkUser, user.user, null); container.innerHTML = processPage; const newScript = document.createElement('script'); newScript.textContent = processScript; document.head.appendChild(newScript).parentNode?.removeChild(newScript); this.processes = await this.getProcesses(); if(this.processes) { this.setProcessesInSelectElement(this.processes) } } public async setProcessesInSelectElement(processList: any[]) { const select = document.querySelector(".select-field"); if(select) { for (const process of processList) { const option = document.createElement("option"); option.setAttribute("value", process.name); option.innerText = process.name; select.appendChild(option); } } const optionList = document.querySelector('.autocomplete-list'); if(optionList) { const observer = new MutationObserver((mutations, observer) => { const options = optionList.querySelectorAll('li') if(options) { for(const option of options) { this.addSubscription(option, 'click', 'showSelectedProcess') } } }); observer.observe(document, { subtree: true, attributes: true, }); } } public async listenToOptionListPopulating(event: Event) { const target = event.target as HTMLUListElement; const options = target?.querySelectorAll('li') } public async showSelectedProcess(event: MouseEvent) { const elem = event.target; if(elem) { const cardContent = document.querySelector(".card-content"); const processes = await this.getProcesses(); console.log("🚀 ~ Services ~ showSelectedProcess ~ processes:", processes) const process = processes.find((process: any) => process.name === (elem as any).dataset.value); if (process) { const processDiv = document.createElement("div"); processDiv.className = "process"; processDiv.id = process.name; const titleDiv = document.createElement("div"); titleDiv.className = "process-title"; titleDiv.innerHTML = `${process.name} : ${process.description}`; processDiv.appendChild(titleDiv); for (const zone of process.zoneList) { const zoneElement = document.createElement("div"); zoneElement.className = "process-element"; zoneElement.setAttribute('zone-id', zone.id.toString()) zoneElement.innerHTML = `Zone ${zone.id} : ${zone.name}`; this.addSubscription(zoneElement, 'click', 'goToProcessPage') processDiv.appendChild(zoneElement); } if(cardContent) cardContent.appendChild(processDiv); } } } goToProcessPage(event: MouseEvent) { const target = event.target as HTMLDivElement; const zoneId = target?.getAttribute('zone-id'); const processList = document.querySelectorAll('.process-element'); if(processList) { for(const process of processList) { process.classList.remove('selected') } } target.classList.add('selected') console.log('=======================> going to process page', zoneId) } async getProcesses(): Promise { const processes = this.sdkClient.get_processes() console.log("🚀 ~ Services ~ getProcesses ~ processes:", processes) return [ { id: 1, name: "Messaging", description: "Encrypted messages", zoneList: [ { id: 1, name: "General", path: '/test' }, ], }, { id: 2, name: "Storage", description: "Distributed storage", zoneList: [ { id: 1, name: "Paris", path: '/test' }, { id: 2, name: "Normandy", path: '/test' }, { id: 3, name: "New York", path: '/test' }, { id: 4, name: "Moscow", path: '/test' }, ], }, ]; } 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' } ] } async setNotification(): Promise { const badge = document.querySelector('.notification-badge') as HTMLDivElement const notifications = this.notifications const noNotifications = document.querySelector('.no-notification') as HTMLDivElement if(notifications?.length) { badge.innerText = notifications.length.toString() const notificationBoard = document.querySelector('.notification-board') as HTMLDivElement noNotifications.style.display = 'none' for(const notif of notifications) { const notifElement = document.createElement("div"); notifElement.className = "notification-element"; notifElement.setAttribute('notif-id', notif.id.toString()) notifElement.innerHTML = `
${notif.title}
${notif.description}
`; // this.addSubscription(notifElement, 'click', 'goToProcessPage') notificationBoard.appendChild(notifElement); } } else { noNotifications.style.display = 'block' } } }