Functional pairing

This commit is contained in:
Sosthene 2025-08-06 17:04:15 +02:00
parent 7c4e81331b
commit afe12860cf
6 changed files with 1430 additions and 146 deletions

View File

@ -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';

View File

@ -38,3 +38,23 @@ export enum MessageType {
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',
}

547
src/relay-manager.ts Normal file
View File

@ -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<void>; // 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<string, RelayConnection> = 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<string> = 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<boolean> {
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<void> {
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<void> {
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<void> {
const startTime = Date.now();
const pollInterval = 100; // Check every 100ms
return new Promise<void>((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<void> {
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<void>((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);
}
}

View File

@ -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<string, any> = new Map();
private processes: Map<string, Process> = 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<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']);
// 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: [
{
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<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({ 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();
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<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 {
// 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');
}
} 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');
}
} 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<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) {
@ -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;
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;
}
// Utility method: Check if roles contain current user
rolesContainsUs(roles: Record<string, any>): boolean {
public rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
let us;
try {
// This would need to be implemented based on your user management
// For now, return true for testing
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;
} catch (error) {
console.error('Error checking roles:', error);
return true; // Fallback to true for testing
}
}
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('<EFBFBD><EFBFBD> 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<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, 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<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}`);
}
}
}

View File

@ -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);
throw new Error('Process doesn\'t have a commited state yet');
}
}
// 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');
}
}
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<string, any> = {};
const publicData: Record<string, any> = {};
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,7 +200,7 @@ class SimpleProcessHandlers {
};
} catch (e) {
const errorMsg = `Failed to update process: ${e}`;
return this.errorResponse(errorMsg, event.clientId, event.data.messageId);
throw new Error(errorMsg);
}
}
@ -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);
}
}
}
@ -270,6 +245,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');
console.log('🔑 Authentication: API key required for all operations');
@ -306,7 +339,6 @@ export class Server {
const response = await this.handlers.handleMessage(serverEvent);
this.sendToClient(ws, response);
} catch (error) {
console.error(`❌ Error handling message from ${clientId}:`, error);
this.sendToClient(ws, {

View File

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