Merge remote-tracking branch 'origin/dev' into cicd
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 2m7s

This commit is contained in:
omaroughriss 2025-07-23 13:40:06 +02:00
commit 205796d22a
6 changed files with 339 additions and 95 deletions

View File

@ -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',
@ -53,4 +55,9 @@ 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',
// Account management
ADD_DEVICE = 'ADD_DEVICE',
DEVICE_ADDED = 'DEVICE_ADDED',
}

View File

@ -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',
@ -139,17 +140,22 @@ export async function init(): Promise<void> {
(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);
}
// 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();
@ -591,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) {
@ -675,7 +700,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(
{
@ -722,8 +747,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 +770,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 +853,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}`);
}

View File

@ -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;
}
});

View File

@ -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);
@ -323,6 +323,38 @@ export class Database {
});
}
public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> {
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<any | null> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
@ -341,23 +373,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<any[]>((resolve, reject) => {
const request = store.getAllKeys();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
}),
new Promise<any[]>((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
}),
]);
return new Promise((resolve, reject) => {
const result: Record<string, any> = {};
const cursor = store.openCursor();
// Combine keys and values into an object
const result: Record<string, any> = Object.fromEntries(keys.map((key, index) => [key, values[index]]));
return result;
cursor.onsuccess = (event) => {
const request = event.target as IDBRequest<IDBCursorWithValue | null>;
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;

View File

@ -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) {

View File

@ -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';
@ -23,6 +23,7 @@ export default class Services {
private processId: string | null = null;
private stateId: string | null = null;
private sdkClient: any;
private processesCache: Record<string, Process> = {};
private myProcesses: Set<string> = new Set();
private notifications: any[] | null = null;
private subscriptions: { element: Element; event: string; eventHandler: string }[] = [];
@ -61,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) {
@ -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;
@ -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);
@ -660,6 +673,41 @@ export default class Services {
await navigate('account');
}
public async updateDevice(): Promise<void> {
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');
@ -673,7 +721,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');
@ -693,11 +753,15 @@ 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 {
public dumpDeviceFromMemory(): Device {
try {
return this.sdkClient.dump_device();
} catch (e) {
@ -722,7 +786,7 @@ export default class Services {
}
}
async saveDeviceInDatabase(device: any): Promise<void> {
async saveDeviceInDatabase(device: Device): Promise<void> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
@ -740,14 +804,13 @@ export default class Services {
}
}
async getDeviceFromDatabase(): Promise<string | null> {
async getDeviceFromDatabase(): Promise<Device | null> {
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;
}
@ -760,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;
@ -785,30 +847,22 @@ export default class Services {
rolesContainsUs(roles: Record<string, RoleDefinition>): 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<string, RoleDefinition>, 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<string, RoleDefinition>, pairingProcessId: string): boolean {
for (const roleDef of Object.values(roles)) {
if (roleDef.members.includes(pairingProcessId)) {
return true;
}
}
return res;
return false;
}
async dumpWallet() {
@ -834,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);
}
@ -854,14 +907,33 @@ export default class Services {
}
}
public async batchSaveProcessesToDb(processes: Record<string, Process>) {
if (Object.keys(processes).length === 0) {
return;
}
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';
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}`);
}
@ -927,43 +999,36 @@ export default class Services {
}
public async getProcess(processId: string): Promise<Process | null> {
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<Record<string, Process>> {
const db = await Database.getInstance();
const processes: Record<string, Process> = 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<string[]> {
// 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<string, Process>) {
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 })) });
} catch (e) {
throw e;
}
await this.restoreProcessesFromDB();
@ -976,6 +1041,7 @@ export default class Services {
const processes: Record<string, Process> = 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!');
@ -1033,7 +1099,7 @@ export default class Services {
}
}
async decodeValue(value: number[]): Promise<any | null> {
decodeValue(value: number[]): any | null {
try {
return this.sdkClient.decode_value(value);
} catch (e) {
@ -1215,7 +1281,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<string, Process> = {};
for (const [processId, process] of Object.entries(newProcesses)) {
const existing = await this.getProcess(processId);
if (existing) {
@ -1235,6 +1315,25 @@ 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
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
@ -1246,9 +1345,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) {
@ -1301,9 +1402,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));
@ -1340,7 +1444,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;
@ -1348,24 +1452,32 @@ export default class Services {
}
public async getMyProcesses(): Promise<string[] | null> {
// 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<string>(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);
@ -1418,6 +1530,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;
@ -1440,6 +1560,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);