This commit is contained in:
Sosthene 2025-07-27 08:50:13 +02:00
parent e98dbaca8d
commit d80e490263
8 changed files with 2673 additions and 99 deletions

1703
src/.service.bak.ts Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
// Main entry point for the SDK Signer server
export { Service } from './service';
export { config } from './config';
export { MessageType } from './models';
export { MessageType, AnkFlag } from './models';
export { isValid32ByteHex } from './utils';
export { RelayManager } from './relay-manager';
// Re-export the main server class
export { Server } from './simple-server';

View File

@ -38,3 +38,23 @@ export enum MessageType {
ADD_DEVICE = 'ADD_DEVICE',
DEVICE_ADDED = 'DEVICE_ADDED',
}
// Re-export AnkFlag from WASM for relay message typing
export { AnkFlag } from '../pkg/sdk_client';
// Message priority levels
export enum MessagePriority {
LOW = 0,
NORMAL = 1,
HIGH = 2,
CRITICAL = 3,
}
// Message delivery status
export enum DeliveryStatus {
PENDING = 'PENDING',
SENT = 'SENT',
DELIVERED = 'DELIVERED',
FAILED = 'FAILED',
RETRY = 'RETRY',
}

96
src/relay-example.ts Normal file
View File

@ -0,0 +1,96 @@
// Example usage of the RelayManager for outbound relay connections
import { RelayManager } from './relay-manager';
import { AnkFlag } from './models';
async function exampleRelayUsage() {
const relayManager = RelayManager.getInstance();
// 1. Connect to relays
console.log('🔗 Connecting to relays...');
// Connect to a faucet relay
await relayManager.connectToRelay(
'faucet-relay-1',
'wss://faucet-relay.example.com/ws',
'sp1qfaucetrelayaddress...'
);
// Connect to a validator relay
await relayManager.connectToRelay(
'validator-relay-1',
'wss://validator-relay.example.com/ws',
'sp1qvalidatorrelayaddress...'
);
// 2. Send messages using protocol-specific methods
console.log('📤 Sending protocol messages...');
// Send faucet message
relayManager.sendFaucetMessage('faucet_request_message');
// Send new transaction
relayManager.sendNewTxMessage('new_transaction_data');
// Send commit message
relayManager.sendCommitMessage('commit_data');
// Send cipher messages
relayManager.sendCipherMessages(['cipher1', 'cipher2']);
// 3. Send messages using generic method with AnkFlag
console.log('📤 Sending generic messages...');
relayManager.sendMessage('NewTx' as AnkFlag, 'transaction_data', 'validator-relay-1');
relayManager.sendMessage('Faucet' as AnkFlag, 'faucet_data', 'faucet-relay-1');
relayManager.sendMessage('Cipher' as AnkFlag, 'cipher_data'); // Broadcast to all
// 4. Check relay status
const stats = relayManager.getStats();
console.log('📊 Relay stats:', stats);
// 5. Get connected relays
const connectedRelays = relayManager.getConnectedRelays();
console.log('🔗 Connected relays:', connectedRelays.map(r => r.id));
// 6. Send to specific relay
const success = relayManager.sendToRelay('faucet-relay-1', 'Faucet' as AnkFlag, 'specific_message');
console.log('✅ Message sent to specific relay:', success);
// 7. Broadcast to all relays
const sentCount = relayManager.broadcastToAllRelays('Sync' as AnkFlag, 'sync_data');
console.log(`📡 Broadcasted to ${sentCount} relays`);
}
// Example of how to add relay addresses to the service
async function addRelayAddresses() {
// This would typically be done in the service configuration
// For now, we'll show the structure
const relayAddresses = {
'wss://faucet-relay.example.com/ws': 'sp1qfaucetrelayaddress...',
'wss://validator-relay.example.com/ws': 'sp1qvalidatorrelayaddress...',
'wss://network-relay.example.com/ws': 'sp1qnetworkrelayaddress...'
};
console.log('📝 Relay addresses configured:', Object.keys(relayAddresses));
}
// Example of storage API usage (separate from relay system)
async function storageExample() {
// Storage is handled via REST API, not WebSocket
console.log('💾 Storage operations would use REST API endpoints:');
console.log(' - POST /storage/push');
console.log(' - GET /storage/retrieve/{hash}');
console.log(' - DELETE /storage/delete/{hash}');
console.log(' - GET /storage/list');
}
// Run examples
if (require.main === module) {
exampleRelayUsage()
.then(() => addRelayAddresses())
.then(() => storageExample())
.catch(console.error);
}
export { exampleRelayUsage, addRelayAddresses, storageExample };

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

