From afe12860cf5476f1dfbb687062b181dc2d4ba5de Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 6 Aug 2025 17:04:15 +0200 Subject: [PATCH] Functional pairing --- src/index.ts | 3 +- src/models.ts | 20 ++ src/relay-manager.ts | 547 ++++++++++++++++++++++++++++ src/service.ts | 837 +++++++++++++++++++++++++++++++++++++++---- src/simple-server.ts | 168 +++++---- src/utils.ts | 1 + 6 files changed, 1430 insertions(+), 146 deletions(-) create mode 100644 src/relay-manager.ts diff --git a/src/index.ts b/src/index.ts index d7bdf1e..e5f03e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,9 @@ // Main entry point for the SDK Signer server export { Service } from './service'; export { config } from './config'; -export { MessageType } from './models'; +export { MessageType, AnkFlag } from './models'; export { isValid32ByteHex } from './utils'; +export { RelayManager } from './relay-manager'; // Re-export the main server class export { Server } from './simple-server'; \ No newline at end of file diff --git a/src/models.ts b/src/models.ts index de90b03..31f2506 100644 --- a/src/models.ts +++ b/src/models.ts @@ -37,4 +37,24 @@ export enum MessageType { // Account management ADD_DEVICE = 'ADD_DEVICE', DEVICE_ADDED = 'DEVICE_ADDED', +} + +// Re-export AnkFlag from WASM for relay message typing +export { AnkFlag } from '../pkg/sdk_client'; + +// Message priority levels +export enum MessagePriority { + LOW = 0, + NORMAL = 1, + HIGH = 2, + CRITICAL = 3, +} + +// Message delivery status +export enum DeliveryStatus { + PENDING = 'PENDING', + SENT = 'SENT', + DELIVERED = 'DELIVERED', + FAILED = 'FAILED', + RETRY = 'RETRY', } \ No newline at end of file diff --git a/src/relay-manager.ts b/src/relay-manager.ts new file mode 100644 index 0000000..64a82ad --- /dev/null +++ b/src/relay-manager.ts @@ -0,0 +1,547 @@ +import WebSocket from 'ws'; +import { AnkFlag } from '../pkg/sdk_client'; +import { Service } from './service'; + +interface RelayConnection { + id: string; + ws: WebSocket; + url: string; + spAddress: string; + isConnected: boolean; + lastHeartbeat: number; + reconnectAttempts: number; + maxReconnectAttempts: number; + handshakeCompleted: boolean; // Track if handshake has been completed + handshakePromise?: Promise; // Promise that resolves when handshake completes + handshakeResolve?: () => void; // Resolver for the handshake promise +} + +interface QueuedMessage { + id: string; + flag: AnkFlag; + payload: any; + targetRelayId?: string; + timestamp: number; + expiresAt?: number; + retryCount: number; + maxRetries: number; +} + +export class RelayManager { + private static instance: RelayManager; + private relays: Map = new Map(); + private messageQueue: QueuedMessage[] = []; + private processingQueue = false; + private heartbeatInterval: NodeJS.Timeout | null = null; + private queueProcessingInterval: NodeJS.Timeout | null = null; + private reconnectInterval: NodeJS.Timeout | null = null; + private handshakeCallback?: (url: string, message: any) => void; + private handshakeCompletedRelays: Set = new Set(); // Track completed handshakes + + private constructor() { + this.startHeartbeat(); + this.startQueueProcessing(); + this.startReconnectMonitoring(); + } + + static getInstance(): RelayManager { + if (!RelayManager.instance) { + RelayManager.instance = new RelayManager(); + } + return RelayManager.instance; + } + + // Set callback for handshake messages + public setHandshakeCallback(callback: (url: string, message: any) => void): void { + this.handshakeCallback = callback; + } + + /** + * Update the SP address for a relay by URL. + * @param wsurl - The WebSocket URL of the relay. + * @param spAddress - The SP Address of the relay. + */ + public updateRelay(wsurl: string, spAddress: string): void { + // Find the relay by URL and update its SP address + const connectedRelays = this.getConnectedRelays(); + const relay = connectedRelays.find(r => r.url === wsurl); + if (relay) { + relay.spAddress = spAddress; + console.log(`Updated relay SP address: ${wsurl} -> ${spAddress}`); + } else { + console.warn(`Relay not found for URL: ${wsurl}`); + } + } + + /** + * Retrieve the spAddress for a given wsurl. + * @param wsurl - The WebSocket URL to look up. + * @returns The SP Address if found, or undefined if not. + */ + public getSpAddress(wsurl: string): string | undefined { + const connectedRelays = this.getConnectedRelays(); + const relay = connectedRelays.find(r => r.url === wsurl); + return relay?.spAddress; + } + + // Relay Management - Outbound Connections + public async connectToRelay(relayId: string, wsUrl: string, spAddress: string): Promise { + try { + console.log(`🔗 Connecting to relay ${relayId} at ${wsUrl}`); + + const ws = new WebSocket(wsUrl); + + const relay: RelayConnection = { + id: relayId, + ws, + url: wsUrl, + spAddress, + isConnected: false, + lastHeartbeat: Date.now(), + reconnectAttempts: 0, + maxReconnectAttempts: 5, + handshakeCompleted: false + }; + + // Set up WebSocket event handlers + ws.on('open', () => { + console.log(`✅ Connected to relay ${relayId}`); + relay.isConnected = true; + relay.reconnectAttempts = 0; + relay.lastHeartbeat = Date.now(); + }); + + ws.on('message', async (data: WebSocket.Data) => { + try { + const message = JSON.parse(data.toString()); + await this.handleRelayMessage(relayId, message); + } catch (error) { + console.error(`❌ Error parsing message from relay ${relayId}:`, error); + } + }); + + ws.on('close', () => { + console.log(`🔌 Disconnected from relay ${relayId}`); + relay.isConnected = false; + this.scheduleReconnect(relayId); + }); + + ws.on('error', (error) => { + console.error(`❌ WebSocket error for relay ${relayId}:`, error); + relay.isConnected = false; + }); + + this.relays.set(relayId, relay); + + // Wait for connection to establish + return new Promise((resolve) => { + const timeout = setTimeout(() => { + resolve(false); + }, 5000); + + ws.once('open', () => { + clearTimeout(timeout); + resolve(true); + }); + }); + + } catch (error) { + console.error(`❌ Failed to connect to relay ${relayId}:`, error); + return false; + } + } + + public disconnectFromRelay(relayId: string): void { + const relay = this.relays.get(relayId); + if (relay) { + relay.isConnected = false; + relay.ws.close(); + this.relays.delete(relayId); + console.log(`🔌 Disconnected from relay ${relayId}`); + } + } + + public getRelayById(relayId: string): RelayConnection | undefined { + return this.relays.get(relayId); + } + + public getConnectedRelays(): RelayConnection[] { + return Array.from(this.relays.values()).filter(relay => relay.isConnected); + } + + // Message Sending Methods using AnkFlag + public sendMessage(flag: AnkFlag, payload: any, targetRelayId?: string): void { + const msg: QueuedMessage = { + id: this.generateMessageId(), + flag, + payload, + targetRelayId, + timestamp: Date.now(), + expiresAt: Date.now() + 30000, // 30 seconds + retryCount: 0, + maxRetries: 3 + }; + + this.queueMessage(msg); + } + + public sendToRelay(relayId: string, flag: AnkFlag, content: any): boolean { + const relay = this.relays.get(relayId); + if (!relay || !relay.isConnected) { + console.warn(`⚠️ Cannot send to relay ${relayId}: not connected`); + return false; + } + + try { + const message = { + flag, + content, + }; + + relay.ws.send(JSON.stringify(message)); + return true; + } catch (error) { + console.error(`❌ Failed to send message to relay ${relayId}:`, error); + return false; + } + } + + public broadcastToAllRelays(flag: AnkFlag, payload: any): number { + const connectedRelays = this.getConnectedRelays(); + let sentCount = 0; + + for (const relay of connectedRelays) { + if (this.sendToRelay(relay.id, flag, payload)) { + sentCount++; + } + } + + console.log(`📡 Broadcasted to ${sentCount}/${connectedRelays.length} relays`); + return sentCount; + } + + // Protocol-Specific Message Methods + public sendNewTxMessage(message: string, targetRelayId?: string): void { + // Use appropriate AnkFlag for new transaction + this.sendMessage("NewTx" as AnkFlag, message, targetRelayId); + } + + public sendCommitMessage(message: string, targetRelayId?: string): void { + // Use appropriate AnkFlag for commit + this.sendMessage("Commit" as AnkFlag, message, targetRelayId); + } + + public sendCipherMessages(ciphers: string[], targetRelayId?: string): void { + for (const cipher of ciphers) { + // Use appropriate AnkFlag for cipher + this.sendMessage("Cipher" as AnkFlag, cipher, targetRelayId); + } + } + + public sendFaucetMessage(message: string, targetRelayId?: string): void { + // Use appropriate AnkFlag for faucet + console.log(`📨 Sending faucet message to relay ${targetRelayId}:`, message); + this.sendMessage("Faucet" as AnkFlag, message, targetRelayId); + } + + // Message Queue Management + private queueMessage(message: QueuedMessage): void { + this.messageQueue.push(message); + console.log(`📬 Queued message ${message.id} with flag ${message.flag}`); + } + + private async processQueue(): Promise { + if (this.processingQueue || this.messageQueue.length === 0) { + return; + } + + this.processingQueue = true; + + try { + const message = this.messageQueue.shift(); + if (!message) { + return; + } + + // Check if message has expired + if (message.expiresAt && Date.now() > message.expiresAt) { + console.warn(`⏰ Message ${message.id} expired`); + return; + } + + await this.deliverMessage(message); + } finally { + this.processingQueue = false; + } + } + + private async deliverMessage(message: QueuedMessage): Promise { + try { + let delivered = false; + + if (message.targetRelayId) { + // Send to specific relay + delivered = this.sendToRelay(message.targetRelayId, message.flag, message.payload); + } else { + // Broadcast to all connected relays + const sentCount = this.broadcastToAllRelays(message.flag, message.payload); + delivered = sentCount > 0; + } + + if (!delivered) { + throw new Error('No suitable relay available'); + } + + console.log(`✅ Message ${message.id} delivered`); + } catch (error) { + console.error(`❌ Failed to deliver message ${message.id}:`, error); + message.retryCount++; + + if (message.retryCount < message.maxRetries) { + // Re-queue with exponential backoff + setTimeout(() => { + this.queueMessage(message); + }, Math.pow(2, message.retryCount) * 1000); + } else { + console.error(`💀 Message ${message.id} failed after ${message.maxRetries} retries`); + } + } + } + + // Relay Message Handling + private handleRelayMessage(relayId: string, message: any): void { + console.log(`📨 Received message from relay ${relayId}:`, message); + + // Handle different types of relay responses + if (message.flag) { + // Handle protocol-specific responses + this.handleProtocolMessage(relayId, message); + } else if (message.type === 'heartbeat') { + // Update heartbeat + const relay = this.relays.get(relayId); + if (relay) { + relay.lastHeartbeat = Date.now(); + } + } else if (message.type === 'handshake') { + // Handle handshake messages + if (this.handshakeCallback) { + this.handshakeCallback(this.relays.get(relayId)?.url || 'unknown', message); + } + this.markHandshakeCompleted(relayId); + } + } + + private handleProtocolMessage(relayId: string, message: any): void { + // Handle different AnkFlag responses + switch (message.flag) { + case "NewTx": + console.log(`📨 NewTx response from relay ${relayId}`); + setImmediate(() => { + Service.getInstance().parseNewTx(message.content); + }); + break; + case "Commit": + console.log(`📨 Commit response from relay ${relayId}`); + break; + case "Cipher": + console.log(`📨 Cipher response from relay ${relayId}`); + setImmediate(() => { + Service.getInstance().parseCipher(message.content); + }); + break; + case "Handshake": + console.log(`📨 Handshake response from relay ${relayId}`); + if (this.handshakeCallback) { + this.handshakeCallback(this.relays.get(relayId)?.url || 'unknown', message); + } + this.markHandshakeCompleted(relayId); + break; + default: + console.log(`📨 Unknown flag response from relay ${relayId}:`, message.flag); + } + } + + // Reconnection Logic + private scheduleReconnect(relayId: string): void { + const relay = this.relays.get(relayId); + if (!relay || relay.reconnectAttempts >= relay.maxReconnectAttempts) { + console.log(`💀 Max reconnection attempts reached for relay ${relayId}`); + this.relays.delete(relayId); + return; + } + + const delay = Math.pow(2, relay.reconnectAttempts) * 1000; // Exponential backoff + console.log(`🔄 Scheduling reconnect to relay ${relayId} in ${delay}ms (attempt ${relay.reconnectAttempts + 1})`); + + setTimeout(async () => { + relay.reconnectAttempts++; + await this.connectToRelay(relayId, relay.url, relay.spAddress); + }, delay); + } + + private startReconnectMonitoring(): void { + this.reconnectInterval = setInterval(() => { + // Check for disconnected relays and attempt reconnection + for (const [relayId, relay] of this.relays) { + if (!relay.isConnected && relay.reconnectAttempts < relay.maxReconnectAttempts) { + this.scheduleReconnect(relayId); + } + } + }, 10000); // Check every 10 seconds + } + + // Heartbeat Management + private startHeartbeat(): void { + this.heartbeatInterval = setInterval(() => { + const now = Date.now(); + const heartbeatMessage = { + type: 'heartbeat', + timestamp: now + }; + + for (const [relayId, relay] of this.relays) { + if (relay.isConnected) { + try { + relay.ws.send(JSON.stringify(heartbeatMessage)); + relay.lastHeartbeat = now; + } catch (error) { + console.error(`❌ Heartbeat failed for relay ${relayId}:`, error); + relay.isConnected = false; + } + } + } + }, 30000); // 30 seconds + } + + private startQueueProcessing(): void { + this.queueProcessingInterval = setInterval(() => { + this.processQueue(); + }, 100); // Process queue every 100ms + } + + // Utility Methods + private generateMessageId(): string { + return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + public getStats(): any { + return { + totalRelays: this.relays.size, + connectedRelays: this.getConnectedRelays().length, + queuedMessages: this.messageQueue.length + }; + } + + public shutdown(): void { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + } + if (this.queueProcessingInterval) { + clearInterval(this.queueProcessingInterval); + } + if (this.reconnectInterval) { + clearInterval(this.reconnectInterval); + } + + for (const [relayId] of this.relays) { + this.disconnectFromRelay(relayId); + } + + console.log('🛑 Relay manager shutdown complete'); + } + + /** + * Mark a relay as having completed its handshake. + * @param relayId - The ID of the relay that completed handshake. + */ + private markHandshakeCompleted(relayId: string): void { + const relay = this.relays.get(relayId); + if (relay) { + relay.handshakeCompleted = true; + this.handshakeCompletedRelays.add(relayId); + console.log(`✅ Handshake completed for relay ${relayId}`); + if (relay.handshakeResolve) { + relay.handshakeResolve(); + } + } + } + + /** + * Wait for handshake completion from at least one relay + * @param timeoutMs - Timeout in milliseconds (default: 10000) + * @returns Promise that resolves when at least one handshake is completed + */ + public async waitForHandshake(timeoutMs: number = 10000): Promise { + const startTime = Date.now(); + const pollInterval = 100; // Check every 100ms + + return new Promise((resolve, reject) => { + const checkForHandshake = () => { + // Check if we have any completed handshakes + if (this.handshakeCompletedRelays.size > 0) { + console.log(`✅ Handshake completed from ${this.handshakeCompletedRelays.size} relay(s)`); + resolve(); + return; + } + + // Check timeout + if (Date.now() - startTime >= timeoutMs) { + reject(new Error(`No handshake completed after ${timeoutMs}ms timeout`)); + return; + } + + // Continue polling + setTimeout(checkForHandshake, pollInterval); + }; + + checkForHandshake(); + }); + } + + /** + * Wait for handshake completion from a specific relay + * @param relayId - The relay ID to wait for + * @param timeoutMs - Timeout in milliseconds (default: 10000) + * @returns Promise that resolves when the specific relay's handshake is completed + */ + public async waitForRelayHandshake(relayId: string, timeoutMs: number = 10000): Promise { + const relay = this.relays.get(relayId); + if (!relay) { + throw new Error(`Relay ${relayId} not found`); + } + + if (relay.handshakeCompleted) { + return Promise.resolve(); + } + + if (!relay.handshakePromise) { + relay.handshakePromise = new Promise((resolve, reject) => { + relay.handshakeResolve = resolve; + + // Set timeout + setTimeout(() => { + reject(new Error(`Handshake timeout for relay ${relayId} after ${timeoutMs}ms`)); + }, timeoutMs); + }); + } + + return relay.handshakePromise; + } + + /** + * Check if a relay has completed handshake + * @param relayId - The relay ID to check + * @returns True if handshake is completed, false otherwise + */ + public isHandshakeCompleted(relayId: string): boolean { + return this.handshakeCompletedRelays.has(relayId); + } + + /** + * Get all relays that have completed handshake + * @returns Array of relay IDs that have completed handshake + */ + public getHandshakeCompletedRelays(): string[] { + return Array.from(this.handshakeCompletedRelays); + } +} \ No newline at end of file diff --git a/src/service.ts b/src/service.ts index c6a015d..89542b1 100644 --- a/src/service.ts +++ b/src/service.ts @@ -2,15 +2,28 @@ 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 = new Map(); + private processes: Map = 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() { @@ -31,39 +44,570 @@ export class Service { return Service.instance; } + // Handle the handshake message + public async handleHandshakeMsg(url: string, parsedMsg: any) { + try { + const handshakeMsg: HandshakeMessage = JSON.parse(parsedMsg.content); + 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 + for (const [processId, member] of Object.entries(handshakeMsg.peers_list)) { + this.membersList[processId] = member as Member; + } + } + + setTimeout(async () => { + const newProcesses: OutPointProcessMap = handshakeMsg.processes_list; + if (!newProcesses || Object.keys(newProcesses).length === 0) { + 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 = {}; + 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']); + // 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 { + 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 { + 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 { + 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 { + const db = await Database.getInstance(); + return await db.getObject('shared_secrets', address); + } + + private async getTokensFromFaucet(): Promise { + try { + await this.ensureSufficientAmount(); + } catch (e) { + console.error('Failed to get tokens from relay, check connection'); + return; + } + } + + public getAllMembers(): Record { + 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 { + // Ensure the amount is available before proceeding + await this.getTokensFromFaucet(); + let unconnectedAddresses = []; + const myAddress = this.getDeviceAddress(); + for (const member of members) { + const sp_addresses = member.sp_addresses; + if (!sp_addresses || sp_addresses.length === 0) continue; + for (const address of sp_addresses) { + // For now, we ignore our own device address, although there might be use cases for having a secret with ourselves + if (address === myAddress) continue; + const sharedSecret = await this.getSecretForAddress(address); + if (!sharedSecret) { + unconnectedAddresses.push(address); + } + } + } + if (unconnectedAddresses && unconnectedAddresses.length != 0) { + const apiResult = await this.connectAddresses(unconnectedAddresses); + await this.handleApiReturn(apiResult); + } + } + + public async connectAddresses(addresses: string[]): Promise { + if (addresses.length === 0) { + throw new Error('Trying to connect to empty addresses list'); + } + + try { + return wasm.create_transaction(addresses, 1); + } catch (e) { + console.error('Failed to connect member:', e); + throw e; + } + } + + private async ensureSufficientAmount(): Promise { + 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 { + 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) { + const jsonCompatibleData: Record = {}; + const binaryData: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + if (this.isFileBlob(value)) { + binaryData[key] = value; + } else { + jsonCompatibleData[key] = value; + } + } + + return { jsonCompatibleData, binaryData }; + } + + public async 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 { + 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 { + 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 = { + pairing: { + members: [], + validation_rules: [ + { + quorum: 1.0, + fields: validation_fields, + min_sig_member: 1.0, + }, + ], + storages: this.storages + }, + }; + try { + return this.createProcess( + privateData, + publicData, + roles + ); + } catch (e) { + throw new Error(`Creating process failed:, ${e}`); + } + } + + public async createProcess( + privateData: Record, + publicData: Record, + roles: Record, + ): Promise { + // 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 = new Set(); + for (const role of Object.values(roles!)) { + for (const member of role.members) { + // Check if we know the member that matches this id + const memberAddresses = this.getAddressesForMemberId(member); + if (memberAddresses && memberAddresses.length != 0) { + members.add({ sp_addresses: memberAddresses }); + } + } + } + await this.checkConnections([...members]); + + const result = wasm.create_new_process ( + encodedPrivateData, + roles, + encodedPublicData, + relayAddress, + feeRate, + this.getAllMembers() + ); + + return(result); + } + + async parseCipher(message: string): Promise { + const membersList = this.getAllMembers(); + try { + const apiReturn = wasm.parse_cipher(message, membersList); + await this.handleApiReturn(apiReturn); + } catch (e) { + console.error(`Failed to parse cipher: ${e}`); + } + } + + async parseNewTx(tx: string): Promise { + 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 { console.log(`📢 Creating PRD update for process ${processId}, state ${stateId}`); try { - // Get the process from cache - const process = this.processes.get(processId); + const process = await this.getProcess(processId); if (!process) { throw new Error('Process not found'); } - // Find the state - const state = process.states.find((s: any) => s.state_id === stateId); - if (!state) { - throw new Error('State not found'); - } - - // Use WASM function to create update message - const result = wasm.create_update_message(process, stateId, this.membersList); - - if (result.updated_process) { - // Update our cache - this.processes.set(processId, result.updated_process.current_process); - - // Save to database - await this.saveProcessToDb(processId, result.updated_process.current_process); - - return result; - } else { - throw new Error('Failed to create update message'); - } + const result = wasm.create_update_message(process, stateId, this.membersList); + return result; } catch (error) { - throw new Error(`WASM error: ${error}`); + throw new Error(`Failed to create update message: ${error}`); } } @@ -72,34 +616,15 @@ export class Service { console.log(`✅ Approving change for process ${processId}, state ${stateId}`); try { - // Get the process from cache const process = this.processes.get(processId); if (!process) { throw new Error('Process not found'); } - // Find the state - const state = process.states.find((s: any) => s.state_id === stateId); - if (!state) { - throw new Error('State not found'); - } - - // Use WASM function to validate state const result = wasm.validate_state(process, stateId, this.membersList); - - if (result.updated_process) { - // Update our cache - this.processes.set(processId, result.updated_process.current_process); - - // Save to database - await this.saveProcessToDb(processId, result.updated_process.current_process); - - return result; - } else { - throw new Error('Failed to validate state'); - } + return result; } catch (error) { - throw new Error(`WASM error: ${error}`); + throw new Error(`Failed to validate state: ${error}`); } } @@ -234,8 +759,66 @@ export class Service { } } - // Utility method: Check if device is paired - isPaired(): boolean { + public async getDeviceFromDatabase(): Promise { + 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 { + const device = await this.getDeviceFromDatabase(); + return device !== null; + } + + public async restoreDeviceFromDatabase(device: Device): Promise { + 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) { @@ -244,29 +827,45 @@ export class Service { } } - // Utility method: Get last committed state - getLastCommitedState(process: any): any { - return process.states.find((s: any) => s.commited_in) || null; + public getLastCommitedState(process: Process): ProcessState | null { + const index = this.getLastCommitedStateIndex(process); + if (index === null) return null; + return process.states[index]; } - // Utility method: Get last committed state index - getLastCommitedStateIndex(process: any): number | null { - const index = process.states.findIndex((s: any) => s.commited_in); - return index >= 0 ? index : null; - } - - // Utility method: Check if roles contain current user - rolesContainsUs(roles: Record): boolean { - try { - // This would need to be implemented based on your user management - // For now, return true for testing - return true; - } catch (error) { - console.error('Error checking roles:', error); - return true; // Fallback to true for testing + public getLastCommitedStateIndex(process: Process): number | null { + if (process.states.length === 0) return null; + const processTip = process.states[process.states.length - 1].commited_in; + for (let i = process.states.length - 1; i >= 0; i--) { + if (process.states[i].commited_in !== processTip) { + return i; + } } + return null; } + public rolesContainsUs(roles: Record): boolean { + let us; + try { + us = wasm.get_pairing_process_id(); + } catch (e) { + throw e; + } + + return this.rolesContainsMember(roles, us); + } + + public rolesContainsMember(roles: Record, 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; @@ -282,24 +881,20 @@ export class Service { } } - // WebSocket message methods (stubs for now) + // WebSocket message methods using Relay Manager async sendNewTxMessage(message: string) { console.log('📤 Sending NewTx message:', message); - // TODO: Implement actual WebSocket sending + this.relayManager.sendNewTxMessage(message); } async sendCommitMessage(message: string) { - console.log('�� Sending Commit message:', message); - // TODO: Implement actual WebSocket sending + console.log('📤 Sending Commit message:', message); + this.relayManager.sendCommitMessage(message); } async sendCipherMessages(ciphers: string[]) { console.log('📤 Sending Cipher messages:', ciphers.length, 'ciphers'); - for (let i = 0; i < ciphers.length; i++) { - const cipher = ciphers[i]; - console.log('📤 Sending Cipher:', cipher); - // TODO: Implement actual WebSocket sending - } + this.relayManager.sendCipherMessages(ciphers); } // Blob and data storage methods @@ -368,10 +963,7 @@ export class Service { return uint8Array; } - // Main handleApiReturn method public async handleApiReturn(apiReturn: ApiReturn) { - console.log('🔄 Handling API return:', apiReturn); - if (apiReturn.partial_tx) { try { const res = wasm.sign_transaction(apiReturn.partial_tx); @@ -459,4 +1051,95 @@ export class Service { await this.sendCipherMessages(apiReturn.ciphers_to_send); } } + + public async batchSaveProcessesToDb(processes: Record) { + 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[]) { + console.log('Requesting data from peers'); + const membersList = this.getAllMembers(); + try { + const res = wasm.request_data(processId, stateIds, roles, membersList); + await this.handleApiReturn(res); + } catch (e) { + console.error(e); + } + } + + 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 { + 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}`); + } + } } diff --git a/src/simple-server.ts b/src/simple-server.ts index 0b961c0..1d9269d 100644 --- a/src/simple-server.ts +++ b/src/simple-server.ts @@ -2,7 +2,9 @@ import WebSocket from 'ws'; import { MessageType } from './models'; import { config } from './config'; import { Service } from './service'; -import { ApiReturn } from '../pkg/sdk_client'; +import { ApiReturn, Process } from '../pkg/sdk_client'; +import { EMPTY32BYTES } from './utils'; +import { rust_zstd_wasm_shim_calloc } from '../pkg/sdk_client_bg.wasm'; interface ServerMessageEvent { data: { @@ -58,15 +60,10 @@ class SimpleProcessHandlers { throw new Error('Device not paired'); } - // Create test process if it doesn't exist - let process = await this.service.getProcess(processId); - if (!process) { - process = this.service.createTestProcess(processId); - } - let res: ApiReturn; try { res = await this.service.createPrdUpdate(processId, stateId); + await this.service.handleApiReturn(res); } catch (e) { throw new Error(e as string); } @@ -77,7 +74,7 @@ class SimpleProcessHandlers { }; } catch (e) { const errorMsg = `Failed to notify update for process: ${e}`; - return this.errorResponse(errorMsg, event.clientId, event.data.messageId); + throw new Error(errorMsg); } } @@ -98,17 +95,10 @@ class SimpleProcessHandlers { throw new Error('Device not paired'); } - // Create test process if it doesn't exist - let process = await this.service.getProcess(processId); - if (!process) { - process = this.service.createTestProcess(processId); - } - - // Execute actual protocol logic let res: ApiReturn; try { res = await this.service.approveChange(processId, stateId); - + await this.service.handleApiReturn(res); } catch (e) { throw new Error(e as string); } @@ -120,7 +110,7 @@ class SimpleProcessHandlers { }; } catch (e) { const errorMsg = `Failed to validate process: ${e}`; - return this.errorResponse(errorMsg, event.clientId, event.data.messageId); + throw new Error(errorMsg); } } @@ -129,93 +119,79 @@ class SimpleProcessHandlers { throw new Error('Invalid message type'); } + if (!this.service.isPaired()) { + throw new Error('Device not paired'); + } + try { + // privateFields is only used if newData contains new fields + // roles can be empty meaning that roles from the last commited state are kept const { processId, newData, privateFields, roles, apiKey } = event.data; if (!apiKey || !this.validateApiKey(apiKey)) { throw new Error('Invalid API key'); } - // Check if device is paired - if (!this.service.isPaired()) { - throw new Error('Device not paired'); - } - - // Get or create the process - let process = await this.service.getProcess(processId); + // Check if the new data is already in the process or if it's a new field + const process = await this.service.getProcess(processId); if (!process) { - process = this.service.createTestProcess(processId); + throw new Error('Process not found'); } - - // Get the last committed state - let lastState = this.service.getLastCommitedState(process); + const lastState = this.service.getLastCommitedState(process); if (!lastState) { - const firstState = process.states[0]; - const roles = firstState.roles; - if (this.service.rolesContainsUs(roles)) { - const approveChangeRes = await this.service.approveChange(processId, firstState.state_id); - const prdUpdateRes = await this.service.createPrdUpdate(processId, firstState.state_id); - } else { - if (firstState.validation_tokens.length > 0) { - const res = await this.service.createPrdUpdate(processId, firstState.state_id); - } - } - // Wait a couple seconds - await new Promise(resolve => setTimeout(resolve, 2000)); - const updatedProcess = await this.service.getProcess(processId); - if (!updatedProcess) { - throw new Error('Failed to get updated process'); - } - process = updatedProcess; - lastState = this.service.getLastCommitedState(process); - if (!lastState) { - throw new Error('Process doesn\'t have a committed state yet'); - } + throw new Error('Process doesn\'t have a commited state yet'); } - const lastStateIndex = this.service.getLastCommitedStateIndex(process); if (lastStateIndex === null) { - throw new Error('Process doesn\'t have a committed state yet'); - } + throw new Error('Process doesn\'t have a commited state yet'); + } - // Split data into private and public const privateData: Record = {}; const publicData: Record = {}; for (const field of Object.keys(newData)) { // Public data are carried along each new state + // TODO I hope that at some point we stop doing that + // So the first thing we can do is check if the new data is public data if (lastState.public_data[field]) { + // Add it to public data publicData[field] = newData[field]; continue; } - // If it's not a public data, it may be either a private data update, or a new field + // If it's not a public data, it may be either a private data update, or a new field (public of private) + // Caller gave us a list of new private fields, if we see it here this is a new private field if (privateFields.includes(field)) { + // Add it to private data privateData[field] = newData[field]; continue; } - // Check if field exists in previous states private data + // Now it can be an update of private data or a new public data + // We check that the field exists in previous states private data for (let i = lastStateIndex; i >= 0; i--) { const state = process.states[i]; if (state.pcd_commitment[field]) { + // We don't even check if it's a public field, we would have seen it in the last state + // TODO maybe that's an issue if we remove a public field at some point? That's not explicitly forbidden but not really supported yet privateData[field] = newData[field]; break; + } else { + // This attribute was not modified in that state, we go back to the previous state + continue; } } if (privateData[field]) continue; - // It's a new public field + // We've get back all the way to the first state without seeing it, it's a new public field publicData[field] = newData[field]; } - let res: ApiReturn; - try { - res = await this.service.updateProcess(process, privateData, publicData, roles); - } catch (e) { - throw new Error(e as string); - } + // We'll let the wasm check if roles are consistent + + const res = await this.service.updateProcess(process, privateData, publicData, roles); + await this.service.handleApiReturn(res); return { type: MessageType.PROCESS_UPDATED, @@ -224,11 +200,11 @@ class SimpleProcessHandlers { }; } catch (e) { const errorMsg = `Failed to update process: ${e}`; - return this.errorResponse(errorMsg, event.clientId, event.data.messageId); + throw new Error(errorMsg); } } - async handleMessage(event: ServerMessageEvent): Promise { + async handleMessage(event: ServerMessageEvent): Promise { try { switch (event.data.type) { case MessageType.NOTIFY_UPDATE: @@ -241,8 +217,7 @@ class SimpleProcessHandlers { throw new Error(`Unhandled message type: ${event.data.type}`); } } catch (error) { - const errorMsg = `Error handling message: ${error}`; - return this.errorResponse(errorMsg, event.clientId, event.data.messageId); + return this.errorResponse(error as string, event.clientId, event.data.messageId); } } } @@ -269,6 +244,64 @@ export class Server { // Setup WebSocket handlers this.setupWebSocketHandlers(); + + // Check if we have a device + const hasDevice = await service.hasDevice(); + if (!hasDevice) { + const spAddress = await service.createNewDevice(); + console.log('🔑 New device created:', spAddress); + } else { + console.log('🔑 Device found, restoring from database...'); + const device = await service.getDeviceFromDatabase(); + const metadata = await service.getDeviceMetadata(); + + if (device) { + await service.restoreDeviceFromDatabase(device); + console.log('🔑 Device restored successfully'); + if (metadata) { + console.log(`📋 Device info: ${metadata.device_address} (created: ${metadata.created_at})`); + } + } else { + console.error('❌ Failed to retrieve device from database'); + } + } + + // Check if we are paired + if (!service.isPaired()) { + console.log('🔑 Not paired, creating pairing process...'); + try { + const pairingResult = await service.createPairingProcess('', []); + const processId: string = pairingResult.updated_process?.process_id; + const stateId = pairingResult.updated_process?.current_process?.states[0].state_id; + if (!processId || !stateId) { + throw new Error('Failed to get process id or state id'); + } + await service.handleApiReturn(pairingResult); + const udpateResult = await service.createPrdUpdate(processId, stateId); + await service.handleApiReturn(udpateResult); + const approveResult = await service.approveChange(processId, stateId); + await service.handleApiReturn(approveResult); + + // now pair the device + service.pairDevice(processId, [service.getDeviceAddress()]); + + // Update the device in the database + const device = service.dumpDeviceFromMemory(); + if (device) { + await service.restoreDeviceFromDatabase(device); + } else { + throw new Error('Failed to dump device from wasm'); + } + + console.log('🔑 Pairing process created successfully'); + } catch (error) { + console.error('❌ Failed to create pairing process:', error); + } + } else { + console.log('🔑 Already paired with id:', service.getPairingProcessId()); + } + + // Relays are automatically initialized in Service constructor console.log(`✅ Simple server running on port ${this.wss.options.port}`); console.log('📋 Supported operations: UPDATE_PROCESS, NOTIFY_UPDATE, VALIDATE_STATE'); @@ -305,8 +338,7 @@ export class Server { }; const response = await this.handlers.handleMessage(serverEvent); - this.sendToClient(ws, response); - + this.sendToClient(ws, response); } catch (error) { console.error(`❌ Error handling message from ${clientId}:`, error); this.sendToClient(ws, { diff --git a/src/utils.ts b/src/utils.ts index cc243bb..2fb30e5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ // Server-specific utility functions +export const EMPTY32BYTES = String('').padStart(64, '0'); export function isValid32ByteHex(value: string): boolean { // Check if the value is a valid 32-byte hex string (64 characters)