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', DATA_RETRIEVED = 'DATA_RETRIEVED',
DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA', DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA',
PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED', PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED',
GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES',
MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED',
// Processes // Processes
CREATE_PROCESS = 'CREATE_PROCESS', CREATE_PROCESS = 'CREATE_PROCESS',
PROCESS_CREATED = 'PROCESS_CREATED', PROCESS_CREATED = 'PROCESS_CREATED',
@ -53,4 +55,9 @@ export enum MessageType {
VALUE_HASHED = 'VALUE_HASHED', VALUE_HASHED = 'VALUE_HASHED',
GET_MERKLE_PROOF = 'GET_MERKLE_PROOF', GET_MERKLE_PROOF = 'GET_MERKLE_PROOF',
MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED', 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 ModalService from './services/modal.service';
import { MessageType } from './models/process.model'; import { MessageType } from './models/process.model';
import { splitPrivateData, isValid32ByteHex } from './utils/service.utils'; import { splitPrivateData, isValid32ByteHex } from './utils/service.utils';
import { MerkleProofResult } from 'pkg/sdk_client';
const routes: { [key: string]: string } = { const routes: { [key: string]: string } = {
home: '/src/pages/home/home.html', home: '/src/pages/home/home.html',
@ -139,17 +140,22 @@ export async function init(): Promise<void> {
(window as any).myService = services; (window as any).myService = services;
const db = await Database.getInstance(); const db = await Database.getInstance();
db.registerServiceWorker('/src/service-workers/database.worker.js'); db.registerServiceWorker('/src/service-workers/database.worker.js');
let device = await services.getDeviceFromDatabase(); const device = await services.getDeviceFromDatabase();
console.log('🚀 ~ setTimeout ~ device:', device); console.log('🚀 ~ setTimeout ~ device:', device);
if (!device) { if (!device) {
device = await services.createNewDevice(); await services.createNewDevice();
} else { } else {
services.restoreDevice(device); 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.restoreProcessesFromDB();
await services.restoreSecretsFromDB(); await services.restoreSecretsFromDB();
// We connect to all relays now
await services.connectAllRelays();
// We register all the event listeners if we run in an iframe // We register all the event listeners if we run in an iframe
if (window.self !== window.top) { if (window.self !== window.top) {
await registerAllListeners(); await registerAllListeners();
@ -591,9 +597,28 @@ export async function registerAllListeners() {
if (!process) { if (!process) {
throw new Error('Process not found'); throw new Error('Process not found');
} }
const lastState = services.getLastCommitedState(process); let lastState = services.getLastCommitedState(process);
if (!lastState) { 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); const lastStateIndex = services.getLastCommitedStateIndex(process);
if (lastStateIndex === null) { if (lastStateIndex === null) {
@ -675,7 +700,7 @@ export async function registerAllListeners() {
throw new Error('Invalid or expired session token'); throw new Error('Invalid or expired session token');
} }
const decodedData = await services.decodeValue(encodedData); const decodedData = services.decodeValue(encodedData);
window.parent.postMessage( window.parent.postMessage(
{ {
@ -722,8 +747,6 @@ export async function registerAllListeners() {
const handleGetMerkleProof = async (event: MessageEvent) => { const handleGetMerkleProof = async (event: MessageEvent) => {
if (event.data.type !== MessageType.GET_MERKLE_PROOF) return; if (event.data.type !== MessageType.GET_MERKLE_PROOF) return;
console.log('handleGetMerkleProof', event.data);
try { try {
const { accessToken, processState, attributeName } = event.data; 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.removeEventListener('message', handleMessage);
window.addEventListener('message', handleMessage); window.addEventListener('message', handleMessage);
@ -795,6 +853,9 @@ export async function registerAllListeners() {
case MessageType.GET_MERKLE_PROOF: case MessageType.GET_MERKLE_PROOF:
await handleGetMerkleProof(event); await handleGetMerkleProof(event);
break; break;
case MessageType.VALIDATE_MERKLE_PROOF:
await handleValidateMerkleProof(event);
break;
default: default:
console.warn(`Unhandled message type: ${event.data.type}`); console.warn(`Unhandled message type: ${event.data.type}`);
} }

View File

@ -45,6 +45,21 @@ self.addEventListener('message', async (event) => {
} catch (error) { } catch (error) {
event.ports[0].postMessage({ status: 'error', message: error.message }); 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 activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
const service = await Services.getInstance(); const service = await Services.getInstance();
const payload = await service.getMyProcesses(); const payload = await service.getMyProcesses();
if (payload!.length != 0) { if (payload && payload.length != 0) {
activeWorker?.postMessage({ type: 'SCAN', payload }); activeWorker?.postMessage({ type: 'SCAN', payload });
} }
}, 5000); }, 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> { public async getObject(storeName: string, key: string): Promise<any | null> {
const db = await this.getDb(); const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly'); const tx = db.transaction(storeName, 'readonly');
@ -341,23 +373,25 @@ export class Database {
const store = tx.objectStore(storeName); const store = tx.objectStore(storeName);
try { try {
// Wait for both getAllKeys() and getAll() to resolve return new Promise((resolve, reject) => {
const [keys, values] = await Promise.all([ const result: Record<string, any> = {};
new Promise<any[]>((resolve, reject) => { const cursor = store.openCursor();
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);
}),
]);
// Combine keys and values into an object cursor.onsuccess = (event) => {
const result: Record<string, any> = Object.fromEntries(keys.map((key, index) => [key, values[index]])); const request = event.target as IDBRequest<IDBCursorWithValue | null>;
return result; 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) { } catch (error) {
console.error('Error fetching data from IndexedDB:', error); console.error('Error fetching data from IndexedDB:', error);
throw error; throw error;

View File

@ -132,7 +132,7 @@ export default class ModalService {
console.log("MEMBERS:", members); console.log("MEMBERS:", members);
// We take all the addresses except our own // We take all the addresses except our own
const service = await Services.getInstance(); const service = await Services.getInstance();
const localAddress = await service.getDeviceAddress(); const localAddress = service.getDeviceAddress();
for (const member of members) { for (const member of members) {
if (member.sp_addresses) { if (member.sp_addresses) {
for (const address of member.sp_addresses) { for (const address of member.sp_addresses) {

View File

@ -1,7 +1,7 @@
import { INotification } from '~/models/notification.model'; import { INotification } from '~/models/notification.model';
import { IProcess } from '~/models/process.model'; import { IProcess } from '~/models/process.model';
import { initWebsocket, sendMessage } from '../websockets'; 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 ModalService from './modal.service';
import Database from './database.service'; import Database from './database.service';
import { navigate } from '../router'; import { navigate } from '../router';
@ -23,6 +23,7 @@ export default class Services {
private processId: string | null = null; private processId: string | null = null;
private stateId: string | null = null; private stateId: string | null = null;
private sdkClient: any; private sdkClient: any;
private processesCache: Record<string, Process> = {};
private myProcesses: Set<string> = new Set(); private myProcesses: Set<string> = new Set();
private notifications: any[] | null = null; private notifications: any[] | null = null;
private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; private subscriptions: { element: Element; event: string; eventHandler: string }[] = [];
@ -61,7 +62,6 @@ export default class Services {
for (const wsurl of Object.values(BOOTSTRAPURL)) { for (const wsurl of Object.values(BOOTSTRAPURL)) {
this.updateRelay(wsurl, ''); this.updateRelay(wsurl, '');
} }
await this.connectAllRelays();
} }
public setProcessId(processId: string | null) { public setProcessId(processId: string | null) {
@ -201,7 +201,7 @@ export default class Services {
// Ensure the amount is available before proceeding // Ensure the amount is available before proceeding
await this.getTokensFromFaucet(); await this.getTokensFromFaucet();
let unconnectedAddresses = []; let unconnectedAddresses = [];
const myAddress = await this.getDeviceAddress(); const myAddress = this.getDeviceAddress();
for (const member of members) { for (const member of members) {
const sp_addresses = member.sp_addresses; const sp_addresses = member.sp_addresses;
if (!sp_addresses || sp_addresses.length === 0) continue; if (!sp_addresses || sp_addresses.length === 0) continue;
@ -386,6 +386,19 @@ export default class Services {
members.add(member) 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]); await this.checkConnections([...members]);
const privateSplitData = this.splitData(privateData); const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData); const publicSplitData = this.splitData(publicData);
@ -660,6 +673,41 @@ export default class Services {
await navigate('account'); 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() { public async pairDevice() {
if (!this.processId) { if (!this.processId) {
console.error('No processId set'); console.error('No processId set');
@ -673,7 +721,19 @@ export default class Services {
let spAddressList: string[] = []; let spAddressList: string[] = [];
try { 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); spAddressList = this.sdkClient.decode_value(encodedSpAddressList);
if (!spAddressList || spAddressList.length == 0) { if (!spAddressList || spAddressList.length == 0) {
throw new Error('Empty pairedAddresses'); throw new Error('Empty pairedAddresses');
@ -693,11 +753,15 @@ export default class Services {
return amount; return amount;
} }
async getDeviceAddress() { getDeviceAddress(): string {
return await this.sdkClient.get_address(); try {
return this.sdkClient.get_address();
} catch (e) {
throw new Error(`Failed to get device address: ${e}`);
}
} }
public dumpDeviceFromMemory(): string { public dumpDeviceFromMemory(): Device {
try { try {
return this.sdkClient.dump_device(); return this.sdkClient.dump_device();
} catch (e) { } 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 db = await Database.getInstance();
const walletStore = 'wallet'; const walletStore = 'wallet';
try { 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 db = await Database.getInstance();
const walletStore = 'wallet'; const walletStore = 'wallet';
try { try {
const dbRes = await db.getObject(walletStore, '1'); const dbRes = await db.getObject(walletStore, '1');
if (dbRes) { if (dbRes) {
const wallet = dbRes['device']; return dbRes['device'];
return wallet;
} else { } else {
return null; return null;
} }
@ -760,8 +823,7 @@ export default class Services {
try { try {
const device = await this.getDeviceFromDatabase(); const device = await this.getDeviceFromDatabase();
if (device) { if (device) {
const parsed: Device = JSON.parse(device); const pairedMember = device['paired_member'];
const pairedMember = parsed['paired_member'];
return pairedMember.sp_addresses; return pairedMember.sp_addresses;
} else { } else {
return null; return null;
@ -785,30 +847,22 @@ export default class Services {
rolesContainsUs(roles: Record<string, RoleDefinition>): boolean { rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
let us; let us;
try { try {
us = this.sdkClient.get_member(); us = this.sdkClient.get_pairing_process_id();
} catch (e) { } catch (e) {
throw e; throw e;
} }
return this.rolesContainsMember(roles, us.sp_addresses); return this.rolesContainsMember(roles, us);
} }
rolesContainsMember(roles: Record<string, RoleDefinition>, member: string[]): boolean { rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean {
let res = false; for (const roleDef of Object.values(roles)) {
for (const [roleName, roleDef] of Object.entries(roles)) { if (roleDef.members.includes(pairingProcessId)) {
for (const otherMember of roleDef.members) { return true;
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);
} }
} }
return res; return false;
} }
async dumpWallet() { async dumpWallet() {
@ -834,10 +888,9 @@ export default class Services {
return spAddress; return spAddress;
} }
restoreDevice(device: string) { public restoreDevice(device: Device) {
try { try {
this.sdkClient.restore_device(device); this.sdkClient.restore_device(device);
const spAddress = this.sdkClient.get_address();
} catch (e) { } catch (e) {
console.error(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) { public async saveProcessToDb(processId: string, process: Process) {
const db = await Database.getInstance(); const db = await Database.getInstance();
const storeName = 'processes';
try { try {
await db.addObject({ await db.addObject({
storeName: 'processes', storeName,
object: process, object: process,
key: processId, key: processId,
}); });
// Update the process in the cache
this.processesCache[processId] = process;
} catch (e) { } catch (e) {
console.error(`Failed to save process ${processId}: ${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> { public async getProcess(processId: string): Promise<Process | null> {
const db = await Database.getInstance(); if (this.processesCache[processId]) {
return await db.getObject('processes', 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>> { public async getProcesses(): Promise<Record<string, Process>> {
const db = await Database.getInstance(); if (Object.keys(this.processesCache).length > 0) {
return this.processesCache;
const processes: Record<string, Process> = await db.dumpStore('processes'); } else {
return processes; 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>) { public async restoreProcessesFromBackUp(processes: Record<string, Process>) {
const db = await Database.getInstance(); const db = await Database.getInstance();
for (const [commitedIn, process] of Object.entries(processes)) { const storeName = 'processes';
await db.addObject({ storeName: 'processes', object: process, key: commitedIn}); try {
await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
} catch (e) {
throw e;
} }
await this.restoreProcessesFromDB(); await this.restoreProcessesFromDB();
@ -976,6 +1041,7 @@ export default class Services {
const processes: Record<string, Process> = await db.dumpStore('processes'); const processes: Record<string, Process> = await db.dumpStore('processes');
if (processes && Object.keys(processes).length != 0) { if (processes && Object.keys(processes).length != 0) {
console.log(`Restoring ${Object.keys(processes).length} processes`); console.log(`Restoring ${Object.keys(processes).length} processes`);
this.processesCache = processes;
this.sdkClient.set_process_cache(processes); this.sdkClient.set_process_cache(processes);
} else { } else {
console.log('No processes to restore!'); 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 { try {
return this.sdkClient.decode_value(value); return this.sdkClient.decode_value(value);
} catch (e) { } catch (e) {
@ -1215,7 +1281,21 @@ export default class Services {
setTimeout(async () => { setTimeout(async () => {
const newProcesses: OutPointProcessMap = handshakeMsg.processes_list; 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)) { for (const [processId, process] of Object.entries(newProcesses)) {
const existing = await this.getProcess(processId); const existing = await this.getProcess(processId);
if (existing) { if (existing) {
@ -1235,6 +1315,25 @@ export default class Services {
if (new_states.length != 0) { if (new_states.length != 0) {
// We request the new states // We request the new states
await this.requestDataFromPeers(processId, new_states, roles); 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 // Otherwise we're probably just in the initial loading at page initialization
@ -1246,9 +1345,11 @@ export default class Services {
} else { } else {
// We add it to db // We add it to db
console.log(`Saving ${processId} to db`); console.log(`Saving ${processId} to db`);
await this.saveProcessToDb(processId, process as Process); toSave[processId] = process;
} }
} }
await this.batchSaveProcessesToDb(toSave);
} }
}, 500) }, 500)
} catch (e) { } catch (e) {
@ -1301,9 +1402,12 @@ export default class Services {
const content = JSON.parse(response); const content = JSON.parse(response);
const error = content.error; const error = content.error;
const errorMsg = error['GenericError']; const errorMsg = error['GenericError'];
if (errorMsg === 'State is identical to the previous state') { const dontRetry = [
return; 'State is identical to the previous state',
} else if (errorMsg === 'Not enough valid proofs') { return; } 'Not enough valid proofs',
'Not enough members to validate',
];
if (dontRetry.includes(errorMsg)) { return; }
// Wait and retry // Wait and retry
setTimeout(async () => { setTimeout(async () => {
await this.sendCommitMessage(JSON.stringify(content)); await this.sendCommitMessage(JSON.stringify(content));
@ -1340,7 +1444,7 @@ export default class Services {
const lastCommitedState = this.getLastCommitedState(process); const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.public_data) { if (lastCommitedState && lastCommitedState.public_data) {
const processName = lastCommitedState!.public_data['processName']; const processName = lastCommitedState!.public_data['processName'];
if (processName) { return processName } if (processName) { return this.decodeValue(processName) }
else { return null } else { return null }
} else { } else {
return null; return null;
@ -1348,24 +1452,32 @@ export default class Services {
} }
public async getMyProcesses(): Promise<string[] | null> { public async getMyProcesses(): Promise<string[] | null> {
// If we're not paired yet, just skip it
try {
this.getPairingProcessId();
} catch (e) {
return null;
}
try { try {
const processes = await this.getProcesses(); const processes = await this.getProcesses();
const newMyProcesses = new Set<string>(this.myProcesses || []);
for (const [processId, process] of Object.entries(processes)) { for (const [processId, process] of Object.entries(processes)) {
// We use myProcesses attribute to not reevaluate all processes everytime // We use myProcesses attribute to not reevaluate all processes everytime
if (this.myProcesses && this.myProcesses.has(processId)) { if (newMyProcesses.has(processId)) {
continue; continue;
} }
try { try {
const roles = this.getRoles(process); const roles = this.getRoles(process);
if (roles && this.rolesContainsUs(roles)) { if (roles && this.rolesContainsUs(roles)) {
this.myProcesses.add(processId); newMyProcesses.add(processId);
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
} }
this.myProcesses = newMyProcesses; // atomic update
return Array.from(this.myProcesses); return Array.from(this.myProcesses);
} catch (e) { } catch (e) {
console.error("Failed to get processes:", e); console.error("Failed to get processes:", e);
@ -1418,6 +1530,14 @@ export default class Services {
return this.sdkClient.get_merkle_proof(processState, attributeName); 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 { public getLastCommitedState(process: Process): ProcessState | null {
if (process.states.length === 0) return null; if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in; const processTip = process.states[process.states.length - 1].commited_in;
@ -1440,6 +1560,13 @@ export default class Services {
return null; 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 { public getStateFromId(process: Process, stateId: string): ProcessState | null {
if (process.states.length === 0) return null; if (process.states.length === 0) return null;
const state = process.states.find(state => state.state_id === stateId); const state = process.states.find(state => state.state_id === stateId);