@ -0,0 +1,396 @@
import WebSocket from 'ws';
import { AnkFlag } from '../pkg/sdk_client';
interface RelayConnection {
id: string;
ws: WebSocket;
url: string;
spAddress: string;
isConnected: boolean;
lastHeartbeat: number;
reconnectAttempts: number;
maxReconnectAttempts: number;
}
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 constructor() {
this.startHeartbeat();
this.startQueueProcessing();
this.startReconnectMonitoring();
}
static getInstance(): RelayManager {
if (!RelayManager.instance) {
RelayManager.instance = new RelayManager();
}
return RelayManager.instance;
}
// 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
};
// 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', (data: WebSocket.Data) => {
try {
const message = JSON.parse(data.toString());
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, payload: 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,
payload,
messageId: this.generateMessageId()
};
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
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();
}
}
}
private handleProtocolMessage(relayId: string, message: any): void {
// Handle different AnkFlag responses
switch (message.flag) {
case "NewTx":
console.log(`📨 NewTx response from relay ${relayId}`);
break;
case "Commit":
console.log(`📨 Commit response from relay ${relayId}`);
break;
case "Cipher":
console.log(`📨 Cipher response from relay ${relayId}`);
break;
case "Faucet":
console.log(`📨 Faucet response from relay ${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');
}
}

View File

@ -2,14 +2,20 @@
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';
const DEFAULTAMOUNT = 1000n;
export class Service {
private static instance: Service;
private processes: Map<string, any> = new Map();
private membersList: any = {};
private relayManager: RelayManager;
private storages: string[] = []; // storage urls
private constructor() {
console.log('🔧 Service initialized');
this.relayManager = RelayManager.getInstance();
this.initWasm();
}
@ -31,6 +37,323 @@ export class Service {
return Service.instance;
}
public async connectToRelays(): Promise<void> {
const relays = this.getAllRelays();
console.log(`🔗 Connecting to ${relays.length} relays...`);
for (const relay of relays) {
try {
const success = await this.relayManager.connectToRelay(
relay.spAddress, // Use spAddress as relay ID
relay.wsurl,
relay.spAddress
);
if (success) {
console.log(`✅ Connected to relay: ${relay.spAddress}`);
} else {
console.warn(`⚠️ Failed to connect to relay: ${relay.spAddress}`);
}
} catch (error) {
console.error(`❌ Error connecting to relay ${relay.spAddress}:`, error);
}
}
}
/**
* 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,
}));
}
/**
* Add a relay address to the RelayManager.
* @param relayId - Unique identifier for the relay
* @param wsUrl - WebSocket URL of the relay
* @param spAddress - Silent Payment address of the relay
*/
public async addRelayAddress(relayId: string, wsUrl: string, spAddress: string): Promise<boolean> {
try {
const success = await this.relayManager.connectToRelay(relayId, wsUrl, spAddress);
if (success) {
console.log(`✅ Added relay: ${relayId} at ${wsUrl}`);
} else {
console.warn(`⚠️ Failed to add relay: ${relayId}`);
}
return success;
} catch (error) {
console.error(`❌ Error adding relay ${relayId}:`, error);
return false;
}
}
/**
* Remove a relay from the RelayManager.
* @param relayId - Unique identifier for the relay to remove
*/
public removeRelayAddress(relayId: string): void {
this.relayManager.disconnectFromRelay(relayId);
console.log(`🔌 Removed relay: ${relayId}`);
}
/**
* 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) {
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, "1");
}
await db.addObject({
storeName: walletStore,
object: { pre_id: '1', device },
key: null,
});
} catch (e) {
console.error(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> {
const relayAddress = this.getAllRelays()[0]['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);
}
// Core protocol method: Create PRD Update
async createPrdUpdate(processId: string, stateId: string): Promise<ApiReturn> {
console.log(`📢 Creating PRD update for process ${processId}, state ${stateId}`);
@ -234,8 +557,31 @@ 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, '1');
if (dbRes) {
return dbRes['device'];
} else {
return null;
}
} catch (e) {
throw new Error(`Failed to retrieve device from db: ${e}`);
}
}
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 isPaired(): boolean {
try {
return wasm.is_paired();
} catch (error) {
@ -244,29 +590,45 @@ export class Service {
}
}
// Utility method: Get last committed state
getLastCommitedState(process: any): any {
return process.states.find((s: any) => s.commited_in) || null;
public getLastCommitedState(process: Process): ProcessState | null {
const index = this.getLastCommitedStateIndex(process);
if (index === null) return null;
return process.states[index];
}
// Utility method: Get last committed state index
getLastCommitedStateIndex(process: any): number | null {
const index = process.states.findIndex((s: any) => s.commited_in);
return index >= 0 ? index : null;
public getLastCommitedStateIndex(process: Process): number | null {
if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in;
for (let i = process.states.length - 1; i >= 0; i--) {
if (process.states[i].commited_in !== processTip) {
return i;
}
}
return null;
}
// Utility method: Check if roles contain current user
rolesContainsUs(roles: Record<string, any>): boolean {
public rolesContainsUs(roles: Record<string, RoleDefinition>): boolean {
let us;
try {
// This would need to be implemented based on your user management
// For now, return true for testing
us = wasm.get_pairing_process_id();
} catch (e) {
throw e;
}
return this.rolesContainsMember(roles, us);
}
public rolesContainsMember(roles: Record<string, RoleDefinition>, pairingProcessId: string): boolean {
for (const roleDef of Object.values(roles)) {
if (roleDef.members.includes(pairingProcessId)) {
return true;
} catch (error) {
console.error('Error checking roles:', error);
return true; // Fallback to true for testing
}
}
return false;
}
// Utility method: Add member to the members list
addMember(outpoint: string, member: any) {
this.membersList[outpoint] = member;
@ -282,24 +644,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,7 +726,6 @@ export class Service {
return uint8Array;
}
// Main handleApiReturn method
public async handleApiReturn(apiReturn: ApiReturn) {
console.log('🔄 Handling API return:', apiReturn);

View File

@ -2,7 +2,8 @@ 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';
interface ServerMessageEvent {
data: {
@ -58,15 +59,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 +73,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 +94,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 +109,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 +118,79 @@ class SimpleProcessHandlers {
throw new Error('Invalid message type');
}
if (!this.service.isPaired()) {
throw new Error('Device not paired');
}
try {
// privateFields is only used if newData contains new fields
// roles can be empty meaning that roles from the last commited state are kept
const { processId, newData, privateFields, roles, apiKey } = event.data;
if (!apiKey || !this.validateApiKey(apiKey)) {
throw new Error('Invalid API key');
}
// Check if device is paired
if (!this.service.isPaired()) {
throw new Error('Device not paired');
}
// Get or create the process
let process = await this.service.getProcess(processId);
// Check if the new data is already in the process or if it's a new field
const process = await this.service.getProcess(processId);
if (!process) {
process = this.service.createTestProcess(processId);
throw new Error('Process not found');
}
// Get the last committed state
let lastState = this.service.getLastCommitedState(process);
const lastState = this.service.getLastCommitedState(process);
if (!lastState) {
const firstState = process.states[0];
const roles = firstState.roles;
if (this.service.rolesContainsUs(roles)) {
const approveChangeRes = await this.service.approveChange(processId, firstState.state_id);
const prdUpdateRes = await this.service.createPrdUpdate(processId, firstState.state_id);
} else {
if (firstState.validation_tokens.length > 0) {
const res = await this.service.createPrdUpdate(processId, firstState.state_id);
throw new Error('Process doesn\'t have a commited state yet');
}
}
// Wait a couple seconds
await new Promise(resolve => setTimeout(resolve, 2000));
const updatedProcess = await this.service.getProcess(processId);
if (!updatedProcess) {
throw new Error('Failed to get updated process');
}
process = updatedProcess;
lastState = this.service.getLastCommitedState(process);
if (!lastState) {
throw new Error('Process doesn\'t have a committed state yet');
}
}
const lastStateIndex = this.service.getLastCommitedStateIndex(process);
if (lastStateIndex === null) {
throw new Error('Process doesn\'t have a committed state yet');
throw new Error('Process doesn\'t have a commited state yet');
}
// Split data into private and public
const privateData: Record<string, any> = {};
const publicData: Record<string, any> = {};
for (const field of Object.keys(newData)) {
// Public data are carried along each new state
// TODO I hope that at some point we stop doing that
// So the first thing we can do is check if the new data is public data
if (lastState.public_data[field]) {
// Add it to public data
publicData[field] = newData[field];
continue;
}
// If it's not a public data, it may be either a private data update, or a new field
// If it's not a public data, it may be either a private data update, or a new field (public of private)
// Caller gave us a list of new private fields, if we see it here this is a new private field
if (privateFields.includes(field)) {
// Add it to private data
privateData[field] = newData[field];
continue;
}
// Check if field exists in previous states private data
// Now it can be an update of private data or a new public data
// We check that the field exists in previous states private data
for (let i = lastStateIndex; i >= 0; i--) {
const state = process.states[i];
if (state.pcd_commitment[field]) {
// We don't even check if it's a public field, we would have seen it in the last state
// TODO maybe that's an issue if we remove a public field at some point? That's not explicitly forbidden but not really supported yet
privateData[field] = newData[field];
break;
} else {
// This attribute was not modified in that state, we go back to the previous state
continue;
}
}
if (privateData[field]) continue;
// It's a new public field
// We've get back all the way to the first state without seeing it, it's a new public field
publicData[field] = newData[field];
}
let res: ApiReturn;
try {
res = await this.service.updateProcess(process, privateData, publicData, roles);
} catch (e) {
throw new Error(e as string);
}
// We'll let the wasm check if roles are consistent
const res = await this.service.updateProcess(process, privateData, publicData, roles);
await this.service.handleApiReturn(res);
return {
type: MessageType.PROCESS_UPDATED,
@ -224,7 +199,7 @@ class SimpleProcessHandlers {
};
} catch (e) {
const errorMsg = `Failed to update process: ${e}`;
return this.errorResponse(errorMsg, event.clientId, event.data.messageId);
throw new Error(errorMsg);
}
}
@ -241,8 +216,7 @@ class SimpleProcessHandlers {
throw new Error(`Unhandled message type: ${event.data.type}`);
}
} catch (error) {
const errorMsg = `Error handling message: ${error}`;
return this.errorResponse(errorMsg, event.clientId, event.data.messageId);
return this.errorResponse(error as string, event.clientId, event.data.messageId);
}
}
}
@ -270,6 +244,33 @@ export class Server {
// Setup WebSocket handlers
this.setupWebSocketHandlers();
// Check if we have a device
const device = await service.getDeviceFromDatabase();
if (!device) {
const spAddress = await service.createNewDevice();
console.log('🔑 New device created:', spAddress);
} else {
console.log('🔑 Device found, restoring from database...');
await service.restoreDeviceFromDatabase(device);
console.log('🔑 Device restored successfully');
}
// Check if we are paired
if (!service.isPaired()) {
console.log('🔑 Not paired, creating pairing process...');
try {
const pairingResult = await service.createPairingProcess('', []);
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());
}
// Connect to relays
await service.connectToRelays();
console.log(`✅ Simple server running on port ${this.wss.options.port}`);
console.log('📋 Supported operations: UPDATE_PROCESS, NOTIFY_UPDATE, VALIDATE_STATE');
console.log('🔑 Authentication: API key required for all operations');
@ -306,7 +307,6 @@ export class Server {
const response = await this.handlers.handleMessage(serverEvent);
this.sendToClient(ws, response);
} catch (error) {
console.error(`❌ Error handling message from ${clientId}:`, error);
this.sendToClient(ws, {

View File

@ -1,4 +1,5 @@
// Server-specific utility functions
export const EMPTY32BYTES = String('').padStart(64, '0');
export function isValid32ByteHex(value: string): boolean {
// Check if the value is a valid 32-byte hex string (64 characters)