Functional pairing
This commit is contained in:
parent
7c4e81331b
commit
afe12860cf
@ -1,8 +1,9 @@
|
|||||||
// Main entry point for the SDK Signer server
|
// Main entry point for the SDK Signer server
|
||||||
export { Service } from './service';
|
export { Service } from './service';
|
||||||
export { config } from './config';
|
export { config } from './config';
|
||||||
export { MessageType } from './models';
|
export { MessageType, AnkFlag } from './models';
|
||||||
export { isValid32ByteHex } from './utils';
|
export { isValid32ByteHex } from './utils';
|
||||||
|
export { RelayManager } from './relay-manager';
|
||||||
|
|
||||||
// Re-export the main server class
|
// Re-export the main server class
|
||||||
export { Server } from './simple-server';
|
export { Server } from './simple-server';
|
@ -37,4 +37,24 @@ export enum MessageType {
|
|||||||
// Account management
|
// Account management
|
||||||
ADD_DEVICE = 'ADD_DEVICE',
|
ADD_DEVICE = 'ADD_DEVICE',
|
||||||
DEVICE_ADDED = 'DEVICE_ADDED',
|
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
547
src/relay-manager.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
837
src/service.ts
837
src/service.ts
@ -2,15 +2,28 @@
|
|||||||
import Database from './database.service';
|
import Database from './database.service';
|
||||||
import * as wasm from '../pkg/sdk_client';
|
import * as wasm from '../pkg/sdk_client';
|
||||||
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../pkg/sdk_client';
|
import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../pkg/sdk_client';
|
||||||
|
import { RelayManager } from './relay-manager';
|
||||||
|
import { config } from './config';
|
||||||
|
import { EMPTY32BYTES } from './utils';
|
||||||
|
|
||||||
|
const DEFAULTAMOUNT = 1000n;
|
||||||
|
const DEVICE_KEY = 'main_device';
|
||||||
|
|
||||||
export class Service {
|
export class Service {
|
||||||
private static instance: Service;
|
private static instance: Service;
|
||||||
private processes: Map<string, any> = new Map();
|
private processes: Map<string, Process> = new Map();
|
||||||
private membersList: any = {};
|
private membersList: any = {};
|
||||||
|
private relayManager: RelayManager;
|
||||||
|
private storages: string[] = []; // storage urls
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
console.log('🔧 Service initialized');
|
console.log('🔧 Service initialized');
|
||||||
|
this.relayManager = RelayManager.getInstance();
|
||||||
|
this.relayManager.setHandshakeCallback((url: string, message: any) => {
|
||||||
|
this.handleHandshakeMsg(url, message);
|
||||||
|
});
|
||||||
this.initWasm();
|
this.initWasm();
|
||||||
|
// Removed automatic relay initialization - will connect when needed
|
||||||
}
|
}
|
||||||
|
|
||||||
private initWasm() {
|
private initWasm() {
|
||||||
@ -31,39 +44,570 @@ export class Service {
|
|||||||
return Service.instance;
|
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
|
// Core protocol method: Create PRD Update
|
||||||
async createPrdUpdate(processId: string, stateId: string): Promise<ApiReturn> {
|
async createPrdUpdate(processId: string, stateId: string): Promise<ApiReturn> {
|
||||||
console.log(`📢 Creating PRD update for process ${processId}, state ${stateId}`);
|
console.log(`📢 Creating PRD update for process ${processId}, state ${stateId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the process from cache
|
const process = await this.getProcess(processId);
|
||||||
const process = this.processes.get(processId);
|
|
||||||
if (!process) {
|
if (!process) {
|
||||||
throw new Error('Process not found');
|
throw new Error('Process not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the state
|
const result = wasm.create_update_message(process, stateId, this.membersList);
|
||||||
const state = process.states.find((s: any) => s.state_id === stateId);
|
return result;
|
||||||
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) {
|
} 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}`);
|
console.log(`✅ Approving change for process ${processId}, state ${stateId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the process from cache
|
|
||||||
const process = this.processes.get(processId);
|
const process = this.processes.get(processId);
|
||||||
if (!process) {
|
if (!process) {
|
||||||
throw new Error('Process not found');
|
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);
|
const result = wasm.validate_state(process, stateId, this.membersList);
|
||||||
|
return result;
|
||||||
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) {
|
} 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
|
public async getDeviceFromDatabase(): Promise<Device | null> {
|
||||||
isPaired(): boolean {
|
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 {
|
try {
|
||||||
return wasm.is_paired();
|
return wasm.is_paired();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -244,29 +827,45 @@ export class Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility method: Get last committed state
|
public getLastCommitedState(process: Process): ProcessState | null {
|
||||||
getLastCommitedState(process: any): any {
|
const index = this.getLastCommitedStateIndex(process);
|
||||||
return process.states.find((s: any) => s.commited_in) || null;
|
if (index === null) return null;
|
||||||
|
return process.states[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility method: Get last committed state index
|
public getLastCommitedStateIndex(process: Process): number | null {
|
||||||
getLastCommitedStateIndex(process: any): number | null {
|
if (process.states.length === 0) return null;
|
||||||
const index = process.states.findIndex((s: any) => s.commited_in);
|
const processTip = process.states[process.states.length - 1].commited_in;
|
||||||
return index >= 0 ? index : null;
|
for (let i = process.states.length - 1; i >= 0; i--) {
|
||||||
}
|
if (process.states[i].commited_in !== processTip) {
|
||||||
|
return i;
|
||||||
// Utility method: Check if roles contain current user
|
}
|
||||||
rolesContainsUs(roles: Record<string, any>): 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
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
|
||||||
|
let us;
|
||||||
|
try {
|
||||||
|
us = wasm.get_pairing_process_id();
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.rolesContainsMember(roles, us);
|
||||||
|
}
|
||||||
|
|
||||||
|
public rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean {
|
||||||
|
for (const roleDef of Object.values(roles)) {
|
||||||
|
if (roleDef.members.includes(pairingProcessId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Utility method: Add member to the members list
|
// Utility method: Add member to the members list
|
||||||
addMember(outpoint: string, member: any) {
|
addMember(outpoint: string, member: any) {
|
||||||
this.membersList[outpoint] = member;
|
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) {
|
async sendNewTxMessage(message: string) {
|
||||||
console.log('📤 Sending NewTx message:', message);
|
console.log('📤 Sending NewTx message:', message);
|
||||||
// TODO: Implement actual WebSocket sending
|
this.relayManager.sendNewTxMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendCommitMessage(message: string) {
|
async sendCommitMessage(message: string) {
|
||||||
console.log('<EFBFBD><EFBFBD> Sending Commit message:', message);
|
console.log('📤 Sending Commit message:', message);
|
||||||
// TODO: Implement actual WebSocket sending
|
this.relayManager.sendCommitMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendCipherMessages(ciphers: string[]) {
|
async sendCipherMessages(ciphers: string[]) {
|
||||||
console.log('📤 Sending Cipher messages:', ciphers.length, 'ciphers');
|
console.log('📤 Sending Cipher messages:', ciphers.length, 'ciphers');
|
||||||
for (let i = 0; i < ciphers.length; i++) {
|
this.relayManager.sendCipherMessages(ciphers);
|
||||||
const cipher = ciphers[i];
|
|
||||||
console.log('📤 Sending Cipher:', cipher);
|
|
||||||
// TODO: Implement actual WebSocket sending
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blob and data storage methods
|
// Blob and data storage methods
|
||||||
@ -368,10 +963,7 @@ export class Service {
|
|||||||
return uint8Array;
|
return uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main handleApiReturn method
|
|
||||||
public async handleApiReturn(apiReturn: ApiReturn) {
|
public async handleApiReturn(apiReturn: ApiReturn) {
|
||||||
console.log('🔄 Handling API return:', apiReturn);
|
|
||||||
|
|
||||||
if (apiReturn.partial_tx) {
|
if (apiReturn.partial_tx) {
|
||||||
try {
|
try {
|
||||||
const res = wasm.sign_transaction(apiReturn.partial_tx);
|
const res = wasm.sign_transaction(apiReturn.partial_tx);
|
||||||
@ -459,4 +1051,95 @@ export class Service {
|
|||||||
await this.sendCipherMessages(apiReturn.ciphers_to_send);
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ import WebSocket from 'ws';
|
|||||||
import { MessageType } from './models';
|
import { MessageType } from './models';
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
import { Service } from './service';
|
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 {
|
interface ServerMessageEvent {
|
||||||
data: {
|
data: {
|
||||||
@ -58,15 +60,10 @@ class SimpleProcessHandlers {
|
|||||||
throw new Error('Device not paired');
|
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;
|
let res: ApiReturn;
|
||||||
try {
|
try {
|
||||||
res = await this.service.createPrdUpdate(processId, stateId);
|
res = await this.service.createPrdUpdate(processId, stateId);
|
||||||
|
await this.service.handleApiReturn(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e as string);
|
throw new Error(e as string);
|
||||||
}
|
}
|
||||||
@ -77,7 +74,7 @@ class SimpleProcessHandlers {
|
|||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMsg = `Failed to notify update for process: ${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');
|
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;
|
let res: ApiReturn;
|
||||||
try {
|
try {
|
||||||
res = await this.service.approveChange(processId, stateId);
|
res = await this.service.approveChange(processId, stateId);
|
||||||
|
await this.service.handleApiReturn(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e as string);
|
throw new Error(e as string);
|
||||||
}
|
}
|
||||||
@ -120,7 +110,7 @@ class SimpleProcessHandlers {
|
|||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMsg = `Failed to validate process: ${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');
|
throw new Error('Invalid message type');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.service.isPaired()) {
|
||||||
|
throw new Error('Device not paired');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
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;
|
const { processId, newData, privateFields, roles, apiKey } = event.data;
|
||||||
|
|
||||||
if (!apiKey || !this.validateApiKey(apiKey)) {
|
if (!apiKey || !this.validateApiKey(apiKey)) {
|
||||||
throw new Error('Invalid API key');
|
throw new Error('Invalid API key');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if device is paired
|
// Check if the new data is already in the process or if it's a new field
|
||||||
if (!this.service.isPaired()) {
|
const process = await this.service.getProcess(processId);
|
||||||
throw new Error('Device not paired');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create the process
|
|
||||||
let process = await this.service.getProcess(processId);
|
|
||||||
if (!process) {
|
if (!process) {
|
||||||
process = this.service.createTestProcess(processId);
|
throw new Error('Process not found');
|
||||||
}
|
}
|
||||||
|
const lastState = this.service.getLastCommitedState(process);
|
||||||
// Get the last committed state
|
|
||||||
let lastState = this.service.getLastCommitedState(process);
|
|
||||||
if (!lastState) {
|
if (!lastState) {
|
||||||
const firstState = process.states[0];
|
throw new Error('Process doesn\'t have a commited state yet');
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastStateIndex = this.service.getLastCommitedStateIndex(process);
|
const lastStateIndex = this.service.getLastCommitedStateIndex(process);
|
||||||
if (lastStateIndex === null) {
|
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 privateData: Record<string, any> = {};
|
||||||
const publicData: Record<string, any> = {};
|
const publicData: Record<string, any> = {};
|
||||||
|
|
||||||
for (const field of Object.keys(newData)) {
|
for (const field of Object.keys(newData)) {
|
||||||
// Public data are carried along each new state
|
// 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]) {
|
if (lastState.public_data[field]) {
|
||||||
|
// Add it to public data
|
||||||
publicData[field] = newData[field];
|
publicData[field] = newData[field];
|
||||||
continue;
|
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)) {
|
if (privateFields.includes(field)) {
|
||||||
|
// Add it to private data
|
||||||
privateData[field] = newData[field];
|
privateData[field] = newData[field];
|
||||||
continue;
|
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--) {
|
for (let i = lastStateIndex; i >= 0; i--) {
|
||||||
const state = process.states[i];
|
const state = process.states[i];
|
||||||
if (state.pcd_commitment[field]) {
|
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];
|
privateData[field] = newData[field];
|
||||||
break;
|
break;
|
||||||
|
} else {
|
||||||
|
// This attribute was not modified in that state, we go back to the previous state
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privateData[field]) 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];
|
publicData[field] = newData[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
let res: ApiReturn;
|
// We'll let the wasm check if roles are consistent
|
||||||
try {
|
|
||||||
res = await this.service.updateProcess(process, privateData, publicData, roles);
|
const res = await this.service.updateProcess(process, privateData, publicData, roles);
|
||||||
} catch (e) {
|
await this.service.handleApiReturn(res);
|
||||||
throw new Error(e as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: MessageType.PROCESS_UPDATED,
|
type: MessageType.PROCESS_UPDATED,
|
||||||
@ -224,11 +200,11 @@ class SimpleProcessHandlers {
|
|||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMsg = `Failed to update process: ${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<ServerResponse> {
|
async handleMessage(event: ServerMessageEvent): Promise<ServerResponse > {
|
||||||
try {
|
try {
|
||||||
switch (event.data.type) {
|
switch (event.data.type) {
|
||||||
case MessageType.NOTIFY_UPDATE:
|
case MessageType.NOTIFY_UPDATE:
|
||||||
@ -241,8 +217,7 @@ class SimpleProcessHandlers {
|
|||||||
throw new Error(`Unhandled message type: ${event.data.type}`);
|
throw new Error(`Unhandled message type: ${event.data.type}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = `Error handling message: ${error}`;
|
return this.errorResponse(error as string, event.clientId, event.data.messageId);
|
||||||
return this.errorResponse(errorMsg, event.clientId, event.data.messageId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -269,6 +244,64 @@ export class Server {
|
|||||||
|
|
||||||
// Setup WebSocket handlers
|
// Setup WebSocket handlers
|
||||||
this.setupWebSocketHandlers();
|
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(`✅ Simple server running on port ${this.wss.options.port}`);
|
||||||
console.log('📋 Supported operations: UPDATE_PROCESS, NOTIFY_UPDATE, VALIDATE_STATE');
|
console.log('📋 Supported operations: UPDATE_PROCESS, NOTIFY_UPDATE, VALIDATE_STATE');
|
||||||
@ -305,8 +338,7 @@ export class Server {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await this.handlers.handleMessage(serverEvent);
|
const response = await this.handlers.handleMessage(serverEvent);
|
||||||
this.sendToClient(ws, response);
|
this.sendToClient(ws, response);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error handling message from ${clientId}:`, error);
|
console.error(`❌ Error handling message from ${clientId}:`, error);
|
||||||
this.sendToClient(ws, {
|
this.sendToClient(ws, {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// Server-specific utility functions
|
// Server-specific utility functions
|
||||||
|
export const EMPTY32BYTES = String('').padStart(64, '0');
|
||||||
|
|
||||||
export function isValid32ByteHex(value: string): boolean {
|
export function isValid32ByteHex(value: string): boolean {
|
||||||
// Check if the value is a valid 32-byte hex string (64 characters)
|
// Check if the value is a valid 32-byte hex string (64 characters)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user