From 207b308173ad894ecafa51f7fb4d438560b308b0 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 30 Jun 2025 22:45:25 +0200 Subject: [PATCH 01/30] Add getMerkleProofForFile --- src/services/service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/service.ts b/src/services/service.ts index 022d873..2d9f6d5 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1,7 +1,7 @@ import { INotification } from '~/models/notification.model'; import { IProcess } from '~/models/process.model'; import { initWebsocket, sendMessage } from '../websockets'; -import { ApiReturn, Device, HandshakeMessage, Member, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../../pkg/sdk_client'; +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'; @@ -1414,6 +1414,10 @@ export default class Services { return this.sdkClient.hash_value(fileBlob, commitedIn, label); } + public getMerkleProofForFile(processState: ProcessState, attributeName: string): MerkleProofResult { + return this.sdkClient.get_merkle_proof(processState, attributeName); + } + public getLastCommitedState(process: Process): ProcessState | null { if (process.states.length === 0) return null; const processTip = process.states[process.states.length - 1].commited_in; From 7c39795cef3e4be19535a4a08eeaf41b9da62dec Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 30 Jun 2025 22:45:50 +0200 Subject: [PATCH 02/30] Add HASH_VALUE and GET_MERKLE_PROOF --- src/router.ts | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/router.ts b/src/router.ts index b6da5a8..8f6d549 100755 --- a/src/router.ts +++ b/src/router.ts @@ -691,6 +691,62 @@ export async function registerAllListeners() { } } + const handleHashValue = async (event: MessageEvent) => { + if (event.data.type !== MessageType.HASH_VALUE) return; + + console.log('handleHashValue', event.data); + + try { + const { accessToken, commitedIn, label, fileBlob } = event.data; + + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + const hash = services.getHashForFile(commitedIn, label, fileBlob); + + window.parent.postMessage( + { + type: MessageType.VALUE_HASHED, + hash, + messageId: event.data.messageId + }, + event.origin + ); + } catch (e) { + const errorMsg = `Failed to hash value: ${e}`; + errorResponse(errorMsg, event.origin, event.data.messageId); + } + } + + const handleGetMerkleProof = async (event: MessageEvent) => { + if (event.data.type !== MessageType.GET_MERKLE_PROOF) return; + + console.log('handleGetMerkleProof', event.data); + + try { + const { accessToken, processState, attributeName } = event.data; + + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + const proof = services.getMerkleProofForFile(processState, attributeName); + + window.parent.postMessage( + { + type: MessageType.MERKLE_PROOF_RETRIEVED, + proof, + messageId: event.data.messageId + }, + event.origin + ); + } catch (e) { + const errorMsg = `Failed to get merkle proof: ${e}`; + errorResponse(errorMsg, event.origin, event.data.messageId); + } + } + window.removeEventListener('message', handleMessage); window.addEventListener('message', handleMessage); @@ -733,6 +789,12 @@ export async function registerAllListeners() { case MessageType.DECODE_PUBLIC_DATA: await handleDecodePublicData(event); break; + case MessageType.HASH_VALUE: + await handleHashValue(event); + break; + case MessageType.GET_MERKLE_PROOF: + await handleGetMerkleProof(event); + break; default: console.warn(`Unhandled message type: ${event.data.type}`); } From 926f41d270d470515a111ceec9a5c7bf057e76ae Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 1 Jul 2025 16:09:15 +0200 Subject: [PATCH 03/30] Fix race condition on getMyProcesses --- src/services/service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/service.ts b/src/services/service.ts index 2d9f6d5..5943c3b 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1351,21 +1351,23 @@ export default class Services { 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 (this.myProcesses && this.myProcesses.has(processId)) { + if (newMyProcesses.has(processId)) { continue; } try { const roles = this.getRoles(process); if (roles && this.rolesContainsUs(roles)) { - this.myProcesses.add(processId); + 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); From 10589b056f1a7f4f262283bcbe5c660532a663d0 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 1 Jul 2025 18:03:04 +0200 Subject: [PATCH 04/30] Solve potential race conditions in dumpStore() --- src/services/database.service.ts | 34 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/services/database.service.ts b/src/services/database.service.ts index 663de5e..ac942b9 100755 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -341,23 +341,25 @@ export class Database { const store = tx.objectStore(storeName); try { - // Wait for both getAllKeys() and getAll() to resolve - const [keys, values] = await Promise.all([ - new Promise((resolve, reject) => { - const request = store.getAllKeys(); - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }), - new Promise((resolve, reject) => { - const request = store.getAll(); - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); - }), - ]); + return new Promise((resolve, reject) => { + const result: Record = {}; + const cursor = store.openCursor(); - // Combine keys and values into an object - const result: Record = Object.fromEntries(keys.map((key, index) => [key, values[index]])); - return result; + cursor.onsuccess = (event) => { + const request = event.target as IDBRequest; + const cursor = request.result; + if (cursor) { + result[cursor.key as string] = cursor.value; + cursor.continue(); + } else { + resolve(result); + } + }; + + cursor.onerror = () => { + reject(cursor.error); + }; + }); } catch (error) { console.error('Error fetching data from IndexedDB:', error); throw error; From 44f0d8c6c917ae2dd8fb25e6f6630875bae45ca5 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 2 Jul 2025 13:50:54 +0200 Subject: [PATCH 05/30] [bug] fix `rolesContainsMember` --- src/services/service.ts | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/services/service.ts b/src/services/service.ts index 5943c3b..17f80cb 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -785,30 +785,22 @@ export default class Services { rolesContainsUs(roles: Record): boolean { let us; try { - us = this.sdkClient.get_member(); + us = this.sdkClient.get_pairing_process_id(); } catch (e) { throw e; } - return this.rolesContainsMember(roles, us.sp_addresses); + return this.rolesContainsMember(roles, us); } - rolesContainsMember(roles: Record, member: string[]): boolean { - let res = false; - for (const [roleName, roleDef] of Object.entries(roles)) { - for (const otherMember of roleDef.members) { - if (res) { return true } - // Get the addresses for the member - const otherMemberAddresses: string[] | null = this.getAddressesForMemberId(otherMember); - if (!otherMemberAddresses) { - // console.error('Failed to get addresses for member', otherMember); - continue; - } - res = this.compareMembers(member, otherMemberAddresses); + rolesContainsMember(roles: Record, pairingProcessId: string): boolean { + for (const roleDef of Object.values(roles)) { + if (roleDef.members.includes(pairingProcessId)) { + return true; } } - return res; + return false; } async dumpWallet() { From 4e109e8fba23781644c417f1f4c1ea2db7b33a21 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 3 Jul 2025 17:54:07 +0200 Subject: [PATCH 06/30] Add validateMerkleProof --- src/services/service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/services/service.ts b/src/services/service.ts index 17f80cb..8f9ac64 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1412,6 +1412,14 @@ export default class Services { 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; From 7391a08a0108068984d4fb636f739ef6ede13d42 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 3 Jul 2025 17:54:36 +0200 Subject: [PATCH 07/30] Add VALIDATE_MERKLE_PROOF MessageType --- src/models/process.model.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/models/process.model.ts b/src/models/process.model.ts index f6faa6c..44f5dc6 100755 --- a/src/models/process.model.ts +++ b/src/models/process.model.ts @@ -53,4 +53,6 @@ export enum MessageType { VALUE_HASHED = 'VALUE_HASHED', GET_MERKLE_PROOF = 'GET_MERKLE_PROOF', MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED', + VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF', + MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED', } From 989263d44af19ddbb2428eb02b498530bab06610 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 3 Jul 2025 17:56:03 +0200 Subject: [PATCH 08/30] handleValidateMerkleProof --- src/router.ts | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/router.ts b/src/router.ts index 8f6d549..c2f348f 100755 --- a/src/router.ts +++ b/src/router.ts @@ -10,6 +10,7 @@ import { prepareAndSendPairingTx } from './utils/sp-address.utils'; import ModalService from './services/modal.service'; import { MessageType } from './models/process.model'; import { splitPrivateData, isValid32ByteHex } from './utils/service.utils'; +import { MerkleProofResult } from 'pkg/sdk_client'; const routes: { [key: string]: string } = { home: '/src/pages/home/home.html', @@ -722,8 +723,6 @@ export async function registerAllListeners() { const handleGetMerkleProof = async (event: MessageEvent) => { if (event.data.type !== MessageType.GET_MERKLE_PROOF) return; - console.log('handleGetMerkleProof', event.data); - try { const { accessToken, processState, attributeName } = event.data; @@ -747,6 +746,41 @@ export async function registerAllListeners() { } } + const handleValidateMerkleProof = async (event: MessageEvent) => { + if (event.data.type !== MessageType.VALIDATE_MERKLE_PROOF) return; + + try { + const { accessToken, merkleProof, documentHash } = event.data; + + if (!accessToken || !(await tokenService.validateToken(accessToken, event.origin))) { + throw new Error('Invalid or expired session token'); + } + + // Try to parse the proof + // We will validate it's a MerkleProofResult in the wasm + let parsedMerkleProof: MerkleProofResult; + try { + parsedMerkleProof= JSON.parse(merkleProof); + } catch (e) { + throw new Error('Provided merkleProof is not a valid json object'); + } + + const res = services.validateMerkleProof(parsedMerkleProof, documentHash); + + window.parent.postMessage( + { + type: MessageType.MERKLE_PROOF_VALIDATED, + isValid: res, + messageId: event.data.messageId + }, + event.origin + ); + } catch (e) { + const errorMsg = `Failed to get merkle proof: ${e}`; + errorResponse(errorMsg, event.origin, event.data.messageId); + } + } + window.removeEventListener('message', handleMessage); window.addEventListener('message', handleMessage); @@ -795,6 +829,9 @@ export async function registerAllListeners() { case MessageType.GET_MERKLE_PROOF: await handleGetMerkleProof(event); break; + case MessageType.VALIDATE_MERKLE_PROOF: + await handleValidateMerkleProof(event); + break; default: console.warn(`Unhandled message type: ${event.data.type}`); } From 189bd3d252c5547eae936007c60b8b3992a88533 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 4 Jul 2025 12:26:11 +0200 Subject: [PATCH 09/30] Don't throw error if unpaired while trying to get my processes --- src/services/database.service.ts | 2 +- src/services/service.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/database.service.ts b/src/services/database.service.ts index ac942b9..49a8da8 100755 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -147,7 +147,7 @@ export class Database { const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!)); const service = await Services.getInstance(); const payload = await service.getMyProcesses(); - if (payload!.length != 0) { + if (payload && payload.length != 0) { activeWorker?.postMessage({ type: 'SCAN', payload }); } }, 5000); diff --git a/src/services/service.ts b/src/services/service.ts index 8f9ac64..5915a5f 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1340,6 +1340,12 @@ export default class Services { } 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(); From 39f24114e19845c4d9fdc7a08854e3a1b506a65b Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 7 Jul 2025 15:20:00 +0200 Subject: [PATCH 10/30] Rm uneccessary async on getDeviceAddress() --- src/services/service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/services/service.ts b/src/services/service.ts index 5915a5f..f37ba07 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -201,7 +201,7 @@ export default class Services { // Ensure the amount is available before proceeding await this.getTokensFromFaucet(); let unconnectedAddresses = []; - const myAddress = await this.getDeviceAddress(); + const myAddress = this.getDeviceAddress(); for (const member of members) { const sp_addresses = member.sp_addresses; if (!sp_addresses || sp_addresses.length === 0) continue; @@ -693,8 +693,12 @@ export default class Services { return amount; } - async getDeviceAddress() { - return await this.sdkClient.get_address(); + getDeviceAddress(): string { + try { + return this.sdkClient.get_address(); + } catch (e) { + throw new Error(`Failed to get device address: ${e}`); + } } public dumpDeviceFromMemory(): string { From d8c2b22c3d8e078d169d4bce8a6efabfec47e88b Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 7 Jul 2025 15:21:25 +0200 Subject: [PATCH 11/30] Rm uneccessary async on decodeValue() --- src/router.ts | 2 +- src/services/service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/router.ts b/src/router.ts index c2f348f..33f5f93 100755 --- a/src/router.ts +++ b/src/router.ts @@ -676,7 +676,7 @@ export async function registerAllListeners() { throw new Error('Invalid or expired session token'); } - const decodedData = await services.decodeValue(encodedData); + const decodedData = services.decodeValue(encodedData); window.parent.postMessage( { diff --git a/src/services/service.ts b/src/services/service.ts index f37ba07..07d0447 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1029,7 +1029,7 @@ export default class Services { } } - async decodeValue(value: number[]): Promise { + decodeValue(value: number[]): any | null { try { return this.sdkClient.decode_value(value); } catch (e) { @@ -1336,7 +1336,7 @@ export default class Services { const lastCommitedState = this.getLastCommitedState(process); if (lastCommitedState && lastCommitedState.public_data) { const processName = lastCommitedState!.public_data['processName']; - if (processName) { return processName } + if (processName) { return this.decodeValue(processName) } else { return null } } else { return null; From d9b8817ecc7d5620cc747098729757f6e49ea3bb Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 7 Jul 2025 15:22:23 +0200 Subject: [PATCH 12/30] Create connections with devices in a pairing process --- src/services/service.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/services/service.ts b/src/services/service.ts index 07d0447..77d69de 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -386,6 +386,19 @@ export default class Services { 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); From deebcefc3dae83cd27f08be28862c9f117e99a91 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 7 Jul 2025 15:23:05 +0200 Subject: [PATCH 13/30] Track states on pairing process --- src/services/service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/services/service.ts b/src/services/service.ts index 77d69de..2563fe2 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1237,6 +1237,14 @@ export default class Services { if (this.rolesContainsUs(state.roles)) { new_states.push(state.state_id); roles.push(state.roles); + } else if (state.public_data && state.public_data['pairedAddresses']) { + // This is a pairing process + const pairedAddresses = this.decodeValue(state.public_data['pairedAddresses']); + // Are we part of it? + if (pairedAddresses.includes(this.getDeviceAddress())) { + // We save the process to db + await this.saveProcessToDb(processId, process as Process); + } } } } From 5a8c31df324ab98734988b65561afef2e6bbe7c4 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 7 Jul 2025 15:23:47 +0200 Subject: [PATCH 14/30] Add getUncommitedStates() --- src/services/service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/services/service.ts b/src/services/service.ts index 2563fe2..ab220f8 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1473,6 +1473,13 @@ export default class Services { return null; } + public getUncommitedStates(process: Process): ProcessState[] { + if (process.states.length === 0) return []; + const processTip = process.states[process.states.length - 1].commited_in; + const res = process.states.filter(state => state.commited_in === processTip); + return res.filter(state => state.state_id !== EMPTY32BYTES); + } + 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); From 5119d04243b6375923563de663f61bafe7bef8e7 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 7 Jul 2025 15:24:15 +0200 Subject: [PATCH 15/30] Don't retry commit message for `Not enough members to validate` errors --- src/services/service.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/services/service.ts b/src/services/service.ts index ab220f8..87dfa49 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1318,9 +1318,12 @@ export default class Services { 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; } + const dontRetry = [ + 'State is identical to the previous state', + 'Not enough valid proofs', + 'Not enough members to validate', + ]; + if (dontRetry.includes(errorMsg)) { return; } // Wait and retry setTimeout(async () => { await this.sendCommitMessage(JSON.stringify(content)); From e9fc0b845413a5f7fdc2920eed204402f0f6182b Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 7 Jul 2025 15:24:55 +0200 Subject: [PATCH 16/30] Rm await on getDeviceAddress() --- src/services/modal.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/modal.service.ts b/src/services/modal.service.ts index 3c2e6c6..284bd70 100755 --- a/src/services/modal.service.ts +++ b/src/services/modal.service.ts @@ -132,7 +132,7 @@ export default class ModalService { console.log("MEMBERS:", members); // We take all the addresses except our own const service = await Services.getInstance(); - const localAddress = await service.getDeviceAddress(); + const localAddress = service.getDeviceAddress(); for (const member of members) { if (member.sp_addresses) { for (const address of member.sp_addresses) { From 62ccfec315ab18735476df82085ccc05297c6ec4 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 7 Jul 2025 15:26:40 +0200 Subject: [PATCH 17/30] Add GET_MEMBER_ADDRESSES and ADD_DEVICE messages --- src/models/process.model.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/models/process.model.ts b/src/models/process.model.ts index 44f5dc6..6bcdf3c 100755 --- a/src/models/process.model.ts +++ b/src/models/process.model.ts @@ -39,6 +39,8 @@ export enum MessageType { DATA_RETRIEVED = 'DATA_RETRIEVED', DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA', PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED', + GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES', + MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED', // Processes CREATE_PROCESS = 'CREATE_PROCESS', PROCESS_CREATED = 'PROCESS_CREATED', @@ -55,4 +57,7 @@ export enum MessageType { MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED', VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF', MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED', + // Account management + ADD_DEVICE = 'ADD_DEVICE', + DEVICE_ADDED = 'DEVICE_ADDED', } From 18d46531a07d05076dcc3f94f4ef39af9a2b4f5b Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 8 Jul 2025 17:18:46 +0200 Subject: [PATCH 18/30] Correctly handle states in pairDevice() --- src/services/service.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/services/service.ts b/src/services/service.ts index 87dfa49..92e7943 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -686,7 +686,19 @@ export default class Services { let spAddressList: string[] = []; try { - const encodedSpAddressList = process.states[0].public_data['pairedAddresses']; + let encodedSpAddressList: number[] = []; + if (this.stateId) { + const state = process.states.find(state => state.state_id === this.stateId); + if (state) { + encodedSpAddressList = state.public_data['pairedAddresses']; + } + } else { + // We assume it's the last commited state + const lastCommitedState = this.getLastCommitedState(process); + if (lastCommitedState) { + encodedSpAddressList = lastCommitedState.public_data['pairedAddresses']; + } + } spAddressList = this.sdkClient.decode_value(encodedSpAddressList); if (!spAddressList || spAddressList.length == 0) { throw new Error('Empty pairedAddresses'); From 5a98fac745c9fc58e6fb613d9b4c471acb0a10ef Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 8 Jul 2025 17:21:29 +0200 Subject: [PATCH 19/30] Update our pairing addresses on receiving updates --- src/services/service.ts | 61 +++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/src/services/service.ts b/src/services/service.ts index 92e7943..4174bd8 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -673,6 +673,41 @@ export default class Services { await navigate('account'); } + public async updateDevice(): Promise { + let myPairingProcessId: string; + try { + myPairingProcessId = this.getPairingProcessId(); + } catch (e) { + console.error('Failed to get pairing process id'); + return; + } + + const myPairingProcess = await this.getProcess(myPairingProcessId); + if (!myPairingProcess) { + console.error('Unknown pairing process'); + return; + } + const myPairingState = this.getLastCommitedState(myPairingProcess); + if (myPairingState) { + const encodedSpAddressList = myPairingState.public_data['pairedAddresses']; + const spAddressList = this.decodeValue(encodedSpAddressList); + if (spAddressList.length === 0) { + console.error('Empty pairedAddresses'); + return; + } + // We can check if our address is included and simply unpair if it's not + if (!spAddressList.includes(this.getDeviceAddress())) { + await this.unpairDevice(); + return; + } + // We can update the device with the new addresses + this.sdkClient.unpair_device(); + this.sdkClient.pair_device(myPairingProcessId, spAddressList); + const newDevice = this.dumpDeviceFromMemory(); + await this.saveDeviceInDatabase(newDevice); + } + } + public async pairDevice() { if (!this.processId) { console.error('No processId set'); @@ -1249,14 +1284,6 @@ export default class Services { if (this.rolesContainsUs(state.roles)) { new_states.push(state.state_id); roles.push(state.roles); - } else if (state.public_data && state.public_data['pairedAddresses']) { - // This is a pairing process - const pairedAddresses = this.decodeValue(state.public_data['pairedAddresses']); - // Are we part of it? - if (pairedAddresses.includes(this.getDeviceAddress())) { - // We save the process to db - await this.saveProcessToDb(processId, process as Process); - } } } } @@ -1266,6 +1293,24 @@ export default class Services { await this.requestDataFromPeers(processId, new_states, roles); } + // Just to be sure check if that's a pairing process + const lastCommitedState = this.getLastCommitedState(process); + if (lastCommitedState && lastCommitedState.public_data && lastCommitedState.public_data['pairedAddresses']) { + // This is a pairing process + try { + const pairedAddresses = this.decodeValue(lastCommitedState.public_data['pairedAddresses']); + // Are we part of it? + if (pairedAddresses && pairedAddresses.length > 0 && pairedAddresses.includes(this.getDeviceAddress())) { + // We save the process to db + await this.saveProcessToDb(processId, process as Process); + // We update the device + await this.updateDevice(); + } + } catch (e) { + console.error('Failed to check for pairing process:', e); + } + } + // Otherwise we're probably just in the initial loading at page initialization // We may learn an update for this process From 1dad1d4e2bd575fc8afa4d48ddb3d261f9269850 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 1 Jul 2025 18:01:42 +0200 Subject: [PATCH 20/30] Add BATCH_WRITING to database.worker --- src/service-workers/database.worker.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/service-workers/database.worker.js b/src/service-workers/database.worker.js index 54b432c..a9e0548 100755 --- a/src/service-workers/database.worker.js +++ b/src/service-workers/database.worker.js @@ -45,6 +45,21 @@ self.addEventListener('message', async (event) => { } catch (error) { event.ports[0].postMessage({ status: 'error', message: error.message }); } + } else if (data.type === 'BATCH_WRITING') { + const { storeName, objects } = data.payload; + const db = await openDatabase(); + const tx = db.transaction(storeName, 'readwrite'); + const store = tx.objectStore(storeName); + + for (const { key, object } of objects) { + if (key) { + await store.put(object, key); + } else { + await store.put(object); + } + } + + await tx.done; } }); From 93d610e9427531986530de61b7440990b4183303 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 1 Jul 2025 18:02:08 +0200 Subject: [PATCH 21/30] Add batchWriting() to database --- src/services/database.service.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/services/database.service.ts b/src/services/database.service.ts index 49a8da8..c6cd73e 100755 --- a/src/services/database.service.ts +++ b/src/services/database.service.ts @@ -323,6 +323,38 @@ export class Database { }); } + public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise { + return new Promise(async (resolve, reject) => { + if (!this.serviceWorkerRegistration) { + this.serviceWorkerRegistration = await navigator.serviceWorker.ready; + } + + const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration); + const messageChannel = new MessageChannel(); + + messageChannel.port1.onmessage = (event) => { + if (event.data.status === 'success') { + resolve(); + } else { + const error = event.data.message; + reject(new Error(error || 'Unknown error occurred while adding objects')); + } + }; + + try { + activeWorker?.postMessage( + { + type: 'BATCH_WRITING', + payload, + }, + [messageChannel.port2], + ); + } catch (error) { + reject(new Error(`Failed to send message to service worker: ${error}`)); + } + }); + } + public async getObject(storeName: string, key: string): Promise { const db = await this.getDb(); const tx = db.transaction(storeName, 'readonly'); From 19b2ab994ebbe83e50927a7b9194ee50664cb136 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 1 Jul 2025 18:02:46 +0200 Subject: [PATCH 22/30] Batch writes processes at initialization --- src/services/service.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/service.ts b/src/services/service.ts index 4174bd8..8303ef6 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1018,8 +1018,12 @@ export default class Services { 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}); + const storeName = 'processes'; + try { + await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) }); + this.processesCache = processes; + } catch (e) { + throw e; } await this.restoreProcessesFromDB(); From 58fed7a53b500326cba956e356668bf82c014aab Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 1 Jul 2025 18:06:44 +0200 Subject: [PATCH 23/30] use a processCache for optimization --- src/services/service.ts | 53 +++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/src/services/service.ts b/src/services/service.ts index 8303ef6..6613d3a 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -23,6 +23,7 @@ export default class Services { private processId: string | null = null; private stateId: string | null = null; private sdkClient: any; + private processesCache: Record = {}; private myProcesses: Set = new Set(); private notifications: any[] | null = null; private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; @@ -912,12 +913,16 @@ export default class Services { public async saveProcessToDb(processId: string, process: Process) { const db = await Database.getInstance(); + const storeName = 'processes'; try { await db.addObject({ - storeName: 'processes', + storeName, object: process, key: processId, }); + + // Update the process in the cache + this.processesCache[processId] = process; } catch (e) { console.error(`Failed to save process ${processId}: ${e}`); } @@ -983,39 +988,29 @@ export default class Services { } public async getProcess(processId: string): Promise { - const db = await Database.getInstance(); - return await db.getObject('processes', processId); + if (this.processesCache[processId]) { + return this.processesCache[processId]; + } else { + const db = await Database.getInstance(); + const process = await db.getObject('processes', processId); + return process; + } } public async getProcesses(): Promise> { - const db = await Database.getInstance(); - - const processes: Record = await db.dumpStore('processes'); - return processes; + if (Object.keys(this.processesCache).length > 0) { + return this.processesCache; + } else { + try { + const db = await Database.getInstance(); + this.processesCache = await db.dumpStore('processes'); + return this.processesCache; + } catch (e) { + throw e; + } + } } - // 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(); const storeName = 'processes'; From dbb7f67154b7ca8f64e8700529ccd6108e77ae95 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 2 Jul 2025 11:34:30 +0200 Subject: [PATCH 24/30] Don't automatically connect to realys in init() --- src/router.ts | 5 +++++ src/services/service.ts | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/router.ts b/src/router.ts index 33f5f93..1b1b6f2 100755 --- a/src/router.ts +++ b/src/router.ts @@ -148,9 +148,14 @@ export async function init(): Promise { } else { services.restoreDevice(device); } + + // If we create a new device, we most probably don't have anything in db, but just in case await services.restoreProcessesFromDB(); await services.restoreSecretsFromDB(); + // We connect to all relays now + await services.connectAllRelays(); + // We register all the event listeners if we run in an iframe if (window.self !== window.top) { await registerAllListeners(); diff --git a/src/services/service.ts b/src/services/service.ts index 6613d3a..d63c715 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -62,7 +62,6 @@ export default class Services { for (const wsurl of Object.values(BOOTSTRAPURL)) { this.updateRelay(wsurl, ''); } - await this.connectAllRelays(); } public setProcessId(processId: string | null) { From aae11200d4b3c5f7f5c6960b0b2214f158d165a8 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 2 Jul 2025 11:34:54 +0200 Subject: [PATCH 25/30] Add `batchSaveProcessesToDb()` --- src/services/service.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/service.ts b/src/services/service.ts index d63c715..d1e410e 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -910,6 +910,17 @@ export default class Services { } } + public async batchSaveProcessesToDb(processes: Record) { + const db = await Database.getInstance(); + const storeName = 'processes'; + try { + await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) }); + this.processesCache = { ...this.processesCache, ...processes }; + } catch (e) { + throw e; + } + } + public async saveProcessToDb(processId: string, process: Process) { const db = await Database.getInstance(); const storeName = 'processes'; From a027004bd0fb91c2a537424827ec136b5d5e48f9 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 2 Jul 2025 11:35:36 +0200 Subject: [PATCH 26/30] [bug] Set cache in `restoreProcessesFromDb`, not Backup --- src/services/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/service.ts b/src/services/service.ts index d1e410e..2815d4f 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1026,7 +1026,6 @@ export default class Services { const storeName = 'processes'; try { await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) }); - this.processesCache = processes; } catch (e) { throw e; } @@ -1041,6 +1040,7 @@ export default class Services { const processes: Record = await db.dumpStore('processes'); if (processes && Object.keys(processes).length != 0) { console.log(`Restoring ${Object.keys(processes).length} processes`); + this.processesCache = processes; this.sdkClient.set_process_cache(processes); } else { console.log('No processes to restore!'); From 5192745a48b8b104743f8c16008fba10b43ec939 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 2 Jul 2025 11:36:15 +0200 Subject: [PATCH 27/30] Refactor handshake message handling using bach writing --- src/services/service.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/services/service.ts b/src/services/service.ts index 2815d4f..de45e4b 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -1280,7 +1280,21 @@ export default class Services { setTimeout(async () => { const newProcesses: OutPointProcessMap = handshakeMsg.processes_list; - if (newProcesses && Object.keys(newProcesses).length !== 0) { + if (!newProcesses || Object.keys(newProcesses).length === 0) { + console.debug('Received empty processes list from', url); + return; + } + + if (this.processesCache && Object.keys(this.processesCache).length === 0) { + // We restored db but cache is empty, meaning we're starting from scratch + try { + await this.batchSaveProcessesToDb(newProcesses); + } catch (e) { + console.error('Failed to save processes to db:', e); + } + } else { + // We need to update our processes with what relay provides + const toSave: Record = {}; for (const [processId, process] of Object.entries(newProcesses)) { const existing = await this.getProcess(processId); if (existing) { @@ -1300,6 +1314,7 @@ export default class Services { if (new_states.length != 0) { // We request the new states await this.requestDataFromPeers(processId, new_states, roles); + toSave[processId] = process; } // Just to be sure check if that's a pairing process @@ -1329,9 +1344,11 @@ export default class Services { } else { // We add it to db console.log(`Saving ${processId} to db`); - await this.saveProcessToDb(processId, process as Process); + toSave[processId] = process; } } + + await this.batchSaveProcessesToDb(toSave); } }, 500) } catch (e) { From f0151fa55ee8a7abe77daafbace8617866df8895 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 2 Jul 2025 12:34:04 +0200 Subject: [PATCH 28/30] Don't try to batch write to db if objects is empty --- src/services/service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/service.ts b/src/services/service.ts index de45e4b..2a8212d 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -911,6 +911,10 @@ export default class Services { } public async batchSaveProcessesToDb(processes: Record) { + if (Object.keys(processes).length === 0) { + return; + } + const db = await Database.getInstance(); const storeName = 'processes'; try { From cb5297e6fe62ea80d23c26063e0eadd110158958 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 2 Jul 2025 12:34:39 +0200 Subject: [PATCH 29/30] Refactoring of handleUpdateProcess to try to commit again processes that have not been commited --- src/router.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/router.ts b/src/router.ts index 1b1b6f2..b24368b 100755 --- a/src/router.ts +++ b/src/router.ts @@ -597,9 +597,28 @@ export async function registerAllListeners() { if (!process) { throw new Error('Process not found'); } - const lastState = services.getLastCommitedState(process); + let lastState = services.getLastCommitedState(process); if (!lastState) { - throw new Error('Process doesn\'t have a commited state yet'); + const firstState = process.states[0]; + const roles = firstState.roles; + if (services.rolesContainsUs(roles)) { + const approveChangeRes= await services.approveChange(processId, firstState.state_id); + await services.handleApiReturn(approveChangeRes); + const prdUpdateRes = await services.createPrdUpdate(processId, firstState.state_id); + await services.handleApiReturn(prdUpdateRes); + } else { + if (firstState.validation_tokens.length > 0) { + // Try to send it again anyway + const res = await services.createPrdUpdate(processId, firstState.state_id); + await services.handleApiReturn(res); + } + } + // Wait a couple seconds + await new Promise(resolve => setTimeout(resolve, 2000)); + lastState = services.getLastCommitedState(process); + if (!lastState) { + throw new Error('Process doesn\'t have a commited state yet'); + } } const lastStateIndex = services.getLastCommitedStateIndex(process); if (lastStateIndex === null) { From d3e207c6dac006d4c02669d9d5ce13abd84e93b0 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 16 Jul 2025 11:31:29 +0200 Subject: [PATCH 30/30] [bug] fix types mismatch with `Device` --- src/router.ts | 4 ++-- src/services/service.ts | 15 ++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/router.ts b/src/router.ts index b24368b..8720137 100755 --- a/src/router.ts +++ b/src/router.ts @@ -140,11 +140,11 @@ export async function init(): Promise { (window as any).myService = services; const db = await Database.getInstance(); db.registerServiceWorker('/src/service-workers/database.worker.js'); - let device = await services.getDeviceFromDatabase(); + const device = await services.getDeviceFromDatabase(); console.log('🚀 ~ setTimeout ~ device:', device); if (!device) { - device = await services.createNewDevice(); + await services.createNewDevice(); } else { services.restoreDevice(device); } diff --git a/src/services/service.ts b/src/services/service.ts index 2a8212d..9020242 100755 --- a/src/services/service.ts +++ b/src/services/service.ts @@ -761,7 +761,7 @@ export default class Services { } } - public dumpDeviceFromMemory(): string { + public dumpDeviceFromMemory(): Device { try { return this.sdkClient.dump_device(); } catch (e) { @@ -786,7 +786,7 @@ export default class Services { } } - async saveDeviceInDatabase(device: any): Promise { + async saveDeviceInDatabase(device: Device): Promise { const db = await Database.getInstance(); const walletStore = 'wallet'; try { @@ -804,14 +804,13 @@ export default class Services { } } - async getDeviceFromDatabase(): Promise { + 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; + return dbRes['device']; } else { return null; } @@ -824,8 +823,7 @@ export default class Services { try { const device = await this.getDeviceFromDatabase(); if (device) { - const parsed: Device = JSON.parse(device); - const pairedMember = parsed['paired_member']; + const pairedMember = device['paired_member']; return pairedMember.sp_addresses; } else { return null; @@ -890,10 +888,9 @@ export default class Services { return spAddress; } - restoreDevice(device: string) { + public restoreDevice(device: Device) { try { this.sdkClient.restore_device(device); - const spAddress = this.sdkClient.get_address(); } catch (e) { console.error(e); }