import * as grpc from '@grpc/grpc-js'; import * as protoLoader from '@grpc/proto-loader'; import * as path from 'path'; import { EventEmitter } from 'events'; import { app } from 'electron'; // Proto 文件路径 - 在打包后需要从 app.asar.unpacked 或 resources 目录加载 function getProtoPath(): string { // 开发环境 if (!app.isPackaged) { return path.join(__dirname, '../../proto/message_router.proto'); } // 生产环境 - proto 文件需要解包 return path.join(process.resourcesPath, 'proto/message_router.proto'); } const PROTO_PATH = getProtoPath(); // 定义 proto 包结构类型 interface ProtoPackage { mpc?: { router?: { v1?: { MessageRouter?: grpc.ServiceClientConstructor; }; }; }; } // 加载 Proto 定义 const packageDefinition = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true, }); interface SessionInfo { sessionId: string; sessionType: string; thresholdN: number; thresholdT: number; messageHash?: Buffer; keygenSessionId?: string; } interface PartyInfo { partyId: string; partyIndex: number; } interface JoinSessionResponse { success: boolean; sessionInfo?: SessionInfo; partyIndex: number; otherParties: PartyInfo[]; } interface MPCMessage { messageId: string; sessionId: string; fromParty: string; isBroadcast: boolean; roundNumber: number; payload: Buffer; createdAt: string; } interface SessionEvent { eventId: string; eventType: string; sessionId: string; thresholdN: number; thresholdT: number; selectedParties: string[]; joinTokens: Record; messageHash?: Buffer; } /** * gRPC 客户端 - 连接到 Message Router * * 连接地址格式: * - 开发环境: localhost:50051 (不加密) * - 生产环境: mpc-grpc.szaiai.com:443 (TLS 加密) */ export class GrpcClient extends EventEmitter { private client: grpc.Client | null = null; private connected = false; private partyId: string | null = null; private heartbeatInterval: NodeJS.Timeout | null = null; private messageStream: grpc.ClientReadableStream | null = null; private eventStream: grpc.ClientReadableStream | null = null; constructor() { super(); } /** * 连接到 Message Router * @param address 完整地址,格式: host:port (例如 mpc-grpc.szaiai.com:443 或 localhost:50051) * @param useTLS 是否使用 TLS 加密 (默认: 自动检测,端口 443 使用 TLS) */ async connect(address: string, useTLS?: boolean): Promise { return new Promise((resolve, reject) => { const proto = grpc.loadPackageDefinition(packageDefinition) as ProtoPackage; const MessageRouter = proto.mpc?.router?.v1?.MessageRouter; if (!MessageRouter) { reject(new Error('Failed to load MessageRouter service definition')); return; } // 解析地址,如果没有端口则默认使用 443 let targetAddress = address; if (!address.includes(':')) { targetAddress = `${address}:443`; } // 自动检测是否使用 TLS: 端口 443 或显式指定 const port = parseInt(targetAddress.split(':')[1], 10); const shouldUseTLS = useTLS !== undefined ? useTLS : (port === 443); // 创建凭证 const credentials = shouldUseTLS ? grpc.credentials.createSsl() // TLS 加密 (生产环境) : grpc.credentials.createInsecure(); // 不加密 (开发环境) console.log(`Connecting to Message Router: ${targetAddress} (TLS: ${shouldUseTLS})`); this.client = new MessageRouter( targetAddress, credentials ) as grpc.Client; // 等待连接就绪 const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 10); (this.client as grpc.Client & { waitForReady: (deadline: Date, callback: (err?: Error) => void) => void }) .waitForReady(deadline, (err?: Error) => { if (err) { reject(err); } else { this.connected = true; resolve(); } }); }); } /** * 断开连接 */ disconnect(): void { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } if (this.messageStream) { this.messageStream.cancel(); this.messageStream = null; } if (this.eventStream) { this.eventStream.cancel(); this.eventStream = null; } if (this.client) { (this.client as grpc.Client & { close: () => void }).close(); this.client = null; } this.connected = false; this.partyId = null; } /** * 检查是否已连接 */ isConnected(): boolean { return this.connected; } /** * 获取当前 Party ID */ getPartyId(): string | null { return this.partyId; } /** * 注册为参与方 */ async registerParty(partyId: string, role: string): Promise { if (!this.client) { throw new Error('Not connected'); } return new Promise((resolve, reject) => { (this.client as grpc.Client & { registerParty: (req: unknown, callback: (err: Error | null, res: { success: boolean }) => void) => void }) .registerParty( { party_id: partyId, party_role: role, version: '1.0.0', }, (err: Error | null, response: { success: boolean }) => { if (err) { reject(err); } else if (!response.success) { reject(new Error('Registration failed')); } else { this.partyId = partyId; this.startHeartbeat(); resolve(); } } ); }); } /** * 开始心跳 */ private startHeartbeat(): void { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } this.heartbeatInterval = setInterval(() => { if (this.client && this.partyId) { (this.client as grpc.Client & { heartbeat: (req: unknown, callback: (err: Error | null) => void) => void }) .heartbeat( { party_id: this.partyId }, (err: Error | null) => { if (err) { console.error('Heartbeat failed:', err.message); this.emit('connectionError', err); } } ); } }, 30000); // 每 30 秒一次 } /** * 加入会话 */ async joinSession(sessionId: string, partyId: string, joinToken: string): Promise { if (!this.client) { throw new Error('Not connected'); } return new Promise((resolve, reject) => { (this.client as grpc.Client & { joinSession: (req: unknown, callback: (err: Error | null, res: JoinSessionResponse) => void) => void }) .joinSession( { session_id: sessionId, party_id: partyId, join_token: joinToken, }, (err: Error | null, response: JoinSessionResponse) => { if (err) { reject(err); } else { resolve(response); } } ); }); } /** * 订阅会话事件 */ subscribeSessionEvents(partyId: string): void { if (!this.client) { throw new Error('Not connected'); } this.eventStream = (this.client as grpc.Client & { subscribeSessionEvents: (req: unknown) => grpc.ClientReadableStream }) .subscribeSessionEvents({ party_id: partyId }); this.eventStream.on('data', (event: SessionEvent) => { this.emit('sessionEvent', event); }); this.eventStream.on('error', (err: Error) => { console.error('Session event stream error:', err.message); this.emit('streamError', err); }); this.eventStream.on('end', () => { console.log('Session event stream ended'); this.emit('streamEnd'); }); } /** * 订阅 MPC 消息 */ subscribeMessages(sessionId: string, partyId: string): void { if (!this.client) { throw new Error('Not connected'); } this.messageStream = (this.client as grpc.Client & { subscribeMessages: (req: unknown) => grpc.ClientReadableStream }) .subscribeMessages({ session_id: sessionId, party_id: partyId, }); this.messageStream.on('data', (message: MPCMessage) => { this.emit('mpcMessage', message); }); this.messageStream.on('error', (err: Error) => { console.error('Message stream error:', err.message); this.emit('messageStreamError', err); }); this.messageStream.on('end', () => { console.log('Message stream ended'); this.emit('messageStreamEnd'); }); } /** * 发送 MPC 消息 */ async routeMessage( sessionId: string, fromParty: string, toParties: string[], roundNumber: number, payload: Buffer ): Promise { if (!this.client) { throw new Error('Not connected'); } return new Promise((resolve, reject) => { (this.client as grpc.Client & { routeMessage: (req: unknown, callback: (err: Error | null, res: { message_id: string }) => void) => void }) .routeMessage( { session_id: sessionId, from_party: fromParty, to_parties: toParties, round_number: roundNumber, payload: payload, }, (err: Error | null, response: { message_id: string }) => { if (err) { reject(err); } else { resolve(response.message_id); } } ); }); } /** * 报告完成 */ async reportCompletion(sessionId: string, partyId: string, publicKey: Buffer): Promise { if (!this.client) { throw new Error('Not connected'); } return new Promise((resolve, reject) => { (this.client as grpc.Client & { reportCompletion: (req: unknown, callback: (err: Error | null, res: { all_completed: boolean }) => void) => void }) .reportCompletion( { session_id: sessionId, party_id: partyId, public_key: publicKey, }, (err: Error | null, response: { all_completed: boolean }) => { if (err) { reject(err); } else { resolve(response.all_completed); } } ); }); } }