import { createUserReturn, User, Process, createTransactionReturn, parse_network_msg, outputs_list, FaucetMessage, AnkFlag, NewTxMessage, encryptWithNewKeyResult, AnkSharedSecret, CachedMessage, UnknownMessage } from '../dist/pkg/sdk_client'; import IndexedDB from './database' import { WebSocketClient } from './websockets'; class Services { private static instance: Services; private sdkClient: any; private current_process: string | null = null; private websocketConnection: WebSocketClient[] = []; private sp_address: string | null = null; // 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) { Services.instance = new Services(); await Services.instance.init(); } return Services.instance; } // The init method is now part of the instance, and should only be called once private async init(): Promise { this.sdkClient = await import("../dist/pkg/sdk_client"); this.sdkClient.setup(); await this.updateProcesses(); } public async addWebsocketConnection(url: string): Promise { const services = await Services.getInstance(); const newClient = new WebSocketClient(url, services); if (!services.websocketConnection.includes(newClient)) { services.websocketConnection.push(newClient); } } public async isNewUser(): Promise { let isNew = false; try { const indexedDB = await IndexedDB.getInstance(); const db = await indexedDB.getDb(); let userListObject = await indexedDB.getAll(db, indexedDB.getStoreList().AnkUser); if (userListObject.length == 0) { isNew = true; } } catch (error) { console.error("Failed to retrieve isNewUser :", error); } return isNew; } public async displayCreateId(): Promise { const services = await Services.getInstance(); await services.createIdInjectHtml(); services.attachSubmitListener("form4nk", (event) => services.createId(event)); services.attachClickListener("displayrecover", services.displayRecover); await services.displayProcess(); } public async displaySendMessage(): Promise { const services = await Services.getInstance(); await services.injectHtml('Messaging'); services.attachSubmitListener("form4nk", (event) => services.sendMessage(event)); // const ourAddress = document.getElementById('our_address'); // if (ourAddress) { // ourAddress.innerHTML = `Our Address: ${this.sp_address}` // } // services.attachClickListener("displaysendmessage", services.displaySendMessage); // await services.displayProcess(); } public async sendMessage(event: Event): Promise { event.preventDefault(); const services = await Services.getInstance(); let availableAmt: number = 0; // check available amount try { availableAmt = await services.sdkClient.get_available_amount_for_user(true); } catch (error) { console.error('Failed to get available amount'); return; } if (availableAmt < 2000) { try { await services.obtainTokenWithFaucet(); } catch (error) { console.error('Failed to obtain faucet token:', error); return; } } const spAddressElement = document.getElementById("sp_address") as HTMLInputElement; const messageElement = document.getElementById("message") as HTMLInputElement; if (!spAddressElement || !messageElement) { console.error("One or more elements not found"); return; } const recipientSpAddress = spAddressElement.value; const message = messageElement.value; const msg_payload: UnknownMessage = {sender: this.sp_address!, message: message}; let notificationInfo = await services.notify_address_for_message(recipientSpAddress, msg_payload); if (notificationInfo) { let networkMsg = notificationInfo.new_network_msg; console.debug(networkMsg); const connection = await services.pickWebsocketConnectionRandom(); const flag: AnkFlag = "Unknown"; try { // send message (transaction in envelope) await services.updateMessages(networkMsg); connection?.sendMessage(flag, networkMsg.ciphertext!); } catch (error) { throw error; } // add peers list // add processes list } } public async createId(event: Event): Promise { event.preventDefault(); // verify we don't already have an user const services = await Services.getInstance(); try { let user = await services.getUserInfo(); if (user) { console.error("User already exists, please recover"); return; } } catch (error) { throw error; } const passwordElement = document.getElementById("password") as HTMLInputElement; const processElement = document.getElementById("selectProcess") as HTMLSelectElement; if (!passwordElement || !processElement) { throw 'One or more elements not found'; } const password = passwordElement.value; this.current_process = processElement.value; // console.log("JS password: " + password + " process: " + this.current_process); // To comment if test // if (!Services.instance.isPasswordValid(password)) return; const label = null; const birthday_signet = 50000; const birthday_main = 500000; let createUserReturn: createUserReturn; try { createUserReturn = services.sdkClient.create_user(password, label, birthday_main, birthday_signet, this.current_process); } catch (error) { throw error; } let user = createUserReturn.user; // const shares = user.shares; // send the shares on the network const revokeData = user.revoke_data; if (!revokeData) { throw 'Failed to get revoke data from wasm'; } // user.shares = []; user.revoke_data = null; try { const indexedDb = await IndexedDB.getInstance(); const db = await indexedDb.getDb(); await indexedDb.writeObject(db, indexedDb.getStoreList().AnkUser, user, null); } catch (error) { throw `Failed to write user object: ${error}`; } try { await services.obtainTokenWithFaucet(); } catch (error) { throw error; } await services.displayRevokeImage(new Uint8Array(revokeData)); } public async displayRecover(): Promise { const services = await Services.getInstance(); await services.recoverInjectHtml(); services.attachSubmitListener("form4nk", (event) => services.recover(event)); services.attachClickListener("displaycreateid", services.displayCreateId); services.attachClickListener("displayrevoke", services.displayRevoke); services.attachClickListener("submitButtonRevoke", services.revoke); await services.displayProcess(); } public async recover(event: Event) { event.preventDefault(); const passwordElement = document.getElementById("password") as HTMLInputElement; const processElement = document.getElementById("selectProcess") as HTMLSelectElement; if (!passwordElement || !processElement) { console.error("One or more elements not found"); return; } const password = passwordElement.value; const process = processElement.value; // console.log("JS password: " + password + " process: " + process); // To comment if test // if (!Services.instance.isPasswordValid(password)) return; // Get user in db const services = await Services.getInstance(); try { const user = await services.getUserInfo(); if (user) { services.sdkClient.login_user(password, user.pre_id, user.recover_data, user.shares, user.outputs); this.sp_address = services.sdkClient.get_recover_address(); if (this.sp_address) { console.info('Using sp_address:', this.sp_address); await services.obtainTokenWithFaucet(); } } } catch (error) { console.error(error); } console.info(this.sp_address); // TODO: check blocks since last_scan and update outputs await services.displaySendMessage(); } public async displayRevokeImage(revokeData: Uint8Array): Promise { const services = await Services.getInstance(); await services.revokeImageInjectHtml(); services.attachClickListener("displayupdateanid", services.displayUpdateAnId); let imageBytes = await services.getRecoverImage('assets/4nk_revoke.jpg'); if (imageBytes != null) { var elem = document.getElementById("revoke") as HTMLAnchorElement; if (elem != null) { let imageWithData = services.sdkClient.add_data_to_image(imageBytes, revokeData, true); const blob = new Blob([imageWithData], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); // Set the href attribute for download elem.href = url; elem.download = 'revoke_4NK.jpg'; } } } private async getRecoverImage(imageUrl:string): Promise { let imageBytes = null; try { const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); imageBytes = new Uint8Array(arrayBuffer); } catch (error) { console.error("Failed to get image : "+imageUrl, error); } return imageBytes; } public async displayRevoke(): Promise { const services = await Services.getInstance(); await services.revokeInjectHtml(); services.attachClickListener("displayrecover", Services.instance.displayRecover); services.attachSubmitListener("form4nk", Services.instance.revoke); } public async revoke(event: Event): Promise { event.preventDefault(); console.log("JS revoke click "); // TODO alert("revoke click to do ..."); } public async displayUpdateAnId() { const services = await Services.getInstance(); await services.updateIdInjectHtml(); services.attachSubmitListener("form4nk", services.updateAnId); } public async parseNetworkMessage(raw: string, feeRate: number): Promise { const services = await Services.getInstance(); try { const msg: CachedMessage = services.sdkClient.parse_network_msg(raw, feeRate); return msg; } catch (error) { throw error; } } public async updateAnId(event: Event): Promise { event.preventDefault(); // TODO get values const firstNameElement = 'firstName'; const lastNameElement = 'lastName'; const firstName = document.getElementById(firstNameElement) as HTMLInputElement; const lastName = document.getElementById(lastNameElement) as HTMLInputElement; console.log("JS updateAnId submit "); // TODO alert("updateAnId submit to do ... Name : "+firstName.value + " "+lastName.value); // TODO Mock add user member to process } public async displayProcess(): Promise { const services = await Services.getInstance(); const processList = await services.getAllProcess(); const selectProcess = document.getElementById("selectProcess"); if (selectProcess) { processList.forEach((process) => { let child = new Option(process.name, process.name); if (!selectProcess.contains(child)) { selectProcess.appendChild(child); } }) } } public async addProcess(process: Process): Promise { try { const indexedDB = await IndexedDB.getInstance(); const db = await indexedDB.getDb(); await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null); } catch (error) { console.log('addProcess failed: ',error); } } public async getAllProcess(): Promise { try { const indexedDB = await IndexedDB.getInstance(); const db = await indexedDB.getDb(); let processListObject = await indexedDB.getAll(db, indexedDB.getStoreList().AnkProcess); return processListObject; } catch (error) { console.log('getAllProcess failed: ',error); return []; } } public async updateOwnedOutputsForUser(): Promise { const services = await Services.getInstance(); let latest_outputs: outputs_list; try { latest_outputs = services.sdkClient.get_outpoints_for_user(); } catch (error) { console.error(error); return; } try { let user = await services.getUserInfo(); if (user) { user.outputs = latest_outputs; // console.warn(user); await services.updateUser(user); } } catch (error) { console.error(error); } } public async getAllProcessForUser(pre_id: string): Promise { const services = await Services.getInstance(); let user: User; let userProcessList: Process[] = []; try { const indexedDB = await IndexedDB.getInstance(); const db = await indexedDB.getDb(); user = await indexedDB.getObject(db, indexedDB.getStoreList().AnkUser, pre_id); } catch (error) { console.error('getAllUserProcess failed: ',error); return []; } try { const processListObject = await services.getAllProcess(); processListObject.forEach(async (processObject) => { if (processObject.members.includes(user.pre_id)) { userProcessList.push(processObject); } }) } catch (error) { console.error('getAllUserProcess failed: ',error); return []; } return userProcessList; } public async getProcessByName(name: string): Promise { console.log('getProcessByName name: '+name); const indexedDB = await IndexedDB.getInstance(); const db = await indexedDB.getDb(); const process = await indexedDB.getFirstMatchWithIndex(db, indexedDB.getStoreList().AnkProcess, 'by_name', name); console.log('getProcessByName process: '+process); return process; } public async updateMessages(message: CachedMessage): Promise { const indexedDb = await IndexedDB.getInstance(); const db = await indexedDb.getDb(); try { await indexedDb.setObject(db, indexedDb.getStoreList().AnkMessages, message, null); } catch (error) { throw error; } } public async removeMessage(id: number): Promise { const indexedDb = await IndexedDB.getInstance(); const db = await indexedDb.getDb(); try { await indexedDb.rmObject(db, indexedDb.getStoreList().AnkMessages, id); } catch (error) { throw error; } } public async updateProcesses(): Promise { const services = await Services.getInstance(); const processList: Process[] = services.sdkClient.get_processes(); processList.forEach(async (process: Process) => { const indexedDB = await IndexedDB.getInstance(); const db = await indexedDB.getDb(); try { const processStore = await indexedDB.getObject(db, indexedDB.getStoreList().AnkProcess, process.id); if (!processStore) { await indexedDB.writeObject(db, indexedDB.getStoreList().AnkProcess, process, null); } } catch (error) { console.error('Error while writing process', process.name, 'to indexedDB:', error); } }) } public attachClickListener(elementId: string, callback: (event: Event) => void): void { const element = document.getElementById(elementId); element?.removeEventListener("click", callback); element?.addEventListener("click", callback); } public attachSubmitListener(elementId: string, callback: (event: Event) => void): void { const element = document.getElementById(elementId); element?.removeEventListener("submit", callback); element?.addEventListener("submit", callback); } public async revokeInjectHtml() { const container = document.getElementById('containerId'); if (!container) { console.error("No html container"); return; } container.innerHTML = `

Revoke an Id



`; } public async revokeImageInjectHtml() { const container = document.getElementById('containerId'); if (!container) { console.error("No html container"); return; } container.innerHTML = `

Revoke image

`; } public async recoverInjectHtml() { const container = document.getElementById('containerId'); if (!container) { console.error("No html container"); return; } const services = await Services.getInstance(); await services.updateProcesses(); container.innerHTML = `

Recover my Id



Revoke

`; } public async createIdInjectHtml() { const container = document.getElementById('containerId'); if (!container) { console.error("No html container"); return; } container.innerHTML = `

Create an Id




`; } public async updateIdInjectHtml() { const container = document.getElementById('containerId'); if (!container) { console.error("No html container"); return; } container.innerHTML = `

Update an Id


`; } public async injectHtml(processName: string) { const container = document.getElementById('containerId'); if (!container) { console.error("No html container"); return; } const services = await Services.getInstance(); // do we have all processes in db? const knownProcesses = await services.getAllProcess(); const processesFromNetwork: Process[] = services.sdkClient.get_processes(); const processToAdd = processesFromNetwork.filter(processFromNetwork => !knownProcesses.some(knownProcess => knownProcess.id === processFromNetwork.id)); processToAdd.forEach(async p => { await services.addProcess(p); }) // get the process we need const process = await services.getProcessByName(processName); if (process) { container.innerHTML = process.html; } else { console.error("No process ", processName); } } // public async getCurrentProcess(): Promise { // let currentProcess = ""; // try { // const indexedDB = await IndexedDB.getInstance(); // const db = indexedDB.getDb(); // currentProcess = await indexedDB.getObject(db, indexedDB.getStoreList().AnkSession, Services.CURRENT_PROCESS); // } catch (error) { // console.error("Failed to retrieve currentprocess object :", error); // } // return currentProcess; // } public isPasswordValid(password: string) { var alertElem = document.getElementById("passwordalert"); var success = true; var strength = 0; if (password.match(/[a-z]+/)) { var strength = 0; strength += 1; } if (password.match(/[A-Z]+/)) { strength += 1; } if (password.match(/[0-9]+/)) { strength += 1; } if (password.match(/[$@#&!]+/)) { strength += 1; } if (alertElem !== null) { // TODO Passer à 18 if (password.length < 4) { alertElem.innerHTML = "Password size is < 4"; success = false; } else { if (password.length > 30) { alertElem.innerHTML = "Password size is > 30"; success = false; } else { if (strength < 4) { alertElem.innerHTML = "Password need [a-z] [A-Z] [0-9]+ [$@#&!]+"; success = false; } } } } return success; } private async pickWebsocketConnectionRandom(): Promise { const services = await Services.getInstance(); const websockets = services.websocketConnection; if (websockets.length === 0) { console.error("No websocket connection available at the moment"); return null; } else { const random = Math.floor(Math.random() * websockets.length); return websockets[random]; } } public async obtainTokenWithFaucet(): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { throw 'no available relay connections'; } let cachedMsg: CachedMessage; try { const flag: AnkFlag = 'Faucet'; cachedMsg = services.sdkClient.create_faucet_msg(); if (cachedMsg.commitment && cachedMsg.recipient) { let faucetMsg: FaucetMessage = { sp_address: cachedMsg.recipient, commitment: cachedMsg.commitment, } connection.sendMessage(flag, JSON.stringify(faucetMsg)); } } catch (error) { throw `Failed to obtain tokens with relay ${connection.getUrl()}: ${error}`; } try { await services.updateMessages(cachedMsg); } catch (error) { throw error; } } public async updateUser(user: User): Promise { try { const indexedDB = await IndexedDB.getInstance(); const db = await indexedDB.getDb(); await indexedDB.setObject(db, indexedDB.getStoreList().AnkUser, user, null); } catch (error) { throw error; } } public async getUserInfo(): Promise { try { const indexedDB = await IndexedDB.getInstance(); const db = await indexedDB.getDb(); let user = await indexedDB.getAll(db, indexedDB.getStoreList().AnkUser); // This should never happen if (user.length > 1) { throw "Multiple users in db"; } else { let res = user.pop(); if (res === undefined) { return null; } else { return res; } } } catch (error) { throw error; } } public async answer_confirmation_message(msg: CachedMessage): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { throw new Error("No connection to relay"); } let user: User; try { let possibleUser = await services.getUserInfo(); if (!possibleUser) { throw new Error("No user loaded, please first create a new user or login"); } else { user = possibleUser; } } catch (error) { throw error; } let notificationInfo: createTransactionReturn; try { const feeRate = 1; notificationInfo = services.sdkClient.answer_confirmation_transaction(msg.id, feeRate); } catch (error) { throw new Error(`Failed to create confirmation transaction: ${error}`); } const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, 'tweak_data': null } connection.sendMessage(flag, JSON.stringify(newTxMsg)); await services.updateMessages(notificationInfo.new_network_msg); return; } public async confirm_sender_address(msg: CachedMessage): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { throw new Error("No connection to relay"); } let user: User; try { let possibleUser = await services.getUserInfo(); if (!possibleUser) { throw new Error("No user loaded, please first create a new user or login"); } else { user = possibleUser; } } catch (error) { throw error; } let notificationInfo: createTransactionReturn; try { const feeRate = 1; notificationInfo = services.sdkClient.create_confirmation_transaction(msg.id, feeRate); } catch (error) { throw new Error(`Failed to create confirmation transaction: ${error}`); } const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, 'tweak_data': null } connection.sendMessage(flag, JSON.stringify(newTxMsg)); await services.updateMessages(notificationInfo.new_network_msg); return; } public async notify_address_for_message(sp_address: string, message: UnknownMessage): Promise { const services = await Services.getInstance(); const connection = await services.pickWebsocketConnectionRandom(); if (!connection) { throw 'No available connection'; } try { const feeRate = 1; let notificationInfo: createTransactionReturn = services.sdkClient.create_notification_transaction(sp_address, message, feeRate); const flag: AnkFlag = "NewTx"; const newTxMsg: NewTxMessage = { 'transaction': notificationInfo.transaction, 'tweak_data': null } connection.sendMessage(flag, JSON.stringify(newTxMsg)); console.info('Successfully sent notification transaction'); return notificationInfo; } catch (error) { throw 'Failed to create notification transaction:", error'; } } } export default Services;