import { INotification } from '~/models/notification.model'; import { IProcess } from '~/models/process.model'; import { initWebsocket, sendMessage } from '../websockets'; import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client'; import ModalService from './modal.service'; import Database from './database.service'; import { navigate } from '../router'; import { storeData, retrieveData, testData } from './storage.service'; import { BackUp } from '~/models/backup.model'; import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; export const U32_MAX = 4294967295; const BASEURL = `https://demo.4nkweb.com`; const BOOTSTRAPURL = [`${BASEURL}/ws/`]; const STORAGEURL = `${BASEURL}/storage` const DEFAULTAMOUNT = 1000n; const EMPTY32BYTES = String('').padStart(64, '0'); export default class Services { private static initializing: Promise | null = null; private static instance: Services; private processId: string | null = null; private stateId: string | null = null; private sdkClient: any; private myProcesses: Set = new Set(); private notifications: any[] | null = null; private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; private database: any; private routingInstance!: ModalService; private relayAddresses: { [wsurl: string]: string } = {}; private membersList: Record = {}; // 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('../../pkg/sdk_client'); this.sdkClient.setup(); for (const wsurl of Object.values(BOOTSTRAPURL)) { this.updateRelay(wsurl, ''); } await this.connectAllRelays(); } public setProcessId(processId: string | null) { this.processId = processId; } public setStateId(stateId: string | null) { this.stateId = stateId; } public getProcessId(): string | null { return this.processId; } public getStateId(): string | null { return this.stateId; } /** * Calls `this.addWebsocketConnection` for each `wsurl` in relayAddresses. */ public async connectAllRelays(): Promise { for (const wsurl of Object.keys(this.relayAddresses)) { try { console.log(`Connecting to: ${wsurl}`); await this.addWebsocketConnection(wsurl); console.log(`Successfully connected to: ${wsurl}`); } catch (error) { console.error(`Failed to connect to ${wsurl}:`, error); } } } public async addWebsocketConnection(url: string): Promise { console.log('Opening new websocket connection'); await initWebsocket(url); } /** * Add or update a key/value pair in relayAddresses. * @param wsurl - The WebSocket URL (key). * @param spAddress - The SP Address (value). */ public updateRelay(wsurl: string, spAddress: string): void { this.relayAddresses[wsurl] = spAddress; console.log(`Updated: ${wsurl} -> ${spAddress}`); } /** * Retrieve the spAddress for a given wsurl. * @param wsurl - The WebSocket URL to look up. * @returns The SP Address if found, or undefined if not. */ public getSpAddress(wsurl: string): string | undefined { return this.relayAddresses[wsurl]; } /** * Get all key/value pairs from relayAddresses. * @returns An array of objects containing wsurl and spAddress. */ public getAllRelays(): { wsurl: string; spAddress: string }[] { return Object.entries(this.relayAddresses).map(([wsurl, spAddress]) => ({ wsurl, spAddress, })); } /** * Print all key/value pairs for debugging. */ public printAllRelays(): void { console.log("Current relay addresses:"); for (const [wsurl, spAddress] of Object.entries(this.relayAddresses)) { console.log(`${wsurl} -> ${spAddress}`); } } public isPaired(): boolean { try { return this.sdkClient.is_paired(); } catch (e) { throw new Error(`isPaired ~ Error: ${e}`); } } public async unpairDevice(): Promise { try { this.sdkClient.unpair_device(); const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); } catch (e) { throw new Error(`Failed to unpair device: ${e}`); } } public async getSecretForAddress(address: string): Promise { const db = await Database.getInstance(); return await db.getObject('shared_secrets', address); } public async getAllSecrets(): Promise { const db = await Database.getInstance(); const sharedSecrets = await db.dumpStore('shared_secrets'); const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); // keys are numeric values const secretsStore = { shared_secrets: sharedSecrets, unconfirmed_secrets: Object.values(unconfirmedSecrets), }; return secretsStore; } public async getAllDiffs(): Promise> { const db = await Database.getInstance(); return await db.dumpStore('diffs'); } public async getDiffByValue(value: string): Promise { const db = await Database.getInstance(); const store = 'diffs'; const res = await db.getObject(store, value); return res; } private async getTokensFromFaucet(): Promise { try { await this.ensureSufficientAmount(); } catch (e) { console.error('Failed to get tokens from relay, check connection'); return; } } public async checkConnections(members: Member[]): Promise { // Ensure the amount is available before proceeding await this.getTokensFromFaucet(); let unconnectedAddresses = []; const myAddress = this.getDeviceAddress(); for (const member of members) { const sp_addresses = member.sp_addresses; if (!sp_addresses || sp_addresses.length === 0) continue; for (const address of sp_addresses) { // For now, we ignore our own device address, although there might be use cases for having a secret with ourselves if (address === myAddress) continue; const sharedSecret = await this.getSecretForAddress(address); if (!sharedSecret) { unconnectedAddresses.push(address); } } } if (unconnectedAddresses && unconnectedAddresses.length != 0) { const apiResult = await this.connectAddresses(unconnectedAddresses); await this.handleApiReturn(apiResult); } } public async connectAddresses(addresses: string[]): Promise { if (addresses.length === 0) { throw new Error('Trying to connect to empty addresses list'); } try { return this.sdkClient.create_transaction(addresses, 1); } catch (e) { console.error('Failed to connect member:', e); throw e; } } private async ensureSufficientAmount(): Promise { const availableAmt = this.getAmount(); const target: BigInt = DEFAULTAMOUNT * BigInt(10); if (availableAmt < target) { const faucetMsg = this.createFaucetMessage(); this.sendFaucetMessage(faucetMsg); await this.waitForAmount(target); } } private async waitForAmount(target: BigInt): Promise { let attempts = 3; while (attempts > 0) { const amount = this.getAmount(); if (amount >= target) { return amount; } attempts--; if (attempts > 0) { await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second } } throw new Error('Amount is still 0 after 3 attempts'); } public async createPairingProcess(userName: string, pairWith: string[]): Promise { if (this.sdkClient.is_paired()) { throw new Error('Device already paired'); } const myAddress: string = this.sdkClient.get_address(); pairWith.push(myAddress); const privateData = { description: 'pairing', counter: 0, }; const publicData = { memberPublicName: userName, pairedAddresses: pairWith, }; const validation_fields: string[] = [...Object.keys(privateData), ...Object.keys(publicData), 'roles']; const roles: Record = { pairing: { members: [], validation_rules: [ { quorum: 1.0, fields: validation_fields, min_sig_member: 1.0, }, ], storages: [STORAGEURL] }, }; try { return this.createProcess( privateData, publicData, roles ); } catch (e) { throw new Error(`Creating process failed:, ${e}`); } } private isFileBlob(value: any): value is { type: string, data: Uint8Array } { return ( typeof value === 'object' && value !== null && typeof value.type === 'string' && value.data instanceof Uint8Array ); } private splitData(obj: Record) { const jsonCompatibleData: Record = {}; const binaryData: Record = {}; for (const [key, value] of Object.entries(obj)) { if (this.isFileBlob(value)) { binaryData[key] = value; } else { jsonCompatibleData[key] = value; } } return { jsonCompatibleData, binaryData }; } public async createProcess( privateData: Record, publicData: Record, roles: Record, ): Promise { const relayAddress = this.getAllRelays()[0]['spAddress']; const feeRate = 1; // We can't encode files as the rest because Uint8Array is not valid json // So we first take them apart and we will encode them separately and put them back in the right object // TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking const privateSplitData = this.splitData(privateData); const publicSplitData = this.splitData(publicData); const encodedPrivateData = { ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), ...this.sdkClient.encode_binary(privateSplitData.binaryData) }; const encodedPublicData = { ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), ...this.sdkClient.encode_binary(publicSplitData.binaryData) }; let members: Set = new Set(); for (const role of Object.values(roles!)) { for (const member of role.members) { // Check if we know the member that matches this id const memberAddresses = this.getAddressesForMemberId(member); if (memberAddresses && memberAddresses.length != 0) { members.add({ sp_addresses: memberAddresses }); } } } await this.checkConnections([...members]); const result = this.sdkClient.create_new_process ( encodedPrivateData, roles, encodedPublicData, relayAddress, feeRate, this.getAllMembers() ); return(result); } public async updateProcess(process: Process, privateData: Record, publicData: Record, roles: Record | null): Promise { // If roles is null, we just take the last commited state roles if (!roles) { roles = this.getRoles(process); } else { // We should check that we have the right to change the roles here, or maybe it's better leave it to the wasm console.log('Provided new roles:', JSON.stringify(roles)); } let members: Set = new Set(); for (const role of Object.values(roles!)) { for (const member of role.members) { members.add(member) } } if (members.size === 0) { // This must be a pairing process // Check if we have a pairedAddresses in the public data const publicData = this.getPublicData(process); if (!publicData || !publicData['pairedAddresses']) { throw new Error('Not a pairing process'); } const decodedAddresses = this.decodeValue(publicData['pairedAddresses']); if (decodedAddresses.length === 0) { throw new Error('Not a pairing process'); } members.add({ sp_addresses: decodedAddresses }); } await this.checkConnections([...members]); const privateSplitData = this.splitData(privateData); const publicSplitData = this.splitData(publicData); const encodedPrivateData = { ...this.sdkClient.encode_json(privateSplitData.jsonCompatibleData), ...this.sdkClient.encode_binary(privateSplitData.binaryData) }; const encodedPublicData = { ...this.sdkClient.encode_json(publicSplitData.jsonCompatibleData), ...this.sdkClient.encode_binary(publicSplitData.binaryData) }; try { return this.sdkClient.update_process(process, encodedPrivateData, roles, encodedPublicData, this.getAllMembers()); } catch (e) { throw new Error(`Failed to update process: ${e}`); } } public async createPrdUpdate(processId: string, stateId: string): Promise { const process = await this.getProcess(processId); if (!process) { throw new Error('Unknown process'); } try { return this.sdkClient.create_update_message(process, stateId, this.getAllMembers()); } catch (e) { throw new Error(`Failed to create prd update: ${e}`); } } public async createPrdResponse(processId: string, stateId: string): Promise { const process = await this.getProcess(processId); if (!process) { throw new Error('Unknown process'); } try { return this.sdkClient.create_response_prd(process, stateId, this.getAllMembers()); } catch (e) { throw new Error(`Failed to create response prd: ${e}`); } } public async approveChange(processId: string, stateId: string): Promise { const process = await this.getProcess(processId); if (!process) { throw new Error('Failed to get process from db'); } try { return this.sdkClient.validate_state(process, stateId, this.getAllMembers()); } catch (e) { throw new Error(`Failed to create prd response: ${e}`); } } public async rejectChange(processId: string, stateId: string): Promise { const process = await this.getProcess(processId); if (!process) { throw new Error('Failed to get process from db'); } try { return this.sdkClient.refuse_state(process, stateId); } catch (e) { throw new Error(`Failed to create prd response: ${e}`); } } async resetDevice() { this.sdkClient.reset_device(); // Clear all stores const db = await Database.getInstance(); await db.clearStore('wallet'); await db.clearStore('shared_secrets'); await db.clearStore('unconfirmed_secrets'); await db.clearStore('processes'); await db.clearStore('diffs'); } async sendNewTxMessage(message: string) { sendMessage('NewTx', message); } async sendCommitMessage(message: string) { sendMessage('Commit', message); } async sendCipherMessages(ciphers: string[]) { for (let i = 0; i < ciphers.length; i++) { const cipher = ciphers[i]; sendMessage('Cipher', cipher); } } sendFaucetMessage(message: string): void { sendMessage('Faucet', message); } async parseCipher(message: string) { const membersList = this.getAllMembers(); try { // console.log('parsing new cipher'); const apiReturn = this.sdkClient.parse_cipher(message, membersList); await this.handleApiReturn(apiReturn); // Device 1 wait Device 2 const waitingModal = document.getElementById('waiting-modal'); if (waitingModal) { this.device2Ready = true; } } catch (e) { console.error(`Parsed cipher with error: ${e}`); } // await this.saveCipherTxToDb(parsedTx) } async parseNewTx(tx: string) { const membersList = this.getAllMembers(); try { const parsedTx = this.sdkClient.parse_new_tx(tx, 0, membersList); if (parsedTx) { try { await this.handleApiReturn(parsedTx); const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); } catch (e) { console.error('Failed to update device with new tx'); } } } catch (e) { console.trace(e); } } public async handleApiReturn(apiReturn: ApiReturn) { console.log(apiReturn); if (apiReturn.partial_tx) { try { const res = this.sdkClient.sign_transaction(apiReturn.partial_tx); apiReturn.new_tx_to_send = res.new_tx_to_send; } catch (e) { console.error('Failed to sign transaction:', e); } } if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) { await this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send)); await new Promise(r => setTimeout(r, 500)); } if (apiReturn.secrets) { const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets; const confirmedSecrets = apiReturn.secrets.shared_secrets; const db = await Database.getInstance(); for (const secret of unconfirmedSecrets) { await db.addObject({ storeName: 'unconfirmed_secrets', object: secret, key: null, }); } const entries = Object.entries(confirmedSecrets).map(([key, value]) => ({ key, value })); for (const entry of entries) { try { await db.addObject({ storeName: 'shared_secrets', object: entry.value, key: entry.key, }); } catch (e) { throw e; } // We don't want to throw an error, it could simply be that we registered directly the shared secret // this.removeUnconfirmedSecret(entry.value); } } if (apiReturn.updated_process) { const updatedProcess = apiReturn.updated_process; const processId: string = updatedProcess.process_id; if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) { for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) { const blob = this.hexToBlob(cipher); try { await this.saveBlobToDb(hash, blob); } catch (e) { console.error(e); } } } // Save process to db await this.saveProcessToDb(processId, updatedProcess.current_process); if (updatedProcess.diffs && updatedProcess.diffs.length != 0) { try { await this.saveDiffsToDb(updatedProcess.diffs); } catch (e) { console.error('Failed to save diffs to db:', e); } } } if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) { for (const hash of apiReturn.push_to_storage) { const blob = await this.getBlobFromDb(hash); if (blob) { await this.saveDataToStorage(hash, blob, null); } else { console.error('Failed to get data from db'); } } } if (apiReturn.commit_to_send) { const commit = apiReturn.commit_to_send; await this.sendCommitMessage(JSON.stringify(commit)); } if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) { await this.sendCipherMessages(apiReturn.ciphers_to_send); } } public async openPairingConfirmationModal(processId: string) { const process = await this.getProcess(processId); if (!process) { console.error('Failed to find pairing process'); return; } const firstState = process.states[0]; const roles = firstState.roles; const stateId = firstState.state_id; try { await this.routingInstance.openPairingConfirmationModal(roles, processId, stateId); } catch (e) { console.error(e); } } public async confirmPairing() { if (!this.processId || !this.stateId) { console.error('Missing process and/or state ID'); return; } let createPrdUpdateReturn; try { createPrdUpdateReturn = await this.createPrdUpdate(this.processId, this.stateId); } catch (e) { throw new Error(`createPrdUpdate failed: ${e}`); } await this.handleApiReturn(createPrdUpdateReturn); let approveChangeReturn; try { approveChangeReturn = await this.approveChange(this.processId, this.stateId); } catch (e) { throw new Error(`approveChange failed: ${e}`); } await this.handleApiReturn(approveChangeReturn); await this.pairDevice(); this.processId = null; this.stateId = null; const newDevice = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(newDevice); await navigate('account'); } public async pairDevice() { if (!this.processId) { console.error('No processId set'); return; } const process = await this.getProcess(this.processId); if (!process) { console.error('Unknown process'); return; } let spAddressList: string[] = []; try { const encodedSpAddressList = process.states[0].public_data['pairedAddresses']; spAddressList = this.sdkClient.decode_value(encodedSpAddressList); if (!spAddressList || spAddressList.length == 0) { throw new Error('Empty pairedAddresses'); } } catch (e) { throw new Error(`Failed to get pairedAddresses from process: ${e}`); } try { this.sdkClient.pair_device(this.processId, spAddressList); } catch (e) { throw new Error(`Failed to pair device: ${e}`); } } public getAmount(): BigInt { const amount = this.sdkClient.get_available_amount(); return amount; } getDeviceAddress(): string { try { return this.sdkClient.get_address(); } catch (e) { throw new Error(`Failed to get device address: ${e}`); } } public dumpDeviceFromMemory(): string { try { return this.sdkClient.dump_device(); } catch (e) { throw new Error(`Failed to dump device: ${e}`); } } public dumpNeuteredDevice(): Device | null { try { return this.sdkClient.dump_neutered_device(); } catch (e) { console.error(`Failed to dump device: ${e}`); return null; } } public getPairingProcessId(): string { try { return this.sdkClient.get_pairing_process_id(); } catch (e) { throw new Error(`Failed to get pairing process: ${e}`); } } async saveDeviceInDatabase(device: any): Promise { const db = await Database.getInstance(); const walletStore = 'wallet'; try { const prevDevice = await this.getDeviceFromDatabase(); if (prevDevice) { await db.deleteObject(walletStore, "1"); } await db.addObject({ storeName: walletStore, object: { pre_id: '1', device }, key: null, }); } catch (e) { console.error(e); } } async getDeviceFromDatabase(): Promise { const db = await Database.getInstance(); const walletStore = 'wallet'; try { const dbRes = await db.getObject(walletStore, '1'); if (dbRes) { const wallet = dbRes['device']; return wallet; } else { return null; } } catch (e) { throw new Error(`Failed to retrieve device from db: ${e}`); } } async getMemberFromDevice(): Promise { try { const device = await this.getDeviceFromDatabase(); if (device) { const parsed: Device = JSON.parse(device); const pairedMember = parsed['paired_member']; return pairedMember.sp_addresses; } else { return null; } } catch (e) { throw new Error(`Failed to retrieve paired_member from device: ${e}`); } } isChildRole(parent: any, child: any): boolean { try { this.sdkClient.is_child_role(JSON.stringify(parent), JSON.stringify(child)); } catch (e) { console.error(e); return false; } return true; } rolesContainsUs(roles: Record): boolean { let us; try { us = this.sdkClient.get_pairing_process_id(); } catch (e) { throw e; } return this.rolesContainsMember(roles, us); } rolesContainsMember(roles: Record, pairingProcessId: string): boolean { for (const roleDef of Object.values(roles)) { if (roleDef.members.includes(pairingProcessId)) { return true; } } return false; } async dumpWallet() { const wallet = await this.sdkClient.dump_wallet(); return wallet; } public createFaucetMessage() { const message = this.sdkClient.create_faucet_msg(); return message; } async createNewDevice() { let spAddress = ''; try { spAddress = await this.sdkClient.create_new_device(0, 'signet'); const device = this.dumpDeviceFromMemory(); await this.saveDeviceInDatabase(device); } catch (e) { console.error('Services ~ Error:', e); } return spAddress; } restoreDevice(device: string) { try { this.sdkClient.restore_device(device); const spAddress = this.sdkClient.get_address(); } catch (e) { console.error(e); } } private async removeProcess(processId: string): Promise { const db = await Database.getInstance(); const storeName = 'processes'; try { await db.deleteObject(storeName, processId); } catch (e) { console.error(e); } } public async saveProcessToDb(processId: string, process: Process) { const db = await Database.getInstance(); try { await db.addObject({ storeName: 'processes', object: process, key: processId, }); } catch (e) { console.error(`Failed to save process ${processId}: ${e}`); } } public async saveBlobToDb(hash: string, data: Blob) { const db = await Database.getInstance(); try { await db.addObject({ storeName: 'data', object: data, key: hash, }); } catch (e) { console.error(`Failed to save data to db: ${e}`); } } public async getBlobFromDb(hash: string): Promise { const db = await Database.getInstance(); try { return await db.getObject('data', hash); } catch (e) { return null; } } public async saveDataToStorage(hash: string, data: Blob, ttl: number | null) { const storages = [STORAGEURL]; try { await storeData(storages, hash, data, ttl); } catch (e) { console.error(`Failed to store data with hash ${hash}: ${e}`); } } public async fetchValueFromStorage(hash: string): Promise { const storages = [STORAGEURL]; return await retrieveData(storages, hash); } public async testDataInStorage(hash: string): Promise | null> { const storages = [STORAGEURL]; return await testData(storages, hash); } public async saveDiffsToDb(diffs: UserDiff[]) { const db = await Database.getInstance(); try { for (const diff of diffs) { await db.addObject({ storeName: 'diffs', object: diff, key: null, }); } } catch (e) { throw new Error(`Failed to save process: ${e}`); } } public async getProcess(processId: string): Promise { const db = await Database.getInstance(); return await db.getObject('processes', processId); } public async getProcesses(): Promise> { const db = await Database.getInstance(); const processes: Record = await db.dumpStore('processes'); return processes; } // TODO rewrite that it's a mess and we don't use it now // public async getChildrenOfProcess(processId: string): Promise { // const processes = await this.getProcesses(); // const res = []; // for (const [hash, process] of Object.entries(processes)) { // const firstState = process.states[0]; // const pcdCommitment = firstState['pcd_commitment']; // try { // const parentIdHash = pcdCommitment['parent_id']; // const diff = await this.getDiffByValue(parentIdHash); // if (diff && diff['new_value'] === processId) { // res.push(JSON.stringify(process)); // } // } catch (e) { // continue; // } // } // return res; // } public async restoreProcessesFromBackUp(processes: Record) { const db = await Database.getInstance(); for (const [commitedIn, process] of Object.entries(processes)) { await db.addObject({ storeName: 'processes', object: process, key: commitedIn}); } await this.restoreProcessesFromDB(); } // Restore process in wasm with persistent storage public async restoreProcessesFromDB() { const db = await Database.getInstance(); try { const processes: Record = await db.dumpStore('processes'); if (processes && Object.keys(processes).length != 0) { console.log(`Restoring ${Object.keys(processes).length} processes`); this.sdkClient.set_process_cache(processes); } else { console.log('No processes to restore!'); } } catch (e) { throw e; } } public async clearSecretsFromDB() { const db = await Database.getInstance(); try { await db.clearStore('shared_secrets'); await db.clearStore('unconfirmed_secrets'); } catch (e) { console.error(e); } } public async restoreSecretsFromBackUp(secretsStore: SecretsStore) { const db = await Database.getInstance(); for (const secret of secretsStore.unconfirmed_secrets) { await db.addObject({ storeName: 'unconfirmed_secrets', object: secret, key: null, }); } const entries = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value })); for (const entry of entries) { await db.addObject({ storeName: 'shared_secrets', object: entry.value, key: entry.key, }); } // Now we can transfer them to memory await this.restoreSecretsFromDB(); } public async restoreSecretsFromDB() { const db = await Database.getInstance(); try { const sharedSecrets: Record = await db.dumpStore('shared_secrets'); const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); const secretsStore = { shared_secrets: sharedSecrets, unconfirmed_secrets: Object.values(unconfirmedSecrets), }; this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore)); } catch (e) { throw e; } } decodeValue(value: number[]): any | null { try { return this.sdkClient.decode_value(value); } catch (e) { console.error(`Failed to decode value: ${e}`); return null; } } async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise { let hash = state.pcd_commitment[attribute]; if (!hash) { // attribute doesn't exist return null; } let key = state.keys[attribute]; const pairingProcessId = this.getPairingProcessId(); // If key is missing, request an update and then retry if (!key) { const roles = state.roles; let hasAccess = false; // If we're not supposed to have access to this attribute, ignore for (const role of Object.values(roles)) { for (const rule of Object.values(role.validation_rules)) { if (rule.fields.includes(attribute)) { if (role.members.includes(pairingProcessId)) { // We have access to this attribute hasAccess = true; break; } } } } if (!hasAccess) return null; // We should have the key, so we're going to ask other members for it await this.requestDataFromPeers(processId, [state.state_id], [state.roles]); const maxRetries = 5; const retryDelay = 500; // delay in milliseconds let retries = 0; while ((!hash || !key) && retries < maxRetries) { await new Promise(resolve => setTimeout(resolve, retryDelay)); // Re-read hash and key after waiting hash = state.pcd_commitment[attribute]; key = state.keys[attribute]; retries++; } } if (hash && key) { const blob = await this.getBlobFromDb(hash); if (blob) { // Decrypt the data const buf = await blob.arrayBuffer(); const cipher = new Uint8Array(buf); const keyUIntArray = this.hexToUInt8Array(key); try { const clear = this.sdkClient.decrypt_data(keyUIntArray, cipher); if (clear) { // deserialize the result to get the actual data const decoded = this.sdkClient.decode_value(clear); return decoded; } else { throw new Error('decrypt_data returned null'); } } catch (e) { console.error(`Failed to decrypt data: ${e}`); } } } return null; } getNotifications(): any[] | null { // 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', // }, // ]; return this.notifications; } setNotifications(notifications: any[]) { this.notifications = notifications; } async importJSON(backup: BackUp): Promise { const device = JSON.stringify(backup.device); // Reset current device await this.resetDevice(); await this.saveDeviceInDatabase(device); this.restoreDevice(device); // TODO restore secrets and processes from file const secretsStore = backup.secrets; await this.restoreSecretsFromBackUp(secretsStore); const processes = backup.processes; await this.restoreProcessesFromBackUp(processes); } public async createBackUp(): Promise { // Get the device from indexedDB const deviceStr = await this.getDeviceFromDatabase(); if (!deviceStr) { console.error('No device loaded'); return null; } const device: Device = JSON.parse(deviceStr); // Get the processes const processes = await this.getProcesses(); // Get the shared secrets const secrets = await this.getAllSecrets(); // Create a backup object const backUp = { device: device, secrets: secrets, processes: processes, }; return backUp; } // Device 1 wait Device 2 public device1: boolean = false; public device2Ready: boolean = false; public resetState() { this.device1 = false; this.device2Ready = false; } // Handle the handshake message public async handleHandshakeMsg(url: string, parsedMsg: any) { try { const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg); this.updateRelay(url, handshakeMsg.sp_address); if (this.membersList && Object.keys(this.membersList).length === 0) { // We start from an empty list, just copy it over this.membersList = handshakeMsg.peers_list; } else { // We are incrementing our list for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) { this.membersList[processId] = member as Member; } } setTimeout(async () => { const newProcesses: OutPointProcessMap = handshakeMsg.processes_list; if (newProcesses && Object.keys(newProcesses).length !== 0) { for (const [processId, process] of Object.entries(newProcesses)) { const existing = await this.getProcess(processId); if (existing) { // Look for state id we don't know yet let new_states = []; let roles = []; for (const state of process.states) { if (!state.state_id || state.state_id === EMPTY32BYTES) { continue; } if (!this.lookForStateId(existing, state.state_id)) { if (this.rolesContainsUs(state.roles)) { new_states.push(state.state_id); roles.push(state.roles); } } } if (new_states.length != 0) { // We request the new states await this.requestDataFromPeers(processId, new_states, roles); } // Otherwise we're probably just in the initial loading at page initialization // We may learn an update for this process // TODO maybe actually check if what the relay is sending us contains more information than what we have // relay should always have more info than us, but we never know // For now let's keep it simple and let the worker do the job } else { // We add it to db console.log(`Saving ${processId} to db`); await this.saveProcessToDb(processId, process as Process); } } } }, 500) } catch (e) { console.error('Failed to parse init message:', e); } } private lookForStateId(process: Process, stateId: string): boolean { for (const state of process.states) { if (state.state_id === stateId) { return true; } } return false; } /** * Retourne la liste de tous les membres ordonnés par leur process id * @returns Un tableau contenant tous les membres */ public getAllMembersSorted(): Record { return Object.fromEntries( Object.entries(this.membersList).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) ); } public getAllMembers(): Record { return this.membersList; } public getAddressesForMemberId(memberId: string): string[] | null { try { return this.membersList[memberId].sp_addresses; } catch (e) { return null; } } public compareMembers(memberA: string[], memberB: string[]): boolean { if (!memberA || !memberB) { return false } if (memberA.length !== memberB.length) { return false } const res = memberA.every(item => memberB.includes(item)) && memberB.every(item => memberA.includes(item)); return res; } public async handleCommitError(response: string) { const content = JSON.parse(response); const error = content.error; const errorMsg = error['GenericError']; if (errorMsg === 'State is identical to the previous state') { return; } else if (errorMsg === 'Not enough valid proofs') { return; } // Wait and retry setTimeout(async () => { await this.sendCommitMessage(JSON.stringify(content)); }, 1000) } public getRoles(process: Process): Record | null { const lastCommitedState = this.getLastCommitedState(process); if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) { return lastCommitedState!.roles; } else if (process.states.length === 2) { const firstState = process.states[0]; if (firstState && firstState.roles && Object.keys(firstState.roles).length != 0) { return firstState!.roles; } } return null; } public getPublicData(process: Process): Record | null { const lastCommitedState = this.getLastCommitedState(process); if (lastCommitedState && lastCommitedState.public_data && Object.keys(lastCommitedState.public_data).length != 0) { return lastCommitedState!.public_data; } else if (process.states.length === 2) { const firstState = process.states[0]; if (firstState && firstState.public_data && Object.keys(firstState.public_data).length != 0) { return firstState!.public_data; } } return null; } public getProcessName(process: Process): string | null { const lastCommitedState = this.getLastCommitedState(process); if (lastCommitedState && lastCommitedState.public_data) { const processName = lastCommitedState!.public_data['processName']; if (processName) { return this.decodeValue(processName) } else { return null } } else { return null; } } public async getMyProcesses(): Promise { // If we're not paired yet, just skip it try { this.getPairingProcessId(); } catch (e) { return null; } try { const processes = await this.getProcesses(); const newMyProcesses = new Set(this.myProcesses || []); for (const [processId, process] of Object.entries(processes)) { // We use myProcesses attribute to not reevaluate all processes everytime if (newMyProcesses.has(processId)) { continue; } try { const roles = this.getRoles(process); if (roles && this.rolesContainsUs(roles)) { newMyProcesses.add(processId); } } catch (e) { console.error(e); } } this.myProcesses = newMyProcesses; // atomic update return Array.from(this.myProcesses); } catch (e) { console.error("Failed to get processes:", e); return null; } } public async requestDataFromPeers(processId: string, stateIds: string[], roles: Record[]) { console.log('Requesting data from peers'); const membersList = this.getAllMembers(); try { const res = this.sdkClient.request_data(processId, stateIds, roles, membersList); await this.handleApiReturn(res); } catch (e) { console.error(e); } } public hexToBlob(hexString: string): Blob { const uint8Array = this.hexToUInt8Array(hexString); return new Blob([uint8Array], { type: "application/octet-stream" }); } public hexToUInt8Array(hexString: string): Uint8Array { if (hexString.length % 2 !== 0) { throw new Error("Invalid hex string: length must be even"); } const uint8Array = new Uint8Array(hexString.length / 2); for (let i = 0; i < hexString.length; i += 2) { uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16); } return uint8Array; } public async blobToHex(blob: Blob): Promise { const buffer = await blob.arrayBuffer(); const bytes = new Uint8Array(buffer); return Array.from(bytes) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); } public getHashForFile(commitedIn: string, label: string, fileBlob: { type: string; data: Uint8Array }): string { return this.sdkClient.hash_value(fileBlob, commitedIn, label); } public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult { return this.sdkClient.get_merkle_proof(processState, attributeName); } public validateMerkleProof(proof: MerkleProofResult, hash: string): boolean { try { return this.sdkClient.validate_merkle_proof(proof, hash); } catch (e) { throw new Error(`Failed to validate merkle proof: ${e}`); } } public getLastCommitedState(process: Process): ProcessState | null { if (process.states.length === 0) return null; const processTip = process.states[process.states.length - 1].commited_in; const lastCommitedState = process.states.findLast(state => state.commited_in !== processTip); if (lastCommitedState) { return lastCommitedState; } else { return null; } } public getLastCommitedStateIndex(process: Process): number | null { if (process.states.length === 0) return null; const processTip = process.states[process.states.length - 1].commited_in; for (let i = process.states.length - 1; i >= 0; i--) { if (process.states[i].commited_in !== processTip) { return i; } } return null; } public getStateFromId(process: Process, stateId: string): ProcessState | null { if (process.states.length === 0) return null; const state = process.states.find(state => state.state_id === stateId); if (state) { return state; } else { return null; } } public getNextStateAfterId(process: Process, stateId: string): ProcessState | null { if (process.states.length === 0) return null; const index = process.states.findIndex(state => state.state_id === stateId); if (index !== -1 && index < process.states.length - 1) { return process.states[index + 1]; } return null; } public isPairingProcess(roles: Record): boolean { if (Object.keys(roles).length != 1) { return false } const pairingRole = roles['pairing']; if (pairingRole) { // For now that's enough, we should probably test more things return true; } else { return false; } } public async updateMemberPublicName(process: Process, newName: string): Promise { const publicData = { 'memberPublicName': newName }; return await this.updateProcess(process, {}, publicData, null); } public async generateProcessPdf(processId: string, processState: ProcessState): Promise { const pdfDoc = await PDFDocument.create(); const page = pdfDoc.addPage([595.28, 841.89]); const font = await pdfDoc.embedFont(StandardFonts.Helvetica); const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); const drawText = (text: string, x: number, y: number, opts: { size?: number, bold?: boolean } = {}) => { const fontSize = opts.size || 12; const usedFont = opts.bold ? fontBold : font; page.drawText(text, { x, y, size: fontSize, font: usedFont, color: rgb(0, 0, 0) }); }; let y = 800; // Header drawText("Cabinet de Maître Jean Dupont", 50, y, { bold: true }); drawText("Notaire à Paris", 50, y -= 15); drawText("12 rue des Archives", 50, y -= 15); drawText("75003 Paris", 50, y -= 15); drawText("Téléphone : 01 23 45 67 89", 50, y -= 15); drawText("Email : contact@notairedupont.fr", 50, y -= 15); // Client y -= 30; drawText("Client : Mme Sophie Martin", 50, y); drawText("8 avenue de la Liberté", 50, y -= 15); drawText("69003 Lyon", 50, y -= 15); drawText("Email : sophie.martin@email.com", 50, y -= 15); // Title y -= 40; drawText("Certificat de Validation de Données", 50, y, { size: 14, bold: true }); // Certification paragraph y -= 40; const certText = `Je soussigné, Maître Jean Dupont, notaire à Paris, certifie par la présente que les données suivantes ont été vérifiées et horodatées à l’aide d’une empreinte cryptographique enregistrée sur la blockchain Bitcoin.`; page.drawText(certText, { x: 50, y, size: 11, font, lineHeight: 14, maxWidth: 500 }); // Dossier number y -= 60; drawText("Numéro de dossier : N-2025-0456-PAR", 50, y); // Process ID y -= 40; drawText(`Identifiant du process: ${processId.split(':')[0]}`, 50, y); // Hash table y -= 40; drawText("Nom", 50, y -= 20, { bold: true }); drawText("Empreinte cryptographique (SHA-256)", 150, y, { bold: true }); for (const [label, hash] of Object.entries(processState.pcd_commitment)) { drawText(label, 50, y -= 18); drawText(hash, 150, y); } // Add the state id as hash total drawText('Ensemble', 50, y -= 18); drawText(processState.state_id, 150, y); // Transaction y -= 40; drawText("Transaction enregistrée sur la blockchain Bitcoin :", 50, y); drawText(processState.commited_in, 50, y -= 15); // Date & signature y -= 50; drawText("Fait à Paris, le 10 juin 2025.", 50, y); drawText("Généré automatiquement par lecoffre.io", 50, y -= 30); // drawText("Signature du notaire : ___________________________", 50, y -= 30); const pdfBytes = await pdfDoc.save(); // Download const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `Certificat_Validation_${processId.slice(0,8)}-${processState.state_id.slice(0,8)}.pdf`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } }