Functional pairing
This commit is contained in:
parent
7c4e81331b
commit
afe12860cf
@ -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';
|
@ -37,4 +37,24 @@ export enum MessageType {
|
||||
// Account management
|
||||
ADD_DEVICE = 'ADD_DEVICE',
|
||||
DEVICE_ADDED = 'DEVICE_ADDED',
|
||||
}
|
||||
|
||||
// Re-export AnkFlag from WASM for relay message typing
|
||||
export { AnkFlag } from '../pkg/sdk_client';
|
||||
|
||||
// Message priority levels
|
||||
export enum MessagePriority {
|
||||
LOW = 0,
|
||||
NORMAL = 1,
|
||||
HIGH = 2,
|
||||
CRITICAL = 3,
|
||||
}
|
||||
|
||||
// Message delivery status
|
||||
export enum DeliveryStatus {
|
||||
PENDING = 'PENDING',
|
||||
SENT = 'SENT',
|
||||
DELIVERED = 'DELIVERED',
|
||||
FAILED = 'FAILED',
|
||||
RETRY = 'RETRY',
|
||||
}
|
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 * 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');
|
||||
}
|
||||
const result = wasm.create_update_message(process, stateId, this.membersList);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`WASM error: ${error}`);
|
||||
throw new Error(`Failed to create update message: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,34 +616,15 @@ export class Service {
|
||||
console.log(`✅ Approving change for process ${processId}, state ${stateId}`);
|
||||
|
||||
try {
|
||||
// Get the process from cache
|
||||
const process = this.processes.get(processId);
|
||||
if (!process) {
|
||||
throw new Error('Process not found');
|
||||
}
|
||||
|
||||
// Find the state
|
||||
const state = process.states.find((s: any) => s.state_id === stateId);
|
||||
if (!state) {
|
||||
throw new Error('State not found');
|
||||
}
|
||||
|
||||
// Use WASM function to validate state
|
||||
const result = wasm.validate_state(process, stateId, this.membersList);
|
||||
|
||||
if (result.updated_process) {
|
||||
// Update our cache
|
||||
this.processes.set(processId, result.updated_process.current_process);
|
||||
|
||||
// Save to database
|
||||
await this.saveProcessToDb(processId, result.updated_process.current_process);
|
||||
|
||||
return result;
|
||||
} else {
|
||||
throw new Error('Failed to validate state');
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`WASM error: ${error}`);
|
||||
throw new Error(`Failed to validate state: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,8 +759,66 @@ export class Service {
|
||||
}
|
||||
}
|
||||
|
||||
// Utility method: Check if device is paired
|
||||
isPaired(): boolean {
|
||||
public async getDeviceFromDatabase(): Promise<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;
|
||||
}
|
||||
|
||||
// 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
|
||||
public getLastCommitedStateIndex(process: Process): number | null {
|
||||
if (process.states.length === 0) return null;
|
||||
const processTip = process.states[process.states.length - 1].commited_in;
|
||||
for (let i = process.states.length - 1; i >= 0; i--) {
|
||||
if (process.states[i].commited_in !== processTip) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
|
||||
let us;
|
||||
try {
|
||||
us = wasm.get_pairing_process_id();
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
return this.rolesContainsMember(roles, us);
|
||||
}
|
||||
|
||||
public rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean {
|
||||
for (const roleDef of Object.values(roles)) {
|
||||
if (roleDef.members.includes(pairingProcessId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Utility method: Add member to the members list
|
||||
addMember(outpoint: string, member: any) {
|
||||
this.membersList[outpoint] = member;
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import WebSocket from 'ws';
|
||||
import { MessageType } from './models';
|
||||
import { config } from './config';
|
||||
import { Service } from './service';
|
||||
import { ApiReturn } from '../pkg/sdk_client';
|
||||
import { ApiReturn, Process } from '../pkg/sdk_client';
|
||||
import { EMPTY32BYTES } from './utils';
|
||||
import { rust_zstd_wasm_shim_calloc } from '../pkg/sdk_client_bg.wasm';
|
||||
|
||||
interface ServerMessageEvent {
|
||||
data: {
|
||||
@ -58,15 +60,10 @@ class SimpleProcessHandlers {
|
||||
throw new Error('Device not paired');
|
||||
}
|
||||
|
||||
// Create test process if it doesn't exist
|
||||
let process = await this.service.getProcess(processId);
|
||||
if (!process) {
|
||||
process = this.service.createTestProcess(processId);
|
||||
}
|
||||
|
||||
let res: ApiReturn;
|
||||
try {
|
||||
res = await this.service.createPrdUpdate(processId, stateId);
|
||||
await this.service.handleApiReturn(res);
|
||||
} catch (e) {
|
||||
throw new Error(e as string);
|
||||
}
|
||||
@ -77,7 +74,7 @@ class SimpleProcessHandlers {
|
||||
};
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to notify update for process: ${e}`;
|
||||
return this.errorResponse(errorMsg, event.clientId, event.data.messageId);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,17 +95,10 @@ class SimpleProcessHandlers {
|
||||
throw new Error('Device not paired');
|
||||
}
|
||||
|
||||
// Create test process if it doesn't exist
|
||||
let process = await this.service.getProcess(processId);
|
||||
if (!process) {
|
||||
process = this.service.createTestProcess(processId);
|
||||
}
|
||||
|
||||
// Execute actual protocol logic
|
||||
let res: ApiReturn;
|
||||
try {
|
||||
res = await this.service.approveChange(processId, stateId);
|
||||
|
||||
await this.service.handleApiReturn(res);
|
||||
} catch (e) {
|
||||
throw new Error(e as string);
|
||||
}
|
||||
@ -120,7 +110,7 @@ class SimpleProcessHandlers {
|
||||
};
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to validate process: ${e}`;
|
||||
return this.errorResponse(errorMsg, event.clientId, event.data.messageId);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,93 +119,79 @@ class SimpleProcessHandlers {
|
||||
throw new Error('Invalid message type');
|
||||
}
|
||||
|
||||
if (!this.service.isPaired()) {
|
||||
throw new Error('Device not paired');
|
||||
}
|
||||
|
||||
try {
|
||||
// privateFields is only used if newData contains new fields
|
||||
// roles can be empty meaning that roles from the last commited state are kept
|
||||
const { processId, newData, privateFields, roles, apiKey } = event.data;
|
||||
|
||||
if (!apiKey || !this.validateApiKey(apiKey)) {
|
||||
throw new Error('Invalid API key');
|
||||
}
|
||||
|
||||
// Check if device is paired
|
||||
if (!this.service.isPaired()) {
|
||||
throw new Error('Device not paired');
|
||||
}
|
||||
|
||||
// Get or create the process
|
||||
let process = await this.service.getProcess(processId);
|
||||
// Check if the new data is already in the process or if it's a new field
|
||||
const process = await this.service.getProcess(processId);
|
||||
if (!process) {
|
||||
process = this.service.createTestProcess(processId);
|
||||
throw new Error('Process not found');
|
||||
}
|
||||
|
||||
// Get the last committed state
|
||||
let lastState = this.service.getLastCommitedState(process);
|
||||
const lastState = this.service.getLastCommitedState(process);
|
||||
if (!lastState) {
|
||||
const firstState = process.states[0];
|
||||
const roles = firstState.roles;
|
||||
if (this.service.rolesContainsUs(roles)) {
|
||||
const approveChangeRes = await this.service.approveChange(processId, firstState.state_id);
|
||||
const prdUpdateRes = await this.service.createPrdUpdate(processId, firstState.state_id);
|
||||
} else {
|
||||
if (firstState.validation_tokens.length > 0) {
|
||||
const res = await this.service.createPrdUpdate(processId, firstState.state_id);
|
||||
}
|
||||
}
|
||||
// Wait a couple seconds
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
const updatedProcess = await this.service.getProcess(processId);
|
||||
if (!updatedProcess) {
|
||||
throw new Error('Failed to get updated process');
|
||||
}
|
||||
process = updatedProcess;
|
||||
lastState = this.service.getLastCommitedState(process);
|
||||
if (!lastState) {
|
||||
throw new Error('Process doesn\'t have a committed state yet');
|
||||
}
|
||||
throw new Error('Process doesn\'t have a commited state yet');
|
||||
}
|
||||
|
||||
const lastStateIndex = this.service.getLastCommitedStateIndex(process);
|
||||
if (lastStateIndex === null) {
|
||||
throw new Error('Process doesn\'t have a committed state yet');
|
||||
}
|
||||
throw new Error('Process doesn\'t have a commited state yet');
|
||||
}
|
||||
|
||||
// Split data into private and public
|
||||
const privateData: Record<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,11 +200,11 @@ class SimpleProcessHandlers {
|
||||
};
|
||||
} catch (e) {
|
||||
const errorMsg = `Failed to update process: ${e}`;
|
||||
return this.errorResponse(errorMsg, event.clientId, event.data.messageId);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
async handleMessage(event: ServerMessageEvent): Promise<ServerResponse> {
|
||||
async handleMessage(event: ServerMessageEvent): Promise<ServerResponse > {
|
||||
try {
|
||||
switch (event.data.type) {
|
||||
case MessageType.NOTIFY_UPDATE:
|
||||
@ -241,8 +217,7 @@ class SimpleProcessHandlers {
|
||||
throw new Error(`Unhandled message type: ${event.data.type}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `Error handling message: ${error}`;
|
||||
return this.errorResponse(errorMsg, event.clientId, event.data.messageId);
|
||||
return this.errorResponse(error as string, event.clientId, event.data.messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -269,6 +244,64 @@ export class Server {
|
||||
|
||||
// Setup WebSocket handlers
|
||||
this.setupWebSocketHandlers();
|
||||
|
||||
// Check if we have a device
|
||||
const hasDevice = await service.hasDevice();
|
||||
if (!hasDevice) {
|
||||
const spAddress = await service.createNewDevice();
|
||||
console.log('🔑 New device created:', spAddress);
|
||||
} else {
|
||||
console.log('🔑 Device found, restoring from database...');
|
||||
const device = await service.getDeviceFromDatabase();
|
||||
const metadata = await service.getDeviceMetadata();
|
||||
|
||||
if (device) {
|
||||
await service.restoreDeviceFromDatabase(device);
|
||||
console.log('🔑 Device restored successfully');
|
||||
if (metadata) {
|
||||
console.log(`📋 Device info: ${metadata.device_address} (created: ${metadata.created_at})`);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Failed to retrieve device from database');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we are paired
|
||||
if (!service.isPaired()) {
|
||||
console.log('🔑 Not paired, creating pairing process...');
|
||||
try {
|
||||
const pairingResult = await service.createPairingProcess('', []);
|
||||
const processId: string = pairingResult.updated_process?.process_id;
|
||||
const stateId = pairingResult.updated_process?.current_process?.states[0].state_id;
|
||||
if (!processId || !stateId) {
|
||||
throw new Error('Failed to get process id or state id');
|
||||
}
|
||||
await service.handleApiReturn(pairingResult);
|
||||
const udpateResult = await service.createPrdUpdate(processId, stateId);
|
||||
await service.handleApiReturn(udpateResult);
|
||||
const approveResult = await service.approveChange(processId, stateId);
|
||||
await service.handleApiReturn(approveResult);
|
||||
|
||||
// now pair the device
|
||||
service.pairDevice(processId, [service.getDeviceAddress()]);
|
||||
|
||||
// Update the device in the database
|
||||
const device = service.dumpDeviceFromMemory();
|
||||
if (device) {
|
||||
await service.restoreDeviceFromDatabase(device);
|
||||
} else {
|
||||
throw new Error('Failed to dump device from wasm');
|
||||
}
|
||||
|
||||
console.log('🔑 Pairing process created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create pairing process:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('🔑 Already paired with id:', service.getPairingProcessId());
|
||||
}
|
||||
|
||||
// Relays are automatically initialized in Service constructor
|
||||
|
||||
console.log(`✅ Simple server running on port ${this.wss.options.port}`);
|
||||
console.log('📋 Supported operations: UPDATE_PROCESS, NOTIFY_UPDATE, VALIDATE_STATE');
|
||||
@ -305,8 +338,7 @@ export class Server {
|
||||
};
|
||||
|
||||
const response = await this.handlers.handleMessage(serverEvent);
|
||||
this.sendToClient(ws, response);
|
||||
|
||||
this.sendToClient(ws, response);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error handling message from ${clientId}:`, error);
|
||||
this.sendToClient(ws, {
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user