ihm_client/src/services/service.ts

703 lines
26 KiB
TypeScript

// 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;
};
export default class Services {
private static instance: Services;
private current_process: string | null = null;
private sdkClient: any;
private websocketConnection: WebSocketClient | null = null;
private sp_address: string | 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<Services> {
if (!Services.instance) {
Services.instance = new Services();
await Services.instance.init();
}
return Services.instance;
}
public async init(): Promise<void> {
this.notifications = this.getNotifications();
this.sdkClient = await import("../../dist/pkg/sdk_client");
// this.database = Database.getInstance()
}
public async addWebsocketConnection(url: string): Promise<void> {
const services = await Services.getInstance();
const newClient = new WebSocketClient(url, services);
if (!services.websocketConnection) {
services.websocketConnection = newClient;
}
}
public async recoverInjectHtml(): Promise<void> {
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')
}
const url = location.href
this.generateQRCode(this.sp_address || '')
}
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);
}
}
async prepareProcessTx(myAddress: string, recipientAddress: string) {
const txid = '0'.repeat(64)
var vout = Number.MAX_SAFE_INTEGER;
const paringTemplate = {
"html": "",
"style": "",
"script": "",
"description": "AliceBob",
"roles": {
"owner": {
"members": [{sp_addresses: [myAddress]}, {sp_addresses:[recipientAddress]}],
"validation_rules":
[
{
"quorum": 1.0,
"fields": [
"roles",
"pairing_tx"
],
"min_sig_member": 1.0
}
]
}
},
"pairing_tx": `${txid}:4294967295`,
}
const service = await Services.getInstance();
const process = await service.sdkClient.create_update_transaction(undefined, JSON.stringify(paringTemplate), 1)
return process
}
async sendPairingTx(sp_address: string): Promise<void> {
const services = await Services.getInstance();
const amount = await this.getAmount() as any
console.log("🚀 ~ Services ~ sendPairingTx ~ amount:", amount)
// if(amount === 0n) {
// const faucetMessage = await services.createFaucetMessage()
// console.log("🚀 ~ WebSocketClient ~ this.ws.onopen= ~ faucetMessage:", faucetMessage)
// services.websocketConnection?.sendNormalMessage(faucetMessage)
// }
const spAddress = await this.getDeviceAddress() as any
let txid = '0'.repeat(64)
setTimeout(async () => {
let pairing = await services.sdkClient.pair_device(`${txid}:4294967295`, [sp_address])
const process = await this.prepareProcessTx(spAddress, sp_address)
const tx = process.new_tx_to_send
const parsedTx = JSON.parse(tx)
const transaction = parsedTx.transaction
txid = await services.sdkClient.get_txid(transaction)
const root_commitment = process.updated_process[0];
const init_process = process.updated_process[1];
console.log("🚀 ~ Services ~ setTimeout ~ init_process:", init_process, init_process.payload)
console.log("🚀 ~ Services ~ setTimeout ~ txToSend:", tx)
const prd = JSON.stringify(init_process.impending_requests[0]);
services.websocketConnection?.sendMessage('NewTx', tx)
pairing = await services.sdkClient.pair_device(`${txid}:0`, [sp_address])
const dump = await services.sdkClient.dump_device()
const prd_response = await services.sdkClient.response_prd(root_commitment, prd, true)
console.log("🚀 ~ Services ~ setTimeout ~ prd_response:", prd_response, prd, root_commitment)
if(process?.ciphers_to_send && process.ciphers_to_send.length) {
await this.sendCipherMessages(process.ciphers_to_send)
}
const router = await Routing.getInstance();
router.openLoginModal(spAddress, sp_address)
}, 2000)
}
async resetDevice() {
const service = await Services.getInstance()
await service.sdkClient.reset_device()
}
async sendNewTxMessage(message: string) {
const services = await Services.getInstance();
console.log("🚀 ~ WebSocketClient ~ this.ws.onopen= ~ newTxMessage:", message)
services.websocketConnection?.sendMessage('NewTx', message)
}
async sendCommitMessage(message: string) {
const services = await Services.getInstance();
console.log("🚀 ~ WebSocketClient ~ this.ws.onopen= ~ CommitMessage:", message)
services.websocketConnection?.sendMessage('Commit', message)
}
async sendCipherMessages(ciphers: string[]) {
const services = await Services.getInstance();
for (let i = 0; i < ciphers.length; i++) {
const cipher = ciphers[i];
await services.websocketConnection?.sendMessage('Cipher', cipher);
}
}
async sendFaucetMessage(): Promise<void> {
const services = await Services.getInstance();
const faucetMessage = await services.createFaucetMessage()
console.log("🚀 ~ WebSocketClient ~ this.ws.onopen= ~ faucetMessage:", faucetMessage)
services.websocketConnection?.sendNormalMessage(faucetMessage)
}
async parseCipher(message: string) {
try {
const services = await Services.getInstance();
try {
JSON.parse(message)
const router = await Routing.getInstance();
router.closeLoginModal()
this.injectProcessListPage()
} catch {
console.log('Not proper format for cipher')
}
const parsedTx = await services.sdkClient.parse_cipher(message, 0.00001)
console.log("🚀 ~ Services ~ parseCipher ~ parsedTx:", parsedTx)
await this.handleApiReturn(parsedTx)
// await this.saveCipherTxToDb(parsedTx)
} catch(e) {
console.log(e)
}
}
async parseNewTx(tx: string) {
try {
console.log('==============> sending txxxxxxx parser', tx)
const services = await Services.getInstance();
const parsedTx = await services.sdkClient.parse_new_tx(tx, 0, 0.01)
console.log("🚀 ~ Services ~ parseNewTx ~ parsedTx:", parsedTx)
if(parsedTx) {
await this.handleApiReturn(parsedTx)
// await this.saveTxToDb(parsedTx)
}
} catch(e) {
console.log(e)
}
}
private async handleApiReturn(apiReturn: ApiReturn) {
// const service = await Services.getInstance()
if(apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length) {
await this.sendCipherMessages(apiReturn.ciphers_to_send)
}
if(apiReturn.new_tx_to_send) {
await this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send))
}
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));
// router.openConfirmationModal(impendingRequest, outpointCommitment)
}
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;
this.sendCommitMessage(JSON.stringify(commit));
}
}
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 services = await Services.getInstance();
const amount = await services.sdkClient.get_available_amount()
console.log("🚀 ~ Services ~ getAmount ~ amount:", amount)
return amount
}
async getDeviceAddress() {
const services = await Services.getInstance();
const address = await services.sdkClient.get_address()
console.log("🚀 ~ Services ~ getDeviceAddress ~ address:", address)
return address
}
async dumpDevice() {
const services = await Services.getInstance();
const device = await services.sdkClient.dump_device()
console.log("🚀 ~ Services ~ dumpDevice ~ device:", device)
return device
}
async saveDevice(device: any): Promise<any> {
console.log("🚀 ~ Services ~ saveDevice ~ device:", device)
localStorage.setItem('wallet', device);
}
async getDevice(): Promise<string | null> {
return localStorage.getItem('wallet')
}
async dumpWallet() {
const services = await Services.getInstance();
const wallet = await services.sdkClient.dump_wallet()
console.log("🚀 ~ Services ~ dumpWallet ~ wallet:", wallet)
return wallet
}
async createFaucetMessage() {
const services = await Services.getInstance()
const message = await services.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() {
const service = await Services.getInstance();
let spAddress = '';
try {
spAddress = await service.sdkClient.create_new_device(0, 'regtest')
const device = await service.dumpDevice()
console.log("🚀 ~ Services ~ device:", device)
await service.saveDevice(device)
} catch (e) {
console.error("Services ~ Error:", e);
}
return spAddress;
}
async restoreDevice(device: string) {
const services = await Services.getInstance();
// const sp_wallet = JSON.parse(address)?.sp_wallet
console.log("🚀 ~ Services ~ restoreDevice ~ services?.sdkClient:", device)
const res = await services?.sdkClient?.restore_device(device)
console.log("🚀 ~ Services ~ restoreDevice ~ res:", res)
}
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;
}
const services = await Services.getInstance();
try {
await services.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.length == 0) {
console.debug("🚀 ~ Services ~ restoreMessages ~ no messages in local storage");
return;
}
const services = await Services.getInstance();
try {
await services.sdkClient.set_message_cache(JSON.stringify(cachedMessages));
} catch (e) {
console.error('Services ~ restoreMessages ~ Error:', e);
}
}
private cleanSubsciptions(): 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<void> {
const container = document.getElementById('containerId');
if (!container) {
console.error("No html container");
return;
}
this.cleanSubsciptions()
const services = await Services.getInstance();
// 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 services.getProcesses();
if(this.processes) {
services.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 services = await Services.getInstance();
const processes = await services.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<IProcess[]> {
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<void> {
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 = `
<div>${notif.title}</div>
<div>${notif.description}</div>
`;
// this.addSubscription(notifElement, 'click', 'goToProcessPage')
notificationBoard.appendChild(notifElement);
}
} else {
noNotifications.style.display = 'block'
}
}
}