rwadurian/backend/mpc-system/services/service-party-app/electron/main.ts

1483 lines
49 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { app, BrowserWindow, ipcMain, shell, dialog } from 'electron';
import * as path from 'path';
import * as fs from 'fs';
import * as crypto from 'crypto';
import express from 'express';
import { GrpcClient } from './modules/grpc-client';
import { DatabaseManager } from './modules/database';
import { addressDerivationService, CHAIN_CONFIGS } from './modules/address-derivation';
import { KavaTxService, KAVA_MAINNET_TX_CONFIG } from './modules/kava-tx-service';
import { AccountClient } from './modules/account-client';
import { TSSHandler, MockTSSHandler, KeygenResult } from './modules/tss-handler';
// 内置 HTTP 服务器端口
const HTTP_PORT = 3456;
// 是否使用 Mock TSS Handler (开发模式)
const USE_MOCK_TSS = process.env.USE_MOCK_TSS === 'true' || process.env.NODE_ENV === 'development';
// ===========================================================================
// 调试日志系统
// ===========================================================================
let debugLogEnabled = false;
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
type LogSource = 'main' | 'grpc' | 'tss' | 'account' | 'renderer';
function sendDebugLog(level: LogLevel, source: LogSource, message: string) {
if (debugLogEnabled && mainWindow) {
mainWindow.webContents.send('debug:log', { level, source, message });
}
// 同时输出到控制台
const prefix = `[${source.toUpperCase()}]`;
switch (level) {
case 'error':
console.error(prefix, message);
break;
case 'warn':
console.warn(prefix, message);
break;
case 'debug':
console.debug(prefix, message);
break;
default:
console.log(prefix, message);
}
}
// 创建日志辅助函数
const debugLog = {
info: (source: LogSource, message: string) => sendDebugLog('info', source, message),
warn: (source: LogSource, message: string) => sendDebugLog('warn', source, message),
error: (source: LogSource, message: string) => sendDebugLog('error', source, message),
debug: (source: LogSource, message: string) => sendDebugLog('debug', source, message),
};
let mainWindow: BrowserWindow | null = null;
let grpcClient: GrpcClient | null = null;
let database: DatabaseManager | null = null;
let httpServer: ReturnType<typeof express.application.listen> | null = null;
let kavaTxService: KavaTxService | null = null;
let accountClient: AccountClient | null = null;
let tssHandler: TSSHandler | MockTSSHandler | null = null;
// 当前正在进行的 Keygen 会话信息
interface ActiveKeygenSession {
sessionId: string;
partyIndex: number;
participants: Array<{ partyId: string; partyIndex: number; name: string }>;
threshold: { t: number; n: number };
walletName: string;
encryptionPassword: string;
}
let activeKeygenSession: ActiveKeygenSession | null = null;
// Keygen 幂等性保护:追踪正在进行的 keygen 会话 ID
let keygenInProgressSessionId: string | null = null;
// 会话事件缓存 - 解决前端订阅时可能错过事件的时序问题
// 当事件到达时,前端可能还在页面导航中,尚未订阅
interface SessionEventData {
type: string;
sessionId: string;
thresholdN?: number;
thresholdT?: number;
selectedParties?: string[];
publicKey?: string;
shareId?: string;
allCompleted?: boolean;
error?: string;
}
const sessionEventCache = new Map<string, SessionEventData[]>();
const SESSION_EVENT_CACHE_MAX_AGE = 60000; // 60秒后清理缓存
const sessionEventCacheTimestamps = new Map<string, number>();
// 添加事件到缓存
function cacheSessionEvent(sessionId: string, event: SessionEventData) {
if (!sessionEventCache.has(sessionId)) {
sessionEventCache.set(sessionId, []);
}
sessionEventCache.get(sessionId)!.push(event);
sessionEventCacheTimestamps.set(sessionId, Date.now());
// 清理过期缓存
cleanExpiredEventCache();
}
// 获取并清除缓存的事件
function getAndClearCachedEvents(sessionId: string): SessionEventData[] {
const events = sessionEventCache.get(sessionId) || [];
sessionEventCache.delete(sessionId);
sessionEventCacheTimestamps.delete(sessionId);
return events;
}
// 清理过期缓存
function cleanExpiredEventCache() {
const now = Date.now();
for (const [sessionId, timestamp] of sessionEventCacheTimestamps.entries()) {
if (now - timestamp > SESSION_EVENT_CACHE_MAX_AGE) {
sessionEventCache.delete(sessionId);
sessionEventCacheTimestamps.delete(sessionId);
}
}
}
// 检查并触发 keygen100% 可靠方案:不依赖事件,直接检查状态)
async function checkAndTriggerKeygen(sessionId: string) {
if (!activeKeygenSession || activeKeygenSession.sessionId !== sessionId) {
console.log('No matching active keygen session for', sessionId);
return;
}
// 如果 TSS 已经在运行,不重复触发
if (tssHandler?.getIsRunning()) {
console.log('TSS already running, skip check');
return;
}
// 从 Account 服务获取最新会话状态
try {
const status = await accountClient?.getSessionStatus(sessionId);
if (!status) {
debugLog.warn('main', 'Failed to get session status');
return;
}
debugLog.info('main', `Session ${sessionId} status: ${status.status} (${status.completed_parties}/${status.total_parties} parties)`);
// 如果会话已经是 in_progress 或 all_joined或者参与方已满触发 keygen
// 使用 status 字段判断是否可以开始
if (status.status === 'in_progress' ||
status.status === 'all_joined' ||
status.status === 'waiting_for_keygen') {
debugLog.info('main', 'Session ready, triggering keygen...');
// 使用后端返回的 participants 信息(包含正确的 party_index
// 这解决了 co_managed_keygen 中 server-party 和 service-party-app 混合的问题
if (status.participants && status.participants.length > 0) {
const myPartyId = grpcClient?.getPartyId();
const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = [];
for (const p of status.participants) {
// 查找已有的参与者名称
const existing = activeKeygenSession.participants.find(ep => ep.partyId === p.party_id);
updatedParticipants.push({
partyId: p.party_id,
partyIndex: p.party_index,
name: existing?.name || (p.party_id === myPartyId ? '我' : `参与方 ${p.party_index + 1}`),
});
}
// 更新参与者列表
activeKeygenSession.participants = updatedParticipants;
// 更新自己的 partyIndex
const myInfo = updatedParticipants.find(p => p.partyId === myPartyId);
if (myInfo) {
activeKeygenSession.partyIndex = myInfo.partyIndex;
}
debugLog.info('main', `Updated participants from server: ${JSON.stringify(updatedParticipants.map(p => ({
partyId: p.partyId.substring(0, 8) + '...',
partyIndex: p.partyIndex,
})))}`);
}
const selectedParties = activeKeygenSession.participants.map(p => p.partyId);
await handleSessionStart({
eventType: 'session_started',
sessionId: sessionId,
thresholdN: activeKeygenSession.threshold.n,
thresholdT: activeKeygenSession.threshold.t,
selectedParties: selectedParties,
});
} else {
debugLog.debug('main', `Session not ready yet, status: ${status.status}`);
}
} catch (error) {
debugLog.error('main', `Failed to check session status: ${error}`);
}
}
// 创建主窗口
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
titleBarStyle: 'hiddenInset',
show: false,
});
// 开发模式下加载 Vite 开发服务器
if (process.env.NODE_ENV === 'development') {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
// 生产模式下加载打包后的文件
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
mainWindow.once('ready-to-show', () => {
mainWindow?.show();
});
mainWindow.on('closed', () => {
mainWindow = null;
});
// 处理外部链接
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
}
// 启动内置 HTTP 服务器
function startHttpServer() {
const expressApp = express();
expressApp.use(express.json());
expressApp.use(express.static(path.join(__dirname, '../dist')));
// API 路由
expressApp.get('/api/status', (_req, res) => {
res.json({
connected: grpcClient?.isConnected() ?? false,
partyId: grpcClient?.getPartyId() ?? null,
});
});
// 所有其他路由返回 index.html (SPA)
expressApp.get('*', (_req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html'));
});
httpServer = expressApp.listen(HTTP_PORT, '127.0.0.1', () => {
console.log(`HTTP server running at http://127.0.0.1:${HTTP_PORT}`);
});
}
// 生成或获取持久化的 partyId
function getOrCreatePartyId(db: DatabaseManager): string {
const settings = db.getAllSettings();
let partyId = settings['party_id'];
if (!partyId) {
// 生成一个新的 UUID 作为 partyId
partyId = crypto.randomUUID();
db.setSetting('party_id', partyId);
debugLog.info('main', `Generated new partyId: ${partyId}`);
} else {
debugLog.info('main', `Loaded existing partyId: ${partyId}`);
}
return partyId;
}
// 初始化服务
async function initServices() {
// 初始化数据库 (必须首先初始化)
database = new DatabaseManager();
// 等待数据库初始化完成(加载 WASM 和创建表)
await database.waitForReady();
debugLog.info('main', 'Database initialized');
// 初始化 gRPC 客户端
grpcClient = new GrpcClient();
// 清理过期的已处理消息记录(防止数据库膨胀)
database.cleanupOldProcessedMessages();
debugLog.debug('main', 'Cleaned up old processed messages');
// 初始化 TSS Handler
if (USE_MOCK_TSS) {
debugLog.info('tss', 'Using Mock TSS Handler (development mode)');
tssHandler = new MockTSSHandler(grpcClient);
} else {
debugLog.info('tss', 'Using real TSS Handler');
tssHandler = new TSSHandler(grpcClient, database);
}
// 设置 TSS 进度事件监听
tssHandler.on('progress', (progress: { round: number; totalRounds: number }) => {
debugLog.info('tss', `Keygen progress: round ${progress.round}/${progress.totalRounds}`);
// 通知前端更新进度
if (activeKeygenSession) {
mainWindow?.webContents.send(`session:events:${activeKeygenSession.sessionId}`, {
type: 'progress',
round: progress.round,
totalRounds: progress.totalRounds,
});
}
});
tssHandler.on('error', (error: Error) => {
debugLog.error('tss', `TSS error: ${error.message}`);
if (activeKeygenSession) {
mainWindow?.webContents.send(`session:events:${activeKeygenSession.sessionId}`, {
type: 'failed',
error: error.message,
});
}
});
// 初始化 Kava 交易服务
kavaTxService = new KavaTxService(KAVA_MAINNET_TX_CONFIG);
// 初始化 Account 服务 HTTP 客户端
// 从数据库读取 Account 服务 URL默认使用生产环境地址
const settings = database.getAllSettings();
const accountServiceUrl = settings['account_service_url'] || 'https://rwaapi.szaiai.com';
accountClient = new AccountClient(accountServiceUrl);
debugLog.info('account', `Account service URL: ${accountServiceUrl}`);
// 设置 IPC 处理器
setupIpcHandlers();
// 启动时自动连接并注册到 Message Router (非阻塞)
connectAndRegisterToMessageRouter().catch((err) => {
debugLog.error('grpc', `Background connection failed: ${err.message}`);
});
}
// 处理会话开始事件 - 触发 Keygen
async function handleSessionStart(event: {
eventType: string;
sessionId: string;
thresholdN: number;
thresholdT: number;
selectedParties: string[];
}) {
if (!activeKeygenSession) {
debugLog.debug('main', 'No active keygen session, ignoring session start event');
return;
}
if (activeKeygenSession.sessionId !== event.sessionId) {
debugLog.debug('main', `Session ID mismatch: expected ${activeKeygenSession.sessionId}, got ${event.sessionId}`);
return;
}
// 幂等性保护:检查是否已经在执行 keygen
if (keygenInProgressSessionId === event.sessionId) {
debugLog.debug('main', `Keygen already in progress for session ${event.sessionId}, skipping duplicate trigger`);
return;
}
// 再次检查 TSS 是否在运行(双重保护)
if (tssHandler?.getIsRunning()) {
debugLog.debug('main', 'TSS already running, skipping');
return;
}
if (!tssHandler) {
debugLog.error('tss', 'TSS handler not initialized');
mainWindow?.webContents.send(`session:events:${event.sessionId}`, {
type: 'failed',
error: 'TSS handler not initialized',
});
return;
}
// 标记 keygen 开始
keygenInProgressSessionId = event.sessionId;
// 从事件中更新参与者列表(如果事件包含完整列表)
if (event.selectedParties && event.selectedParties.length > 0) {
const myPartyId = grpcClient?.getPartyId();
const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = [];
event.selectedParties.forEach((partyId, index) => {
// 查找已有的参与者信息
const existing = activeKeygenSession!.participants.find(p => p.partyId === partyId);
updatedParticipants.push({
partyId: partyId,
partyIndex: index,
name: existing?.name || (partyId === myPartyId ? '我' : `参与方 ${index + 1}`),
});
});
activeKeygenSession.participants = updatedParticipants;
// 更新自己的 partyIndex
const myInfo = updatedParticipants.find(p => p.partyId === myPartyId);
if (myInfo) {
activeKeygenSession.partyIndex = myInfo.partyIndex;
}
debugLog.info('main', `Updated participants: ${JSON.stringify(updatedParticipants.map(p => ({
partyId: p.partyId.substring(0, 8) + '...',
partyIndex: p.partyIndex,
name: p.name,
})))}`);
}
debugLog.info('tss', `Starting keygen for session ${event.sessionId}...`);
try {
const result = await tssHandler.participateKeygen(
activeKeygenSession.sessionId,
grpcClient?.getPartyId() || '',
activeKeygenSession.partyIndex,
activeKeygenSession.participants,
activeKeygenSession.threshold,
activeKeygenSession.encryptionPassword
);
if (result.success) {
debugLog.info('tss', 'Keygen completed successfully');
await handleKeygenComplete(result);
} else {
debugLog.error('tss', `Keygen failed: ${result.error}`);
mainWindow?.webContents.send(`session:events:${activeKeygenSession.sessionId}`, {
type: 'failed',
error: result.error || 'Keygen failed',
});
// 清除幂等性标志
keygenInProgressSessionId = null;
}
} catch (error) {
debugLog.error('tss', `Keygen error: ${(error as Error).message}`);
mainWindow?.webContents.send(`session:events:${activeKeygenSession?.sessionId}`, {
type: 'failed',
error: (error as Error).message,
});
// 清除幂等性标志
keygenInProgressSessionId = null;
}
}
// 处理 Keygen 完成 - 保存 share 并报告完成
async function handleKeygenComplete(result: KeygenResult) {
if (!activeKeygenSession || !database || !grpcClient) {
debugLog.error('main', 'Missing required components for keygen completion');
return;
}
const sessionId = activeKeygenSession.sessionId;
const partyId = grpcClient.getPartyId();
try {
// 1. 保存 share 到本地数据库
const publicKeyHex = result.publicKey.toString('hex');
// 转换 participants 格式:从 { partyId, partyIndex, name } 到 { partyId, name }
const participantsForSave = activeKeygenSession.participants.map(p => ({
partyId: p.partyId,
name: p.name,
}));
const saved = database.saveShare({
sessionId: sessionId,
walletName: activeKeygenSession.walletName,
partyId: partyId || '',
partyIndex: result.partyIndex,
thresholdT: activeKeygenSession.threshold.t,
thresholdN: activeKeygenSession.threshold.n,
publicKeyHex: publicKeyHex,
rawShare: result.encryptedShare.toString('base64'),
participants: participantsForSave,
}, activeKeygenSession.encryptionPassword);
debugLog.info('main', `Share saved to local database: ${saved?.id}`);
// 2. 报告完成给 session-coordinator
const allCompleted = await grpcClient.reportCompletion(
sessionId,
partyId || '',
result.publicKey
);
debugLog.info('grpc', `Reported completion to session-coordinator, all_completed: ${allCompleted}`);
// 3. 通知前端
mainWindow?.webContents.send(`session:events:${sessionId}`, {
type: 'completed',
publicKey: publicKeyHex,
shareId: saved?.id,
allCompleted: allCompleted,
});
// 4. 清理活跃会话和幂等性标志
activeKeygenSession = null;
keygenInProgressSessionId = null;
debugLog.info('main', 'Keygen session completed and cleaned up');
} catch (error) {
debugLog.error('main', `Failed to handle keygen completion: ${error}`);
mainWindow?.webContents.send(`session:events:${sessionId}`, {
type: 'failed',
error: (error as Error).message,
});
// 清除幂等性标志
keygenInProgressSessionId = null;
}
}
// 连接并注册到 Message Router
async function connectAndRegisterToMessageRouter() {
if (!grpcClient || !database) {
debugLog.error('grpc', 'gRPC client or database not initialized');
return;
}
try {
const settings = database.getAllSettings();
const routerUrl = settings['message_router_url'] || 'mpc-grpc.szaiai.com:443';
const partyId = getOrCreatePartyId(database);
const role = 'temporary'; // Service-Party-App 使用 temporary 角色
debugLog.info('grpc', `Connecting to Message Router: ${routerUrl}...`);
await grpcClient.connect(routerUrl);
debugLog.info('grpc', 'Connected to Message Router');
debugLog.info('grpc', `Registering as party: ${partyId} (role: ${role})...`);
await grpcClient.registerParty(partyId, role);
debugLog.info('grpc', 'Registered to Message Router successfully');
// 订阅会话事件
grpcClient.subscribeSessionEvents(partyId);
debugLog.info('grpc', 'Subscribed to session events');
// 监听连接状态变化
grpcClient.on('disconnected', (reason: string) => {
debugLog.warn('grpc', `Disconnected from Message Router: ${reason}`);
mainWindow?.webContents.send('grpc:connectionStatus', { connected: false, reason });
});
grpcClient.on('reconnected', () => {
debugLog.info('grpc', 'Reconnected to Message Router');
mainWindow?.webContents.send('grpc:connectionStatus', { connected: true });
});
grpcClient.on('reconnectFailed', (reason: string) => {
debugLog.error('grpc', `Failed to reconnect: ${reason}`);
mainWindow?.webContents.send('grpc:connectionStatus', { connected: false, error: reason });
});
// 监听会话事件并处理
grpcClient.on('sessionEvent', async (event: {
eventId: string;
eventType: string;
sessionId: string;
thresholdN: number;
thresholdT: number;
selectedParties: string[];
joinTokens: Record<string, string>;
messageHash?: Buffer;
}) => {
debugLog.info('grpc', `Received session event: ${event.eventType} for session ${event.sessionId}`);
const eventData: SessionEventData = {
type: event.eventType,
sessionId: event.sessionId,
thresholdN: event.thresholdN,
thresholdT: event.thresholdT,
selectedParties: event.selectedParties,
};
// 缓存事件(解决前端可能还未订阅的时序问题)
cacheSessionEvent(event.sessionId, eventData);
// 转发事件到前端
mainWindow?.webContents.send(`session:events:${event.sessionId}`, eventData);
// 根据事件类型处理
if (event.eventType === 'session_started' || event.eventType === 'all_joined') {
// 当会话开始或所有参与方都加入时,检查是否需要触发 keygen
await handleSessionStart(event);
}
});
} catch (error) {
console.error('Failed to connect/register to Message Router:', (error as Error).message);
// 不抛出错误,允许应用继续启动,用户可以稍后手动重试
}
}
// 设置 IPC 通信处理器
function setupIpcHandlers() {
// ===========================================================================
// gRPC 相关
// ===========================================================================
// gRPC 连接
ipcMain.handle('grpc:connect', async (_event, { url }) => {
try {
await grpcClient?.connect(url);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 注册为参与方
ipcMain.handle('grpc:register', async (_event, { partyId, role }) => {
try {
await grpcClient?.registerParty(partyId, role);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 加入会话
ipcMain.handle('grpc:joinSession', async (_event, { sessionId, partyId, joinToken, walletName }) => {
try {
debugLog.info('grpc', `Joining session: sessionId=${sessionId}, partyId=${partyId}, has_token=${!!joinToken}, token_length=${joinToken?.length || 0}`);
const result = await grpcClient?.joinSession(sessionId, partyId, joinToken);
if (result?.success) {
// 设置活跃的 keygen 会话信息
const participants: Array<{ partyId: string; partyIndex: number; name: string }> = result.otherParties?.map((p: { partyId: string; partyIndex: number }, idx: number) => ({
partyId: p.partyId,
partyIndex: p.partyIndex,
name: `参与方 ${idx + 1}`, // 暂时使用默认名称
})) || [];
// 添加自己到参与者列表
participants.push({
partyId: partyId,
partyIndex: result.partyIndex,
name: '我',
});
// 按 partyIndex 排序
participants.sort((a, b) => a.partyIndex - b.partyIndex);
activeKeygenSession = {
sessionId: sessionId,
partyIndex: result.partyIndex,
participants: participants,
threshold: {
t: result.sessionInfo?.thresholdT || 0,
n: result.sessionInfo?.thresholdN || 0,
},
walletName: walletName || result.sessionInfo?.sessionId || 'Shared Wallet',
encryptionPassword: '', // 不使用加密密码
};
console.log('Active keygen session set:', {
sessionId: activeKeygenSession.sessionId,
partyIndex: activeKeygenSession.partyIndex,
participantCount: activeKeygenSession.participants.length,
threshold: activeKeygenSession.threshold,
});
// 100% 可靠方案:延迟检查会话状态,如果所有人都已加入则触发 keygen
// 这样即使错过 session_started 事件也能正常工作
setTimeout(() => {
checkAndTriggerKeygen(sessionId);
}, 1000); // 延迟 1 秒等待可能的事件到达
}
return { success: true, data: result };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// gRPC - 创建会话 (通过 Account 服务 HTTP API)
ipcMain.handle('grpc:createSession', async (_event, params) => {
try {
// 获取当前 party ID
const partyId = grpcClient?.getPartyId();
if (!partyId) {
return { success: false, error: '请先连接到消息路由器' };
}
const result = await accountClient?.createKeygenSession({
wallet_name: params.walletName,
threshold_t: params.thresholdT,
threshold_n: params.thresholdN,
initiator_party_id: partyId,
initiator_name: params.initiatorName || '发起者',
persistent_count: 0, // 服务端 party 数量,共管钱包模式下为 0
external_count: params.thresholdN, // 所有参与方都是外部 party
expires_in_seconds: 86400, // 24 小时
});
if (!result?.session_id) {
return { success: false, error: '创建会话失败: 未返回会话ID' };
}
// 发起方自动加入会话
// 优先使用 join_tokens map如果没有则使用 wildcard token (join_token)
const joinToken = result.join_tokens?.[partyId] || result.join_tokens?.['*'] || result.join_token;
if (joinToken) {
console.log('Initiator auto-joining session...');
const joinResult = await grpcClient?.joinSession(result.session_id, partyId, joinToken);
if (joinResult?.success) {
// 设置活跃的 keygen 会话信息
const participants: Array<{ partyId: string; partyIndex: number; name: string }> = [];
// 添加发起方(自己)
participants.push({
partyId: partyId,
partyIndex: joinResult.partyIndex,
name: params.initiatorName || '发起者',
});
activeKeygenSession = {
sessionId: result.session_id,
partyIndex: joinResult.partyIndex,
participants: participants,
threshold: {
t: params.thresholdT,
n: params.thresholdN,
},
walletName: params.walletName,
encryptionPassword: '', // 不使用加密密码
};
console.log('Initiator active keygen session set:', {
sessionId: activeKeygenSession.sessionId,
partyIndex: activeKeygenSession.partyIndex,
participantCount: activeKeygenSession.participants.length,
threshold: activeKeygenSession.threshold,
});
// 100% 可靠方案:延迟检查会话状态,如果所有人都已加入则触发 keygen
setTimeout(() => {
checkAndTriggerKeygen(result.session_id);
}, 1000);
} else {
console.warn('Initiator failed to join session');
}
} else {
console.warn('No join token found for initiator partyId:', partyId);
}
return {
success: true,
sessionId: result?.session_id,
inviteCode: result?.invite_code,
walletName: result?.wallet_name,
expiresAt: result?.expires_at,
joinTokens: result?.join_tokens,
};
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// gRPC - 验证邀请码 (通过 Account 服务 HTTP API)
ipcMain.handle('grpc:validateInviteCode', async (_event, { code }) => {
try {
debugLog.info('account', `Validating invite code: ${code}`);
const result = await accountClient?.getSessionByInviteCode(code);
debugLog.info('account', `Got session for invite code: session_id=${result?.session_id}, has_join_token=${!!result?.join_token}, token_length=${result?.join_token?.length || 0}`);
return {
success: true,
sessionInfo: {
sessionId: result?.session_id,
walletName: result?.wallet_name,
threshold: {
t: result?.threshold_t,
n: result?.threshold_n,
},
status: result?.status,
currentParticipants: result?.completed_parties || result?.joined_parties || 0,
totalParticipants: result?.total_parties || result?.threshold_n || 0,
},
joinToken: result?.join_token,
};
} catch (error) {
debugLog.error('account', `Failed to validate invite code: ${(error as Error).message}`);
return { success: false, error: (error as Error).message };
}
});
// gRPC - 获取会话状态 (通过 Account 服务 HTTP API)
ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId }) => {
try {
const result = await accountClient?.getSessionStatus(sessionId);
return {
success: true,
session: {
sessionId: result?.session_id,
status: result?.status,
completedParties: result?.completed_parties,
totalParties: result?.total_parties,
sessionType: result?.session_type,
publicKey: result?.public_key,
},
};
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// gRPC - 测试连接
ipcMain.handle('grpc:testConnection', async (_event, { url }) => {
try {
// 地址格式: host:port (例如 mpc-grpc.szaiai.com:443)
await grpcClient?.connect(url);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// gRPC - 获取已注册的参与方列表
ipcMain.handle('grpc:getRegisteredParties', async (_event, { roleFilter, onlyOnline }) => {
try {
const parties = await grpcClient?.getRegisteredParties(roleFilter, onlyOnline);
return { success: true, parties: parties || [] };
} catch (error) {
return { success: false, error: (error as Error).message, parties: [] };
}
});
// gRPC - 获取当前 partyId
ipcMain.handle('grpc:getPartyId', async () => {
try {
const partyId = grpcClient?.getPartyId();
return { success: true, partyId: partyId || null };
} catch (error) {
return { success: false, error: (error as Error).message, partyId: null };
}
});
// gRPC - 检查连接状态
ipcMain.handle('grpc:isConnected', async () => {
try {
const connected = grpcClient?.isConnected() || false;
return { success: true, connected };
} catch (error) {
return { success: false, error: (error as Error).message, connected: false };
}
});
// gRPC - 验证签名会话 (通过 Account 服务 HTTP API)
ipcMain.handle('grpc:validateSigningSession', async (_event, { code }) => {
try {
const result = await accountClient?.getSignSessionByInviteCode(code);
return {
success: true,
session: {
sessionId: result?.session_id,
keygenSessionId: result?.keygen_session_id,
walletName: result?.wallet_name,
messageHash: result?.message_hash,
threshold: {
t: result?.threshold_t,
n: result?.parties?.length || 0,
},
currentParticipants: result?.joined_count || 0,
status: result?.status,
parties: result?.parties,
},
};
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// gRPC - 加入签名会话
ipcMain.handle('grpc:joinSigningSession', async (_event, params) => {
try {
// 从本地 SQLite 获取 share 数据
const share = database?.getShare(params.shareId, params.password);
if (!share) {
return { success: false, error: 'Share not found or incorrect password' };
}
// 加入签名会话 (通过 gRPC)
// TODO: 实际加入会话逻辑需要使用 gRPC client
// 这里先返回成功,表示验证通过
return {
success: true,
partyId: share.party_id,
partyIndex: share.party_index,
};
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// ===========================================================================
// Account 服务相关 (HTTP API)
// ===========================================================================
// 创建签名会话
ipcMain.handle('account:createSignSession', async (_event, params) => {
try {
// 从本地 SQLite 获取 share 数据
const share = database?.getShare(params.shareId, params.password);
if (!share) {
return { success: false, error: 'Share not found or incorrect password' };
}
// 解析 participants_json 获取参与方列表
const participants = JSON.parse(share.participants_json || '[]');
const parties = participants.map((p: { partyId: string }, index: number) => ({
party_id: p.partyId,
party_index: index,
}));
const result = await accountClient?.createSignSession({
keygen_session_id: share.session_id,
wallet_name: share.wallet_name,
message_hash: params.messageHash,
parties: parties,
threshold_t: share.threshold_t,
initiator_name: params.initiatorName,
});
return {
success: true,
sessionId: result?.session_id,
inviteCode: result?.invite_code,
expiresAt: result?.expires_at,
joinToken: result?.join_token,
};
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 获取签名会话状态 (通过邀请码)
ipcMain.handle('account:getSignSessionByInviteCode', async (_event, { inviteCode }) => {
try {
const result = await accountClient?.getSignSessionByInviteCode(inviteCode);
return {
success: true,
session: {
sessionId: result?.session_id,
keygenSessionId: result?.keygen_session_id,
walletName: result?.wallet_name,
messageHash: result?.message_hash,
thresholdT: result?.threshold_t,
status: result?.status,
inviteCode: result?.invite_code,
expiresAt: result?.expires_at,
parties: result?.parties,
joinedCount: result?.joined_count,
},
};
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// Account 服务健康检查
ipcMain.handle('account:healthCheck', async () => {
try {
const result = await accountClient?.healthCheck();
return { success: true, data: result };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 测试 Account 服务连接
ipcMain.handle('account:testConnection', async () => {
try {
const connected = await accountClient?.testConnection();
return { success: connected };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 更新 Account 服务 URL
ipcMain.handle('account:updateUrl', async (_event, { url }) => {
try {
accountClient?.setBaseUrl(url);
database?.setSetting('account_service_url', url);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 获取 Account 服务 URL
ipcMain.handle('account:getUrl', async () => {
return accountClient?.getBaseUrl() || 'https://rwaapi.szaiai.com';
});
// ===========================================================================
// Share 存储相关 (SQLite)
// ===========================================================================
// 保存 share
ipcMain.handle('storage:saveShare', async (_event, { share, password }) => {
try {
const saved = database?.saveShare({
sessionId: share.sessionId,
walletName: share.walletName,
partyId: share.partyId,
partyIndex: share.partyIndex,
thresholdT: share.threshold.t,
thresholdN: share.threshold.n,
publicKeyHex: share.publicKey,
rawShare: share.rawShare,
participants: share.participants || [],
}, password);
// 自动派生 Kava 地址
if (saved && share.publicKey) {
try {
const kavaAddress = addressDerivationService.deriveAddress(share.publicKey, 'kava');
database?.saveDerivedAddress({
shareId: saved.id,
chain: 'kava',
derivationPath: kavaAddress.derivationPath,
address: kavaAddress.address,
publicKeyHex: share.publicKey,
});
} catch (err) {
console.error('Failed to derive Kava address:', err);
}
}
return { success: true, data: saved };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 获取 share 列表
ipcMain.handle('storage:listShares', async () => {
try {
const shares = database?.listShares() ?? [];
// 转换为前端期望的格式
const formatted = shares.map(share => ({
id: share.id,
sessionId: share.session_id,
walletName: share.wallet_name,
partyId: share.party_id,
partyIndex: share.party_index,
threshold: {
t: share.threshold_t,
n: share.threshold_n,
},
publicKey: share.public_key_hex,
createdAt: share.created_at,
lastUsedAt: share.last_used_at,
participants: JSON.parse(share.participants_json || '[]'),
}));
return { success: true, data: formatted };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 获取单个 share (解密)
ipcMain.handle('storage:getShare', async (_event, { id, password }) => {
try {
const share = database?.getShare(id, password);
if (!share) return null;
return {
id: share.id,
sessionId: share.session_id,
walletName: share.wallet_name,
partyId: share.party_id,
partyIndex: share.party_index,
threshold: {
t: share.threshold_t,
n: share.threshold_n,
},
publicKey: share.public_key_hex,
rawShare: share.raw_share,
createdAt: share.created_at,
lastUsedAt: share.last_used_at,
participants: JSON.parse(share.participants_json || '[]'),
};
} catch (error) {
return null;
}
});
// 删除 share
ipcMain.handle('storage:deleteShare', async (_event, { id }) => {
try {
database?.deleteShare(id);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 导出 share
ipcMain.handle('storage:exportShare', async (_event, { id, password }) => {
try {
const data = database?.exportShare(id, password);
return { success: true, data };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 导入 share
ipcMain.handle('storage:importShare', async (_event, { filePath, password }) => {
try {
const data = fs.readFileSync(filePath);
const share = database?.importShare(data, password);
return { success: true, share };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// ===========================================================================
// 地址派生相关
// ===========================================================================
// 派生地址
ipcMain.handle('address:derive', async (_event, { shareId, chain, password }) => {
try {
const share = database?.getShare(shareId, password);
if (!share) {
return { success: false, error: 'Share not found' };
}
const derived = addressDerivationService.deriveAddress(share.public_key_hex, chain);
// 保存到数据库
const saved = database?.saveDerivedAddress({
shareId,
chain,
derivationPath: derived.derivationPath,
address: derived.address,
publicKeyHex: share.public_key_hex,
});
return { success: true, data: { ...derived, id: saved?.id } };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 派生所有支持的链地址
ipcMain.handle('address:deriveAll', async (_event, { shareId, password }) => {
try {
const share = database?.getShare(shareId, password);
if (!share) {
return { success: false, error: 'Share not found' };
}
const addresses = addressDerivationService.deriveAllAddresses(share.public_key_hex);
// 保存所有地址
for (const addr of addresses) {
database?.saveDerivedAddress({
shareId,
chain: addr.chain,
derivationPath: addr.derivationPath,
address: addr.address,
publicKeyHex: share.public_key_hex,
});
}
return { success: true, data: addresses };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 获取 share 的所有派生地址
ipcMain.handle('address:list', async (_event, { shareId }) => {
try {
const addresses = database?.getAddressesByShare(shareId) ?? [];
return {
success: true,
data: addresses.map(addr => ({
id: addr.id,
shareId: addr.share_id,
chain: addr.chain,
chainName: CHAIN_CONFIGS[addr.chain]?.name || addr.chain,
derivationPath: addr.derivation_path,
address: addr.address,
publicKeyHex: addr.public_key_hex,
createdAt: addr.created_at,
})),
};
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 获取支持的链列表
ipcMain.handle('address:getSupportedChains', async () => {
return addressDerivationService.getSupportedChains();
});
// ===========================================================================
// 签名历史相关
// ===========================================================================
// 获取签名历史
ipcMain.handle('signing:getHistory', async (_event, { shareId }) => {
try {
const history = database?.getSigningHistoryByShare(shareId) ?? [];
return {
success: true,
data: history.map(h => ({
id: h.id,
shareId: h.share_id,
sessionId: h.session_id,
messageHash: h.message_hash,
signature: h.signature,
status: h.status,
errorMessage: h.error_message,
createdAt: h.created_at,
completedAt: h.completed_at,
})),
};
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// ===========================================================================
// 设置相关
// ===========================================================================
// 获取设置
ipcMain.handle('storage:getSettings', async () => {
try {
const settings = database?.getAllSettings() ?? {};
return {
messageRouterUrl: settings['message_router_url'] || 'mpc-grpc.szaiai.com:443',
autoBackup: settings['auto_backup'] === 'true',
backupPath: settings['backup_path'] || '',
};
} catch (error) {
return null;
}
});
// 保存设置
ipcMain.handle('storage:saveSettings', async (_event, { settings }) => {
try {
if (settings.messageRouterUrl !== undefined) {
database?.setSetting('message_router_url', settings.messageRouterUrl);
}
if (settings.autoBackup !== undefined) {
database?.setSetting('auto_backup', settings.autoBackup ? 'true' : 'false');
}
if (settings.backupPath !== undefined) {
database?.setSetting('backup_path', settings.backupPath);
}
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// ===========================================================================
// Kava 区块链相关
// ===========================================================================
// 查询 Kava 余额
ipcMain.handle('kava:getBalance', async (_event, { address }) => {
try {
const balance = await kavaTxService?.getKavaBalance(address);
return { success: true, data: balance };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 查询所有余额
ipcMain.handle('kava:getAllBalances', async (_event, { address }) => {
try {
const balances = await kavaTxService?.getAllBalances(address);
return { success: true, data: balances };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 查询账户信息
ipcMain.handle('kava:getAccountInfo', async (_event, { address }) => {
try {
const info = await kavaTxService?.getAccountInfo(address);
return { success: true, data: info };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 构建转账交易 (待签名)
ipcMain.handle('kava:buildSendTx', async (_event, { fromAddress, toAddress, amount, publicKeyHex, memo }) => {
try {
const unsignedTx = await kavaTxService?.buildSendTx(
fromAddress,
toAddress,
amount,
publicKeyHex,
memo || ''
);
return { success: true, data: unsignedTx };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 完成签名并广播交易
ipcMain.handle('kava:completeTxAndBroadcast', async (_event, { unsignedTx, signatureHex }) => {
try {
// 完成交易签名
const signedTx = await kavaTxService?.completeTx(unsignedTx, signatureHex);
if (!signedTx) {
return { success: false, error: 'Failed to complete transaction' };
}
// 广播交易
const result = await kavaTxService?.broadcastTx(signedTx);
return { success: true, data: result };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 查询交易状态
ipcMain.handle('kava:getTxStatus', async (_event, { txHash }) => {
try {
const status = await kavaTxService?.getTxStatus(txHash);
return { success: true, data: status };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 获取 Kava 配置
ipcMain.handle('kava:getConfig', async () => {
return kavaTxService?.getConfig();
});
// 更新 Kava 配置
ipcMain.handle('kava:updateConfig', async (_event, { config }) => {
try {
kavaTxService?.updateConfig(config);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// Kava 健康检查
ipcMain.handle('kava:healthCheck', async () => {
try {
const result = await kavaTxService?.healthCheck();
return { success: result?.ok ?? false, data: result };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// ===========================================================================
// 对话框相关
// ===========================================================================
// 选择目录
ipcMain.handle('dialog:selectDirectory', async () => {
const result = await dialog.showOpenDialog(mainWindow!, {
properties: ['openDirectory'],
});
return result.canceled ? null : result.filePaths[0];
});
// 选择文件
ipcMain.handle('dialog:selectFile', async (_event, { filters }) => {
const result = await dialog.showOpenDialog(mainWindow!, {
properties: ['openFile'],
filters: filters || [],
});
return result.canceled ? null : result.filePaths[0];
});
// 保存文件
ipcMain.handle('dialog:saveFile', async (_event, { defaultPath, filters }) => {
const result = await dialog.showSaveDialog(mainWindow!, {
defaultPath,
filters: filters || [],
});
return result.canceled ? null : result.filePath;
});
// ===========================================================================
// 调试相关
// ===========================================================================
// 订阅日志
ipcMain.on('debug:subscribe', () => {
debugLogEnabled = true;
debugLog.info('main', 'Debug console connected');
});
// 取消订阅日志
ipcMain.on('debug:unsubscribe', () => {
debugLogEnabled = false;
});
// 接收来自渲染进程的日志
ipcMain.on('debug:log', (_event, { level, source, message }) => {
sendDebugLog(level as LogLevel, source as LogSource, message);
});
// ===========================================================================
// 会话事件订阅(带缓存事件发送)
// ===========================================================================
// 前端订阅会话事件时,立即发送缓存的事件
ipcMain.on('grpc:subscribeSessionEvents', (_event, { sessionId }) => {
debugLog.debug('main', `Frontend subscribing to session events: ${sessionId}`);
// 获取并发送缓存的事件
const cachedEvents = getAndClearCachedEvents(sessionId);
if (cachedEvents.length > 0) {
debugLog.info('main', `Sending ${cachedEvents.length} cached events to frontend for session ${sessionId}`);
for (const event of cachedEvents) {
mainWindow?.webContents.send(`session:events:${sessionId}`, event);
}
}
});
// 前端取消订阅
ipcMain.on('grpc:unsubscribeSessionEvents', (_event, { sessionId }) => {
debugLog.debug('main', `Frontend unsubscribing from session events: ${sessionId}`);
});
}
// 应用生命周期
app.whenReady().then(async () => {
await initServices();
// HTTP 服务器仅在开发模式下启动
if (process.env.NODE_ENV === 'development') {
startHttpServer();
}
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('before-quit', () => {
// 清理资源
grpcClient?.disconnect();
database?.close();
httpServer?.close();
kavaTxService?.disconnect();
});