ihm_client/src/services/service.ts
2024-11-08 09:13:56 +00:00

537 lines
17 KiB
TypeScript

// 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<Services> | 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<Services> {
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<void> {
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<void> {
// 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<void> {
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<void> {
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<void> {
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<BigInt> {
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<any> {
const db = await Database.getInstance();
db.addObject({
storeName: 'wallet',
object: { pre_id: '1', device },
key: '1',
});
localStorage.setItem('wallet', device);
}
async getDevice(): Promise<string | null> {
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<any[]> {
const process = [
[
'1',
{
title: 'Messaging',
description: 'Messagerie chiffrée',
html: '<div><input /></div>',
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: '<div><input value="{{inputValue}}" /></div>',
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',
},
];
}
}