sdk_signer/src/service.ts

1300 lines
42 KiB
TypeScript

// Simple server service with core protocol methods using WASM SDK
import Database from './database.service';
import * as wasm from '../pkg/sdk_client';
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../pkg/sdk_client';
import { RelayManager } from './relay-manager';
import { config } from './config';
import { EMPTY32BYTES } from './utils';
const DEFAULTAMOUNT = 1000n;
const DEVICE_KEY = 'main_device';
export class Service {
private static instance: Service;
private processes: Map<string, any> = new Map();
private membersList: any = {};
private relayManager: RelayManager;
private storages: string[] = []; // storage urls
private constructor() {
console.log('🔧 Service initialized');
this.relayManager = RelayManager.getInstance();
this.relayManager.setHandshakeCallback((url: string, message: any) => {
this.handleHandshakeMsg(url, message);
});
this.initWasm();
// Removed automatic relay initialization - will connect when needed
}
private initWasm() {
try {
console.log('🔧 Initializing WASM SDK...');
wasm.setup();
console.log('✅ WASM SDK initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize WASM SDK:', error);
throw error;
}
}
static getInstance(): Service {
if (!Service.instance) {
Service.instance = new Service();
}
return Service.instance;
}
// Handle the handshake message
public async handleHandshakeMsg(url: string, parsedMsg: any) {
try {
const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg.content);
if (handshakeMsg.sp_address) {
this.relayManager.updateRelay(url, handshakeMsg.sp_address);
}
if (this.membersList && Object.keys(this.membersList).length === 0) {
// We start from an empty list, just copy it over
this.membersList = handshakeMsg.peers_list;
} else {
// We are incrementing our list
if (handshakeMsg.peers_list) {
for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) {
this.membersList[processId] = member as Member;
}
}
}
setTimeout(async () => {
if (handshakeMsg.processes_list) {
const newProcesses: OutPointProcessMap = handshakeMsg.processes_list;
if (!newProcesses || Object.keys(newProcesses).length === 0) {
console.debug('Received empty processes list from', url);
return;
}
if (this.processes.size === 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) {
// Look for state id we don't know yet
let new_states = [];
let roles = [];
for (const state of process.states) {
if (!state.state_id || state.state_id === EMPTY32BYTES) { continue; }
if (!this.lookForStateId(existing, state.state_id)) {
if (this.rolesContainsUs(state.roles)) {
new_states.push(state.state_id);
roles.push(state.roles);
}
}
}
if (new_states.length != 0) {
// We request the new states
await this.requestDataFromPeers(processId, new_states, roles);
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'] as unknown as number[]);
// 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
// TODO maybe actually check if what the relay is sending us contains more information than what we have
// relay should always have more info than us, but we never know
// For now let's keep it simple and let the worker do the job
} else {
// We add it to db
console.log(`Saving ${processId} to db`);
toSave[processId] = process;
}
}
await this.batchSaveProcessesToDb(toSave);
}
}
}, 500);
} catch (e) {
console.error('Failed to parse init message:', e);
}
}
public async connectToRelays(): Promise<void> {
const { relayUrls } = config;
console.log(`🔗 Connecting to ${relayUrls.length} relays...`);
for (let i = 0; i < relayUrls.length; i++) {
const wsUrl = relayUrls[i].trim();
const relayId = `default-relay-${i}`;
try {
const success = await this.relayManager.connectToRelay(relayId, wsUrl, '');
if (success) {
console.log(`✅ Connected to relay: ${relayId} at ${wsUrl}`);
} else {
console.warn(`⚠️ Failed to connect to relay: ${relayId}`);
}
} catch (error) {
console.error(`❌ Error connecting to relay ${relayId}:`, error);
}
}
}
/**
* Connect to relays and wait for at least one handshake to complete.
* This guarantees that relay addresses are properly updated in memory.
* @param timeoutMs - Timeout for handshake completion (default: 10000)
* @returns Promise that resolves when at least one handshake is completed
*/
public async connectToRelaysAndWaitForHandshake(timeoutMs: number = 10000): Promise<void> {
console.log(`🔗 Connecting to relays and waiting for handshake...`);
// First connect to all relays
await this.connectToRelays();
// Then wait for at least one handshake to complete
try {
await this.relayManager.waitForHandshake(timeoutMs);
console.log(`✅ Successfully connected and received handshake from at least one relay`);
} catch (error) {
console.error(`❌ Failed to receive handshake within ${timeoutMs}ms:`, error);
throw new Error(`No handshake received from any relay within ${timeoutMs}ms`);
}
}
/**
* Connect to a specific relay and wait for its handshake to complete.
* @param relayId - The relay ID to connect to
* @param wsUrl - The WebSocket URL of the relay
* @param spAddress - The SP address of the relay
* @param timeoutMs - Timeout for handshake completion (default: 10000)
* @returns Promise that resolves when the relay's handshake is completed
*/
public async connectToRelayAndWaitForHandshake(
relayId: string,
wsUrl: string,
spAddress: string,
timeoutMs: number = 10000
): Promise<void> {
console.log(`🔗 Connecting to relay ${relayId} and waiting for handshake...`);
// Connect to the relay
const success = await this.relayManager.connectToRelay(relayId, wsUrl, spAddress);
if (!success) {
throw new Error(`Failed to connect to relay ${relayId}`);
}
// Wait for handshake completion
try {
await this.relayManager.waitForRelayHandshake(relayId, timeoutMs);
console.log(`✅ Successfully connected and received handshake from relay ${relayId}`);
} catch (error) {
console.error(`❌ Failed to receive handshake from relay ${relayId} within ${timeoutMs}ms:`, error);
throw new Error(`No handshake received from relay ${relayId} within ${timeoutMs}ms`);
}
}
/**
* Verify that at least one relay has completed handshake and has a valid SP address.
* @returns True if at least one relay has completed handshake with valid SP address
*/
public hasValidRelayConnection(): boolean {
const connectedRelays = this.relayManager.getConnectedRelays();
const handshakeCompletedRelays = this.relayManager.getHandshakeCompletedRelays();
// Check if we have at least one connected relay with completed handshake
for (const relay of connectedRelays) {
if (handshakeCompletedRelays.includes(relay.id) && relay.spAddress && relay.spAddress.trim() !== '') {
return true;
}
}
return false;
}
/**
* Get the first relay that has completed handshake and has a valid SP address.
* @returns The relay connection or null if none found
*/
public getFirstValidRelay(): { id: string; url: string; spAddress: string } | null {
const connectedRelays = this.relayManager.getConnectedRelays();
const handshakeCompletedRelays = this.relayManager.getHandshakeCompletedRelays();
for (const relay of connectedRelays) {
if (handshakeCompletedRelays.includes(relay.id) && relay.spAddress && relay.spAddress.trim() !== '') {
return {
id: relay.id,
url: relay.url,
spAddress: relay.spAddress
};
}
}
return null;
}
/**
* Get all connected relays from RelayManager.
* @returns An array of objects containing relay information.
*/
public getAllRelays(): { wsurl: string; spAddress: string }[] {
const connectedRelays = this.relayManager.getConnectedRelays();
return connectedRelays.map(relay => ({
wsurl: relay.url,
spAddress: relay.spAddress
}));
}
/**
* Get relay statistics from RelayManager.
* @returns Statistics about connected relays
*/
public getRelayStats(): any {
return this.relayManager.getStats();
}
public async getSecretForAddress(address: string): Promise<string | null> {
const db = await Database.getInstance();
return await db.getObject('shared_secrets', address);
}
private async getTokensFromFaucet(): Promise<void> {
try {
await this.ensureSufficientAmount();
} catch (e) {
console.error('Failed to get tokens from relay, check connection');
return;
}
}
public getAllMembers(): Record<string, Member> {
return this.membersList;
}
public getAddressesForMemberId(memberId: string): string[] | null {
try {
return this.membersList[memberId].sp_addresses;
} catch (e) {
return null;
}
}
public async checkConnections(members: Member[]): Promise<void> {
// Ensure the amount is available before proceeding
await this.getTokensFromFaucet();
let unconnectedAddresses = [];
const myAddress = this.getDeviceAddress();
for (const member of members) {
const sp_addresses = member.sp_addresses;
if (!sp_addresses || sp_addresses.length === 0) continue;
for (const address of sp_addresses) {
// For now, we ignore our own device address, although there might be use cases for having a secret with ourselves
if (address === myAddress) continue;
const sharedSecret = await this.getSecretForAddress(address);
if (!sharedSecret) {
unconnectedAddresses.push(address);
}
}
}
if (unconnectedAddresses && unconnectedAddresses.length != 0) {
const apiResult = await this.connectAddresses(unconnectedAddresses);
await this.handleApiReturn(apiResult);
}
}
public async connectAddresses(addresses: string[]): Promise<ApiReturn> {
if (addresses.length === 0) {
throw new Error('Trying to connect to empty addresses list');
}
try {
return wasm.create_transaction(addresses, 1);
} catch (e) {
console.error('Failed to connect member:', e);
throw e;
}
}
private async ensureSufficientAmount(): Promise<void> {
const availableAmt: BigInt = wasm.get_available_amount();
const target: BigInt = DEFAULTAMOUNT * BigInt(10);
if (availableAmt < target) {
// Ensure we have a relay connection before sending faucet message
if (!this.hasValidRelayConnection()) {
console.log('No valid relay connection found, attempting to connect...');
await this.connectToRelaysAndWaitForHandshake();
}
try {
const faucetMsg = wasm.create_faucet_msg();
this.relayManager.sendFaucetMessage(faucetMsg);
} catch (e) {
throw new Error('Failed to create faucet message');
}
await this.waitForAmount(target);
}
}
private async waitForAmount(target: BigInt): Promise<BigInt> {
let attempts = 3;
while (attempts > 0) {
const amount: BigInt = wasm.get_available_amount();
if (amount >= target) {
return amount;
}
attempts--;
if (attempts > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second
}
}
throw new Error('Amount is still 0 after 3 attempts');
}
private isFileBlob(value: any): value is { type: string, data: Uint8Array } {
return (
typeof value === 'object' &&
value !== null &&
typeof value.type === 'string' &&
value.data instanceof Uint8Array
);
}
private splitData(obj: Record<string, any>) {
const jsonCompatibleData: Record<string, any> = {};
const binaryData: Record<string, { type: string; data: Uint8Array }> = {};
for (const [key, value] of Object.entries(obj)) {
if (this.isFileBlob(value)) {
binaryData[key] = value;
} else {
jsonCompatibleData[key] = value;
}
}
return { jsonCompatibleData, binaryData };
}
public async createNewDevice() {
try {
const spAddress = wasm.create_new_device(0, 'signet');
const device = wasm.dump_device();
await this.saveDeviceInDatabase(device);
return spAddress;
} catch (e) {
throw new Error(`Failed to create new device: ${e}`);
}
}
public async saveDeviceInDatabase(device: Device): Promise<void> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
const prevDevice = await this.getDeviceFromDatabase();
if (prevDevice) {
await db.deleteObject(walletStore, DEVICE_KEY);
}
await db.addObject({
storeName: walletStore,
object: {
device_id: DEVICE_KEY,
device_address: wasm.get_address(),
created_at: new Date().toISOString(),
device
},
key: DEVICE_KEY,
});
} catch (e) {
console.error('Failed to save device to database:', e);
}
}
public getPairingProcessId(): string {
try {
return wasm.get_pairing_process_id();
} catch (e) {
throw new Error(`Failed to get pairing process: ${e}`);
}
}
public async createPairingProcess(userName: string, pairWith: string[]): Promise<ApiReturn> {
if (wasm.is_paired()) {
throw new Error('Device already paired');
}
const myAddress: string = wasm.get_address();
pairWith.push(myAddress);
const privateData = {
description: 'pairing',
counter: 0,
};
const publicData = {
memberPublicName: userName,
pairedAddresses: pairWith,
};
const validation_fields: string[] = [...Object.keys(privateData), ...Object.keys(publicData), 'roles'];
const roles: Record<string, RoleDefinition> = {
pairing: {
members: [],
validation_rules: {
"stub_validation_rule": {
id: "stub_validation_rule",
quorum: 1.0,
field_name: "validation_field",
rule_type: "custom" as any,
role_id: "stub_role",
parameters: { min_sig_member: 1.0 },
},
}
},
};
try {
return this.createProcess(
privateData,
publicData,
roles
);
} catch (e) {
throw new Error(`Creating process failed:, ${e}`);
}
}
public async createProcess(
privateData: Record<string, any>,
publicData: Record<string, any>,
roles: Record<string, RoleDefinition>,
): Promise<ApiReturn> {
// Ensure we have a valid relay connection with completed handshake
if (!this.hasValidRelayConnection()) {
console.log('No valid relay connection found, attempting to connect and wait for handshake...');
await this.connectToRelaysAndWaitForHandshake();
}
const validRelay = this.getFirstValidRelay();
if (!validRelay) {
throw new Error('No valid relay connection found after handshake');
}
const relayAddress = validRelay.spAddress;
const feeRate = 1;
// We can't encode files as the rest because Uint8Array is not valid json
// So we first take them apart and we will encode them separately and put them back in the right object
// TODO encoding of relatively large binaries (=> 1M) is a bit long now and blocking
const privateSplitData = this.splitData(privateData);
const publicSplitData = this.splitData(publicData);
const encodedPrivateData = {
...wasm.encode_json(privateSplitData.jsonCompatibleData),
...wasm.encode_binary(privateSplitData.binaryData)
};
const encodedPublicData = {
...wasm.encode_json(publicSplitData.jsonCompatibleData),
...wasm.encode_binary(publicSplitData.binaryData)
};
let members: Set<Member> = new Set();
for (const role of Object.values(roles!)) {
for (const member of role.members) {
// Check if we know the member that matches this id
const memberAddresses = this.getAddressesForMemberId(member);
if (memberAddresses && memberAddresses.length != 0) {
members.add({ id: "stub_member", name: "stub_member", public_key: "stub_key", process_id: "stub_process", roles: [], sp_addresses: memberAddresses });
}
}
}
await this.checkConnections([...members]);
const result = wasm.create_new_process (
encodedPrivateData,
roles,
encodedPublicData,
relayAddress,
feeRate,
this.getAllMembers()
);
return(result);
}
async parseCipher(message: string): Promise<void> {
const membersList = this.getAllMembers();
const processes = Object.fromEntries(this.getProcesses());
try {
const apiReturn = wasm.parse_cipher(message, membersList, processes);
await this.handleApiReturn(apiReturn);
} catch (e) {
console.error(`Failed to parse cipher: ${e}`);
}
}
async parseNewTx(tx: string): Promise<void> {
const membersList = this.getAllMembers();
try {
// TODO: get the block height somewhere to pass it
const parsedTx = wasm.parse_new_tx(tx, 0, membersList);
if (parsedTx) {
try {
await this.handleApiReturn(parsedTx);
const newDevice = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(newDevice);
} catch (e) {
console.error('Failed to update device with new tx');
}
}
} catch (e) {
console.error('Failed to parse new tx', e);
}
}
async parseFaucet(faucetResponse: string) {
try {
console.log('🪙 Parsing faucet response:', faucetResponse);
// The faucet response should contain transaction data that updates the device's amount
// Parse it similar to how we parse new transactions
const membersList = this.getAllMembers();
const parsedTx = wasm.parse_new_tx(faucetResponse, 0, membersList);
if (parsedTx) {
await this.handleApiReturn(parsedTx);
// Update device in database after faucet response
const newDevice = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(newDevice);
console.log('✅ Faucet response processed successfully');
} else {
console.warn('⚠️ No transaction data in faucet response');
}
} catch (e) {
console.error('❌ Failed to parse faucet response:', e);
}
}
// Core protocol method: Create PRD Update
async createPrdUpdate(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`📢 Creating PRD update for process ${processId}, state ${stateId}`);
try {
const process = await this.getProcess(processId);
if (!process) {
throw new Error('Process not found');
}
const result = wasm.create_update_message(process, stateId, this.membersList);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error || 'Unknown error');
throw new Error(errorMessage);
}
}
// Core protocol method: Approve Change (Validate State)
async approveChange(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`✅ Approving change for process ${processId}, state ${stateId}`);
try {
const process = this.processes.get(processId);
if (!process) {
throw new Error('Process not found');
}
const result = wasm.validate_state(process, stateId, this.membersList);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error || 'Unknown error');
throw new Error(errorMessage);
}
}
// Core protocol method: Update Process
async updateProcess(
process: any,
privateData: Record<string, any>,
publicData: Record<string, any>,
roles: Record<string, any> | null
): Promise<ApiReturn> {
console.log(`🔄 Updating process ${process.states[0]?.state_id || 'unknown'}`);
console.log('Private data:', privateData);
console.log('Public data:', publicData);
console.log('Roles:', roles);
try {
// Convert data to WASM format
const newAttributes = wasm.encode_json(privateData);
const newPublicData = wasm.encode_json(publicData);
const newRoles = roles || process.states[0]?.roles || {};
// Use WASM function to update process
const result = wasm.update_process(process, newAttributes, newRoles, newPublicData, this.membersList);
if (result.updated_process) {
// Update our cache
this.processes.set(result.updated_process.process_id, result.updated_process.current_process);
// Save to database
await this.saveProcessToDb(result.updated_process.process_id, result.updated_process.current_process);
return result;
} else {
throw new Error('Failed to update process');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error || 'Unknown error');
throw new Error(errorMessage);
}
}
public async getMyProcesses(): Promise<string[] | null> {
// If we're not paired yet, just skip it
let pairingProcessId = null;
try {
pairingProcessId = this.getPairingProcessId();
} catch (e) {
return null;
}
if (!pairingProcessId) {
return null;
}
try {
const newMyProcesses = new Set<string>();
// MyProcesses automatically contains pairing process
newMyProcesses.add(pairingProcessId);
for (const [processId, process] of Object.entries(this.processes)) {
try {
const roles = this.getRoles(process);
if (roles && this.rolesContainsMember(roles, pairingProcessId)) {
newMyProcesses.add(processId);
}
} catch (e) {
console.error(e);
}
}
return Array.from(newMyProcesses);
} catch (e) {
console.error("Failed to get processes:", e);
return null;
}
}
// Utility method: Get Process
async getProcess(processId: string): Promise<any | null> {
// First check in-memory cache
const cachedProcess = this.processes.get(processId);
if (cachedProcess) {
return cachedProcess;
}
// If not in cache, try to get from database
try {
const db = await Database.getInstance();
const dbProcess = await db.getObject('processes', processId);
if (dbProcess) {
// Cache it for future use
this.processes.set(processId, dbProcess);
return dbProcess;
}
} catch (error) {
console.error('Error getting process from database:', error);
}
return null;
}
// Database method: Save Process
async saveProcessToDb(processId: string, process: any): Promise<void> {
try {
const db = await Database.getInstance();
await db.addObject({
storeName: 'processes',
object: process,
key: processId
});
// Update in-memory cache
this.processes.set(processId, process);
console.log(`💾 Process ${processId} saved to database`);
} catch (error) {
console.error('Error saving process to database:', error);
throw error;
}
}
public getProcesses(): Map<string, Process> {
return this.processes;
}
async getAllProcessesFromDb(): Promise<Record<string, any>> {
try {
const db = await Database.getInstance();
const processes = await db.dumpStore('processes');
// Update in-memory cache with all processes
for (const [processId, process] of Object.entries(processes)) {
this.processes.set(processId, process as any);
}
return processes;
} catch (error) {
console.error('Error getting all processes from database:', error);
return {};
}
}
// Utility method: Create a test process
async createTestProcess(processId: string): Promise<any> {
console.log(`🔧 Creating test process: ${processId}`);
try {
// Create test data
const privateData = wasm.encode_json({ secret: 'initial_secret' });
const publicData = wasm.encode_json({ name: 'Test Process', created: Date.now() });
const roles = { admin: { members: [], validation_rules: [], storages: [] } };
const relayAddress = 'test_relay_address';
const feeRate = 1;
// Use WASM to create new process
const result = wasm.create_new_process(privateData, roles, publicData, relayAddress, feeRate, this.membersList);
if (result.updated_process) {
const process = result.updated_process.current_process;
this.processes.set(processId, process);
// Save to database
await this.saveProcessToDb(processId, process);
console.log(`✅ Test process created: ${processId}`);
return process;
} else {
throw new Error('Failed to create test process');
}
} catch (error) {
console.error('Error creating test process:', error);
throw error;
}
}
public async getDeviceFromDatabase(): Promise<Device | null> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
const dbRes = await db.getObject(walletStore, DEVICE_KEY);
if (dbRes) {
return dbRes['device'];
} else {
return null;
}
} catch (e) {
throw new Error(`Failed to retrieve device from db: ${e}`);
}
}
public async getDeviceMetadata(): Promise<{ device_id: string; device_address: string; created_at: string } | null> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try {
const dbRes = await db.getObject(walletStore, DEVICE_KEY);
if (dbRes) {
return {
device_id: dbRes['device_id'],
device_address: dbRes['device_address'],
created_at: dbRes['created_at']
};
} else {
return null;
}
} catch (e) {
console.error('Failed to retrieve device metadata from db:', e);
return null;
}
}
public async hasDevice(): Promise<boolean> {
const device = await this.getDeviceFromDatabase();
return device !== null;
}
public async restoreDeviceFromDatabase(device: Device): Promise<void> {
try {
wasm.restore_device(device);
console.log('✅ Device restored in WASM successfully');
} catch (e) {
throw new Error(`Failed to restore device in WASM: ${e}`);
}
}
public pairDevice(processId: string, addresses: string[]): void {
try {
wasm.pair_device(processId, addresses);
} catch (e) {
throw new Error(`Failed to pair device: ${e}`);
}
}
public isPaired(): boolean {
try {
return wasm.is_paired();
} catch (error) {
console.error('Error checking if paired:', error);
throw error;
}
}
public getLastCommitedState(process: Process): ProcessState | null {
const index = this.getLastCommitedStateIndex(process);
if (index === null) return null;
return process.states[index];
}
public getLastCommitedStateIndex(process: Process): number | null {
if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in;
for (let i = process.states.length - 1; i >= 0; i--) {
if ((process.states[i] as any).commited_in !== processTip) {
return i;
}
}
return null;
}
public getRoles(process: Process): Record<string, RoleDefinition> | null {
const lastCommitedState = this.getLastCommitedState(process);
if (lastCommitedState && lastCommitedState.roles && Object.keys(lastCommitedState.roles).length != 0) {
return lastCommitedState!.roles;
} else if (process.states.length === 2) {
const firstState = process.states[0];
if (firstState && firstState.roles && Object.keys(firstState.roles).length != 0) {
return firstState!.roles;
}
}
return null;
}
public rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
let us;
try {
us = wasm.get_pairing_process_id();
} catch (e) {
throw e;
}
return this.rolesContainsMember(roles, us);
}
public rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean {
for (const roleDef of Object.values(roles)) {
if (roleDef.members.includes(pairingProcessId)) {
return true;
}
}
return false;
}
// Utility method: Add member to the members list
addMember(outpoint: string, member: any) {
this.membersList[outpoint] = member;
}
// Utility method: Get device address
getDeviceAddress(): string {
try {
return wasm.get_address();
} catch (error) {
console.error('Error getting device address:', error);
throw error;
}
}
// WebSocket message methods using Relay Manager
async sendNewTxMessage(message: string) {
console.log('📤 Sending NewTx message:', message);
this.relayManager.sendNewTxMessage(message);
}
async sendCommitMessage(message: string) {
console.log('📤 Sending Commit message:', message);
this.relayManager.sendCommitMessage(message);
}
async sendCipherMessages(ciphers: string[]) {
console.log('📤 Sending Cipher messages:', ciphers.length, 'ciphers');
this.relayManager.sendCipherMessages(ciphers);
}
// Blob and data storage methods
async saveBlobToDb(hash: string, data: Blob) {
const db = await Database.getInstance();
try {
await db.addObject({
storeName: 'data',
object: data,
key: hash,
});
} catch (e) {
console.error(`Failed to save data to db: ${e}`);
}
}
async getBlobFromDb(hash: string): Promise<Blob | null> {
const db = await Database.getInstance();
try {
return await db.getObject('data', hash);
} catch (e) {
return null;
}
}
async saveDataToStorage(hash: string, data: Blob, ttl: number | null) {
console.log('💾 Saving data to storage:', hash);
// TODO: Implement actual storage service
// const storages = [STORAGEURL];
// try {
// await storeData(storages, hash, data, ttl);
// } catch (e) {
// console.error(`Failed to store data with hash ${hash}: ${e}`);
// }
}
async saveDiffsToDb(diffs: any[]) {
const db = await Database.getInstance();
try {
for (const diff of diffs) {
await db.addObject({
storeName: 'diffs',
object: diff,
key: null,
});
}
} catch (e) {
throw new Error(`Failed to save diffs: ${e}`);
}
}
// Utility methods for data conversion
hexToBlob(hexString: string): Blob {
const uint8Array = this.hexToUInt8Array(hexString);
return new Blob([uint8Array], { type: "application/octet-stream" });
}
hexToUInt8Array(hexString: string): Uint8Array {
if (hexString.length % 2 !== 0) {
throw new Error("Invalid hex string: length must be even");
}
const uint8Array = new Uint8Array(hexString.length / 2);
for (let i = 0; i < hexString.length; i += 2) {
uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
}
return uint8Array;
}
public async handleApiReturn(apiReturn: ApiReturn) {
// Check for errors in the returned objects
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.error) {
const error = apiReturn.new_tx_to_send.error;
const errorMessage = typeof error === 'object' && error !== null ?
(error as any).GenericError || JSON.stringify(error) :
String(error);
throw new Error(`Transaction error: ${errorMessage}`);
}
if (apiReturn.commit_to_send && apiReturn.commit_to_send.error) {
const error = apiReturn.commit_to_send.error;
const errorMessage = typeof error === 'object' && error !== null ?
(error as any).GenericError || JSON.stringify(error) :
String(error);
throw new Error(`Commit error: ${errorMessage}`);
}
if (apiReturn.partial_tx) {
try {
const res = wasm.sign_transaction(apiReturn.partial_tx);
apiReturn.new_tx_to_send = res.new_tx_to_send;
} catch (e) {
console.error('Failed to sign transaction:', e);
}
}
if (apiReturn.new_tx_to_send && apiReturn.new_tx_to_send.transaction.length != 0) {
await this.sendNewTxMessage(JSON.stringify(apiReturn.new_tx_to_send));
await new Promise(r => setTimeout(r, 500));
}
if (apiReturn.secrets) {
const unconfirmedSecrets = apiReturn.secrets.unconfirmed_secrets;
const confirmedSecrets = apiReturn.secrets.shared_secrets;
const db = await Database.getInstance();
for (const secret of unconfirmedSecrets) {
await db.addObject({
storeName: 'unconfirmed_secrets',
object: secret,
key: null,
});
}
const entries = Object.entries(confirmedSecrets).map(([key, value]) => ({ key, value }));
for (const entry of entries) {
try {
await db.addObject({
storeName: 'shared_secrets',
object: entry.value,
key: entry.key,
});
} catch (e) {
throw e;
}
}
}
if (apiReturn.updated_process) {
const updatedProcess = apiReturn.updated_process;
const processId: string = updatedProcess.process_id;
if (updatedProcess.encrypted_data && Object.keys(updatedProcess.encrypted_data).length != 0) {
for (const [hash, cipher] of Object.entries(updatedProcess.encrypted_data)) {
if (typeof cipher === 'string') {
const blob = this.hexToBlob(cipher);
try {
await this.saveBlobToDb(hash, blob);
} catch (e) {
console.error(e);
}
}
}
}
// Save process to db
await this.saveProcessToDb(processId, updatedProcess.current_process);
if (updatedProcess.diffs && updatedProcess.diffs.length != 0) {
try {
await this.saveDiffsToDb(updatedProcess.diffs);
} catch (e) {
console.error('Failed to save diffs to db:', e);
}
}
}
if (apiReturn.push_to_storage && apiReturn.push_to_storage.length != 0) {
for (const hash of apiReturn.push_to_storage) {
const blob = await this.getBlobFromDb(hash);
if (blob) {
await this.saveDataToStorage(hash, blob, null);
} else {
console.error('Failed to get data from db');
}
}
}
if (apiReturn.commit_to_send) {
const commit = apiReturn.commit_to_send;
await this.sendCommitMessage(JSON.stringify(commit));
}
if (apiReturn.ciphers_to_send && apiReturn.ciphers_to_send.length != 0) {
await this.sendCipherMessages(apiReturn.ciphers_to_send);
}
}
public async 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 })) });
// Update the processes Map with the new processes
for (const [key, value] of Object.entries(processes)) {
this.processes.set(key, value);
}
} catch (e) {
throw e;
}
}
private lookForStateId(process: Process, stateId: string): boolean {
for (const state of process.states) {
if (state.state_id === stateId) {
return true;
}
}
return false;
}
public async requestDataFromPeers(processId: string, stateIds: string[], roles: Record<string, RoleDefinition>[]) {
console.log('Requesting data from peers');
const membersList = this.getAllMembers();
try {
const res = wasm.request_data(processId, stateIds, Object.keys(roles), membersList);
await this.handleApiReturn(res);
} catch (e) {
console.error(e);
}
}
async decryptAttribute(processId: string, state: ProcessState, attribute: string): Promise<any | null> {
let hash = state.pcd_commitment[attribute];
if (!hash) {
// attribute doesn't exist
return null;
}
let key = state.keys[attribute];
const pairingProcessId = this.getPairingProcessId();
// If key is missing, request an update and then retry
if (!key) {
const roles = state.roles;
let hasAccess = false;
// If we're not supposed to have access to this attribute, ignore
for (const role of Object.values(roles)) {
for (const rule of Object.values(role.validation_rules)) {
if (typeof rule === 'object' && rule !== null && 'fields' in rule && Array.isArray(rule.fields)) {
if (rule.fields.includes(attribute)) {
if (role.members.includes(pairingProcessId)) {
// We have access to this attribute
hasAccess = true;
break;
}
}
}
}
}
if (!hasAccess) return null;
// We should have the key, so we're going to ask other members for it
await this.requestDataFromPeers(processId, [state.state_id], [state.roles]);
const maxRetries = 5;
const retryDelay = 500; // delay in milliseconds
let retries = 0;
while ((!hash || !key) && retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, retryDelay));
// Re-read hash and key after waiting
hash = state.pcd_commitment[attribute];
key = state.keys[attribute];
retries++;
}
}
if (hash && key) {
const blob = await this.getBlobFromDb(hash);
if (blob) {
// Decrypt the data
const buf = await blob.arrayBuffer();
const cipher = new Uint8Array(buf);
const keyUIntArray = this.hexToUInt8Array(key);
try {
const clear = wasm.decrypt_data(keyUIntArray, cipher);
if (clear) {
// deserialize the result to get the actual data
const decoded = wasm.decode_value(clear);
return decoded;
} else {
throw new Error('decrypt_data returned null');
}
} catch (e) {
console.error(`Failed to decrypt data: ${e}`);
}
}
}
return null;
}
decodeValue(value: number[]): any | null {
try {
return wasm.decode_value(new Uint8Array(value));
} catch (e) {
console.error(`Failed to decode value: ${e}`);
return null;
}
}
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())) {
// Note: unpairDevice method doesn't exist in current service, skipping for now
return;
}
// We can update the device with the new addresses
wasm.unpair_device();
wasm.pair_device(myPairingProcessId, spAddressList);
const newDevice = this.dumpDeviceFromMemory();
await this.saveDeviceInDatabase(newDevice);
}
}
public dumpDeviceFromMemory(): Device {
try {
return wasm.dump_device();
} catch (e) {
throw new Error(`Failed to dump device: ${e}`);
}
}
}