2356 lines
83 KiB
TypeScript
2356 lines
83 KiB
TypeScript
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, KAVA_TESTNET_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 (开发模式)
|
||
// 注意:即使是开发模式,默认也使用真实的 TSS Handler
|
||
// 只有显式设置 USE_MOCK_TSS=true 才会使用 Mock Handler
|
||
const USE_MOCK_TSS = process.env.USE_MOCK_TSS === 'true';
|
||
|
||
// ===========================================================================
|
||
// 调试日志系统
|
||
// ===========================================================================
|
||
let debugLogEnabled = false;
|
||
|
||
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
||
type LogSource = 'main' | 'grpc' | 'tss' | 'account' | 'renderer' | 'kava';
|
||
|
||
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;
|
||
|
||
// ===========================================================================
|
||
// Co-Sign 相关状态
|
||
// ===========================================================================
|
||
|
||
// 当前正在进行的 Co-Sign 会话信息
|
||
interface ActiveCoSignSession {
|
||
sessionId: string;
|
||
partyIndex: number;
|
||
participants: Array<{ partyId: string; partyIndex: number; name: string }>;
|
||
threshold: { t: number; n: number };
|
||
walletName: string;
|
||
messageHash: string;
|
||
shareId: string;
|
||
sharePassword: string;
|
||
}
|
||
let activeCoSignSession: ActiveCoSignSession | null = null;
|
||
|
||
// Co-Sign 幂等性保护:追踪正在进行的签名会话 ID
|
||
let signInProgressSessionId: 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查并触发 keygen(在收到 all_joined 事件后调用)
|
||
// 5 分钟超时是指:所有人加入后,启动 keygen 的超时时间
|
||
// 注意:此函数应该在收到 all_joined 事件后才调用,而不是 joinSession 后立即调用
|
||
async function checkAndTriggerKeygen(sessionId: string) {
|
||
console.log('[KEYGEN] checkAndTriggerKeygen called with sessionId:', sessionId);
|
||
|
||
if (!activeKeygenSession || activeKeygenSession.sessionId !== sessionId) {
|
||
console.log('[KEYGEN] No matching active keygen session for', sessionId);
|
||
return;
|
||
}
|
||
|
||
console.log('[KEYGEN] Active session found:', {
|
||
sessionId: activeKeygenSession.sessionId,
|
||
partyIndex: activeKeygenSession.partyIndex,
|
||
threshold: activeKeygenSession.threshold,
|
||
participantCount: activeKeygenSession.participants.length,
|
||
});
|
||
|
||
// 如果 TSS 已经在运行,不重复触发
|
||
if (tssHandler?.getIsRunning()) {
|
||
console.log('[KEYGEN] TSS already running, skip check');
|
||
return;
|
||
}
|
||
|
||
const pollIntervalMs = 2000; // 2秒轮询间隔
|
||
const maxWaitMs = 5 * 60 * 1000; // 5分钟超时(所有人加入后的启动超时)
|
||
const startTime = Date.now();
|
||
|
||
console.log('[KEYGEN] Starting to poll session status...');
|
||
debugLog.info('main', `Starting to poll session status for ${sessionId}`);
|
||
|
||
while (Date.now() - startTime < maxWaitMs) {
|
||
// 检查会话是否仍然有效
|
||
if (!activeKeygenSession || activeKeygenSession.sessionId !== sessionId) {
|
||
debugLog.warn('main', 'Active keygen session changed, stopping poll');
|
||
return;
|
||
}
|
||
|
||
// 如果 TSS 已经在运行(可能被其他事件触发),停止轮询
|
||
if (tssHandler?.getIsRunning()) {
|
||
debugLog.info('main', 'TSS started running, stopping poll');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const status = await accountClient?.getSessionStatus(sessionId);
|
||
if (!status) {
|
||
debugLog.warn('main', 'Failed to get session status, will retry');
|
||
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
||
continue;
|
||
}
|
||
|
||
// 优先使用后端返回的 threshold 值
|
||
const thresholdN = status.threshold_n || status.total_parties || activeKeygenSession.threshold.n;
|
||
const thresholdT = status.threshold_t || activeKeygenSession.threshold.t;
|
||
const expectedN = thresholdN;
|
||
const currentParticipants = status.participants?.length || 0;
|
||
|
||
debugLog.debug('main', `Session ${sessionId} status: ${status.status}, participants: ${currentParticipants}/${expectedN}, threshold: ${thresholdT}-of-${thresholdN}`);
|
||
|
||
// 检查是否满足启动条件
|
||
const hasAllParticipants = currentParticipants >= expectedN;
|
||
const statusReady = status.status === 'in_progress' ||
|
||
status.status === 'all_joined' ||
|
||
status.status === 'waiting_for_keygen';
|
||
|
||
console.log('[KEYGEN] Check conditions:', {
|
||
hasAllParticipants,
|
||
statusReady,
|
||
currentParticipants,
|
||
expectedN,
|
||
status: status.status,
|
||
});
|
||
|
||
if (hasAllParticipants && statusReady) {
|
||
console.log('[KEYGEN] Conditions met! Triggering keygen...');
|
||
debugLog.info('main', `All ${expectedN} participants joined (status: ${status.status}), triggering keygen...`);
|
||
|
||
// 使用后端返回的 participants 信息(包含正确的 party_index)
|
||
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;
|
||
|
||
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,
|
||
})))}`);
|
||
}
|
||
|
||
// 更新 activeKeygenSession 的 threshold(使用后端返回的正确值)
|
||
activeKeygenSession.threshold = { t: thresholdT, n: thresholdN };
|
||
|
||
const selectedParties = activeKeygenSession.participants.map(p => p.partyId);
|
||
|
||
await handleSessionStart({
|
||
eventType: 'session_started',
|
||
sessionId: sessionId,
|
||
thresholdN: thresholdN,
|
||
thresholdT: thresholdT,
|
||
selectedParties: selectedParties,
|
||
});
|
||
|
||
return; // 成功触发,退出循环
|
||
}
|
||
|
||
// 等待下次轮询
|
||
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
||
|
||
} catch (error) {
|
||
debugLog.error('main', `Failed to check session status: ${error}, will retry`);
|
||
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
||
}
|
||
}
|
||
|
||
// 超时 - 所有人已加入但 5 分钟内未能启动 keygen
|
||
debugLog.error('main', `Timeout: failed to start keygen within 5 minutes after all_joined for session ${sessionId}`);
|
||
|
||
// 通知前端超时
|
||
if (mainWindow) {
|
||
mainWindow.webContents.send(`session:events:${sessionId}`, {
|
||
type: 'keygen_start_timeout',
|
||
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 交易服务 (从数据库读取网络设置,默认测试网)
|
||
const kavaNetwork = database.getSetting('kava_network') || 'testnet';
|
||
const kavaConfig = kavaNetwork === 'mainnet' ? KAVA_MAINNET_TX_CONFIG : KAVA_TESTNET_TX_CONFIG;
|
||
kavaTxService = new KavaTxService(kavaConfig);
|
||
debugLog.info('kava', `Kava network: ${kavaNetwork}`);
|
||
|
||
// 初始化 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[];
|
||
}) {
|
||
console.log('[KEYGEN] handleSessionStart called:', {
|
||
eventType: event.eventType,
|
||
sessionId: event.sessionId,
|
||
thresholdN: event.thresholdN,
|
||
thresholdT: event.thresholdT,
|
||
selectedParties: event.selectedParties?.length,
|
||
});
|
||
|
||
if (!activeKeygenSession) {
|
||
console.log('[KEYGEN] No active keygen session, ignoring');
|
||
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;
|
||
|
||
// 从事件中更新参与者列表(如果事件包含完整列表)
|
||
// 注意:activeKeygenSession.participants 可能已经包含正确的 partyIndex(从 checkAndTriggerKeygen 更新)
|
||
// 只有当事件来自 session_started 事件时才需要重新构建
|
||
if (event.selectedParties && event.selectedParties.length > 0) {
|
||
const myPartyId = grpcClient?.getPartyId();
|
||
const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = [];
|
||
|
||
event.selectedParties.forEach((partyId) => {
|
||
// 查找已有的参与者信息(优先使用已有的 partyIndex)
|
||
const existing = activeKeygenSession!.participants.find(p => p.partyId === partyId);
|
||
if (existing) {
|
||
// 保留已有的 partyIndex
|
||
updatedParticipants.push({
|
||
partyId: partyId,
|
||
partyIndex: existing.partyIndex,
|
||
name: existing.name || (partyId === myPartyId ? '我' : `参与方 ${existing.partyIndex + 1}`),
|
||
});
|
||
} else {
|
||
// 找不到已有信息,这不应该发生
|
||
debugLog.warn('main', `Party ${partyId} not found in existing participants, using fallback index`);
|
||
updatedParticipants.push({
|
||
partyId: partyId,
|
||
partyIndex: updatedParticipants.length,
|
||
name: partyId === myPartyId ? '我' : `参与方 ${updatedParticipants.length + 1}`,
|
||
});
|
||
}
|
||
});
|
||
|
||
// 按 partyIndex 排序
|
||
updatedParticipants.sort((a, b) => a.partyIndex - b.partyIndex);
|
||
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,
|
||
})))}`);
|
||
}
|
||
|
||
console.log('[KEYGEN] Calling tssHandler.participateKeygen with:', {
|
||
sessionId: activeKeygenSession.sessionId,
|
||
partyId: grpcClient?.getPartyId(),
|
||
partyIndex: activeKeygenSession.partyIndex,
|
||
participants: activeKeygenSession.participants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex })),
|
||
threshold: activeKeygenSession.threshold,
|
||
});
|
||
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;
|
||
}
|
||
}
|
||
|
||
// ===========================================================================
|
||
// Co-Sign 相关处理函数
|
||
// ===========================================================================
|
||
|
||
// 检查并触发 Co-Sign(在收到 all_joined 事件后调用)
|
||
async function checkAndTriggerCoSign(sessionId: string) {
|
||
console.log('[CO-SIGN] checkAndTriggerCoSign called with sessionId:', sessionId);
|
||
|
||
if (!activeCoSignSession || activeCoSignSession.sessionId !== sessionId) {
|
||
console.log('[CO-SIGN] No matching active co-sign session for', sessionId);
|
||
return;
|
||
}
|
||
|
||
console.log('[CO-SIGN] Active session found:', {
|
||
sessionId: activeCoSignSession.sessionId,
|
||
partyIndex: activeCoSignSession.partyIndex,
|
||
threshold: activeCoSignSession.threshold,
|
||
participantCount: activeCoSignSession.participants.length,
|
||
});
|
||
|
||
// 如果 TSS 已经在运行,不重复触发
|
||
if (tssHandler?.getIsRunning()) {
|
||
console.log('[CO-SIGN] TSS already running, skip check');
|
||
return;
|
||
}
|
||
|
||
const pollIntervalMs = 2000; // 2秒轮询间隔
|
||
const maxWaitMs = 5 * 60 * 1000; // 5分钟超时
|
||
const startTime = Date.now();
|
||
|
||
console.log('[CO-SIGN] Starting to poll session status...');
|
||
debugLog.info('main', `Starting to poll co-sign session status for ${sessionId}`);
|
||
|
||
while (Date.now() - startTime < maxWaitMs) {
|
||
// 检查会话是否仍然有效
|
||
if (!activeCoSignSession || activeCoSignSession.sessionId !== sessionId) {
|
||
debugLog.warn('main', 'Active co-sign session changed, stopping poll');
|
||
return;
|
||
}
|
||
|
||
// 如果 TSS 已经在运行,停止轮询
|
||
if (tssHandler?.getIsRunning()) {
|
||
debugLog.info('main', 'TSS started running, stopping poll');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 获取签名会话状态
|
||
const status = await accountClient?.getSignSessionStatus(sessionId);
|
||
if (!status) {
|
||
debugLog.warn('main', 'Failed to get sign session status, will retry');
|
||
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
||
continue;
|
||
}
|
||
|
||
const expectedT = activeCoSignSession.threshold.t;
|
||
const currentParticipants = status.joined_count || 0;
|
||
|
||
debugLog.debug('main', `Sign session ${sessionId} status: ${status.status}, participants: ${currentParticipants}/${expectedT}`);
|
||
|
||
// 检查是否满足启动条件
|
||
const hasAllParticipants = currentParticipants >= expectedT;
|
||
const statusReady = status.status === 'in_progress' ||
|
||
status.status === 'all_joined' ||
|
||
status.status === 'waiting_for_sign';
|
||
|
||
console.log('[CO-SIGN] Check conditions:', {
|
||
hasAllParticipants,
|
||
statusReady,
|
||
currentParticipants,
|
||
expectedT,
|
||
status: status.status,
|
||
});
|
||
|
||
if (hasAllParticipants && statusReady) {
|
||
console.log('[CO-SIGN] Conditions met! Triggering sign...');
|
||
debugLog.info('main', `All ${expectedT} participants joined (status: ${status.status}), triggering sign...`);
|
||
|
||
// 更新参与者列表
|
||
if (status.parties && status.parties.length > 0) {
|
||
const myPartyId = grpcClient?.getPartyId();
|
||
const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = [];
|
||
|
||
for (const p of status.parties) {
|
||
const existing = activeCoSignSession.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}`),
|
||
});
|
||
}
|
||
|
||
activeCoSignSession.participants = updatedParticipants;
|
||
|
||
const myInfo = updatedParticipants.find(p => p.partyId === myPartyId);
|
||
if (myInfo) {
|
||
activeCoSignSession.partyIndex = myInfo.partyIndex;
|
||
}
|
||
}
|
||
|
||
const selectedParties = activeCoSignSession.participants.map(p => p.partyId);
|
||
|
||
await handleCoSignStart({
|
||
eventType: 'session_started',
|
||
sessionId: sessionId,
|
||
thresholdT: activeCoSignSession.threshold.t,
|
||
thresholdN: activeCoSignSession.threshold.n,
|
||
selectedParties: selectedParties,
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
||
|
||
} catch (error) {
|
||
debugLog.error('main', `Failed to check sign session status: ${error}, will retry`);
|
||
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
||
}
|
||
}
|
||
|
||
// 超时
|
||
debugLog.error('main', `Timeout: failed to start sign within 5 minutes for session ${sessionId}`);
|
||
|
||
if (mainWindow) {
|
||
mainWindow.webContents.send(`cosign:events:${sessionId}`, {
|
||
type: 'sign_start_timeout',
|
||
error: '启动签名超时,请重试',
|
||
});
|
||
}
|
||
}
|
||
|
||
// 处理 Co-Sign 会话开始事件 - 触发签名
|
||
async function handleCoSignStart(event: {
|
||
eventType: string;
|
||
sessionId: string;
|
||
thresholdT: number;
|
||
thresholdN: number;
|
||
selectedParties: string[];
|
||
}) {
|
||
console.log('[CO-SIGN] handleCoSignStart called:', {
|
||
eventType: event.eventType,
|
||
sessionId: event.sessionId,
|
||
thresholdT: event.thresholdT,
|
||
thresholdN: event.thresholdN,
|
||
selectedParties: event.selectedParties?.length,
|
||
});
|
||
|
||
if (!activeCoSignSession) {
|
||
console.log('[CO-SIGN] No active co-sign session, ignoring');
|
||
debugLog.debug('main', 'No active co-sign session, ignoring sign start event');
|
||
return;
|
||
}
|
||
|
||
if (activeCoSignSession.sessionId !== event.sessionId) {
|
||
debugLog.debug('main', `Session ID mismatch: expected ${activeCoSignSession.sessionId}, got ${event.sessionId}`);
|
||
return;
|
||
}
|
||
|
||
// 幂等性保护
|
||
if (signInProgressSessionId === event.sessionId) {
|
||
debugLog.debug('main', `Sign already in progress for session ${event.sessionId}, skipping duplicate trigger`);
|
||
return;
|
||
}
|
||
|
||
if (tssHandler?.getIsRunning()) {
|
||
debugLog.debug('main', 'TSS already running, skipping');
|
||
return;
|
||
}
|
||
|
||
if (!tssHandler || !('participateSign' in tssHandler)) {
|
||
debugLog.error('tss', 'TSS handler not initialized or does not support signing');
|
||
mainWindow?.webContents.send(`cosign:events:${event.sessionId}`, {
|
||
type: 'failed',
|
||
error: 'TSS handler not initialized',
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 标记签名开始
|
||
signInProgressSessionId = event.sessionId;
|
||
|
||
// 获取 share 数据
|
||
const share = database?.getShare(activeCoSignSession.shareId, activeCoSignSession.sharePassword);
|
||
if (!share) {
|
||
debugLog.error('main', 'Failed to get share data');
|
||
mainWindow?.webContents.send(`cosign:events:${event.sessionId}`, {
|
||
type: 'failed',
|
||
error: 'Failed to get share data',
|
||
});
|
||
signInProgressSessionId = null;
|
||
return;
|
||
}
|
||
|
||
console.log('[CO-SIGN] Calling tssHandler.participateSign with:', {
|
||
sessionId: activeCoSignSession.sessionId,
|
||
partyId: grpcClient?.getPartyId(),
|
||
partyIndex: activeCoSignSession.partyIndex,
|
||
participants: activeCoSignSession.participants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex })),
|
||
threshold: activeCoSignSession.threshold,
|
||
messageHash: activeCoSignSession.messageHash.substring(0, 16) + '...',
|
||
});
|
||
debugLog.info('tss', `Starting sign for session ${event.sessionId}...`);
|
||
|
||
try {
|
||
const result = await (tssHandler as TSSHandler).participateSign(
|
||
activeCoSignSession.sessionId,
|
||
grpcClient?.getPartyId() || '',
|
||
activeCoSignSession.partyIndex,
|
||
activeCoSignSession.participants,
|
||
activeCoSignSession.threshold,
|
||
activeCoSignSession.messageHash,
|
||
share.raw_share,
|
||
activeCoSignSession.sharePassword
|
||
);
|
||
|
||
if (result.success) {
|
||
debugLog.info('tss', 'Sign completed successfully');
|
||
await handleCoSignComplete(result);
|
||
} else {
|
||
debugLog.error('tss', `Sign failed: ${result.error}`);
|
||
mainWindow?.webContents.send(`cosign:events:${activeCoSignSession.sessionId}`, {
|
||
type: 'failed',
|
||
error: result.error || 'Sign failed',
|
||
});
|
||
signInProgressSessionId = null;
|
||
}
|
||
} catch (error) {
|
||
debugLog.error('tss', `Sign error: ${(error as Error).message}`);
|
||
mainWindow?.webContents.send(`cosign:events:${activeCoSignSession?.sessionId}`, {
|
||
type: 'failed',
|
||
error: (error as Error).message,
|
||
});
|
||
signInProgressSessionId = null;
|
||
}
|
||
}
|
||
|
||
// 处理 Co-Sign 完成 - 保存签名并报告完成
|
||
async function handleCoSignComplete(result: { success: boolean; signature: Buffer; error?: string }) {
|
||
if (!activeCoSignSession || !database || !grpcClient) {
|
||
debugLog.error('main', 'Missing required components for sign completion');
|
||
return;
|
||
}
|
||
|
||
const sessionId = activeCoSignSession.sessionId;
|
||
const partyId = grpcClient.getPartyId();
|
||
|
||
try {
|
||
const signatureHex = result.signature.toString('hex');
|
||
|
||
// 1. 更新签名历史
|
||
database.updateSigningHistory(sessionId, {
|
||
status: 'completed',
|
||
signature: signatureHex,
|
||
});
|
||
|
||
debugLog.info('main', `Signature saved: ${signatureHex.substring(0, 32)}...`);
|
||
|
||
// 2. 报告完成给 session-coordinator
|
||
const allCompleted = await grpcClient.reportCompletion(
|
||
sessionId,
|
||
partyId || '',
|
||
result.signature
|
||
);
|
||
|
||
debugLog.info('grpc', `Reported sign completion to session-coordinator, all_completed: ${allCompleted}`);
|
||
|
||
// 3. 通知前端
|
||
mainWindow?.webContents.send(`cosign:events:${sessionId}`, {
|
||
type: 'completed',
|
||
signature: signatureHex,
|
||
allCompleted: allCompleted,
|
||
});
|
||
|
||
// 4. 清理活跃会话和幂等性标志
|
||
activeCoSignSession = null;
|
||
signInProgressSessionId = null;
|
||
debugLog.info('main', 'Co-Sign session completed and cleaned up');
|
||
|
||
} catch (error) {
|
||
debugLog.error('main', `Failed to handle sign completion: ${error}`);
|
||
mainWindow?.webContents.send(`cosign:events:${sessionId}`, {
|
||
type: 'failed',
|
||
error: (error as Error).message,
|
||
});
|
||
signInProgressSessionId = 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 });
|
||
});
|
||
|
||
// 监听会话事件并处理
|
||
// Note: gRPC response uses snake_case field names due to keepCase: true in proto-loader
|
||
grpcClient.on('sessionEvent', async (event: {
|
||
event_id: string;
|
||
event_type: string;
|
||
session_id: string;
|
||
threshold_n: number;
|
||
threshold_t: number;
|
||
selected_parties: string[];
|
||
join_tokens: Record<string, string>;
|
||
message_hash?: Buffer;
|
||
}) => {
|
||
debugLog.info('grpc', `Received session event: ${event.event_type} for session ${event.session_id}`);
|
||
|
||
const eventData: SessionEventData = {
|
||
type: event.event_type,
|
||
sessionId: event.session_id,
|
||
thresholdN: event.threshold_n,
|
||
thresholdT: event.threshold_t,
|
||
selectedParties: event.selected_parties,
|
||
};
|
||
|
||
// 缓存事件(解决前端可能还未订阅的时序问题)
|
||
cacheSessionEvent(event.session_id, eventData);
|
||
|
||
// 转发事件到前端
|
||
mainWindow?.webContents.send(`session:events:${event.session_id}`, eventData);
|
||
|
||
// 根据事件类型处理 - 区分 Keygen 和 Co-Sign 会话
|
||
const isCoSignSession = activeCoSignSession?.sessionId === event.session_id;
|
||
const isKeygenSession = activeKeygenSession?.sessionId === event.session_id;
|
||
|
||
if (event.event_type === 'all_joined') {
|
||
// 收到 all_joined 事件表示所有参与方都已加入
|
||
debugLog.info('main', `Received all_joined event for session ${event.session_id}, isCoSign=${isCoSignSession}, isKeygen=${isKeygenSession}`);
|
||
|
||
if (isCoSignSession) {
|
||
// Co-Sign 会话:转发到 cosign 频道并触发签名
|
||
mainWindow?.webContents.send(`cosign:events:${event.session_id}`, eventData);
|
||
setImmediate(() => {
|
||
checkAndTriggerCoSign(event.session_id);
|
||
});
|
||
} else if (isKeygenSession) {
|
||
// Keygen 会话:启动 5 分钟倒计时
|
||
setImmediate(() => {
|
||
checkAndTriggerKeygen(event.session_id);
|
||
});
|
||
}
|
||
} else if (event.event_type === 'session_started') {
|
||
// session_started 事件表示可以开始了
|
||
if (isCoSignSession) {
|
||
// Co-Sign 会话
|
||
mainWindow?.webContents.send(`cosign:events:${event.session_id}`, eventData);
|
||
await handleCoSignStart({
|
||
eventType: event.event_type,
|
||
sessionId: event.session_id,
|
||
thresholdN: event.threshold_n,
|
||
thresholdT: event.threshold_t,
|
||
selectedParties: event.selected_parties,
|
||
});
|
||
} else if (isKeygenSession) {
|
||
// Keygen 会话
|
||
await handleSessionStart({
|
||
eventType: event.event_type,
|
||
sessionId: event.session_id,
|
||
thresholdN: event.threshold_n,
|
||
thresholdT: event.threshold_t,
|
||
selectedParties: event.selected_parties,
|
||
});
|
||
}
|
||
} else if (event.event_type === 'participant_joined') {
|
||
// 参与者加入事件 - 也需要区分会话类型
|
||
if (isCoSignSession) {
|
||
mainWindow?.webContents.send(`cosign:events:${event.session_id}`, eventData);
|
||
}
|
||
}
|
||
});
|
||
} 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 会话信息
|
||
// Note: gRPC response uses snake_case field names due to keepCase: true in proto-loader
|
||
const participants: Array<{ partyId: string; partyIndex: number; name: string }> = result.other_parties?.map((p: { party_id: string; party_index: number }, idx: number) => ({
|
||
partyId: p.party_id,
|
||
partyIndex: p.party_index,
|
||
name: `参与方 ${idx + 1}`, // 暂时使用默认名称
|
||
})) || [];
|
||
|
||
// 添加自己到参与者列表
|
||
participants.push({
|
||
partyId: partyId,
|
||
partyIndex: result.party_index,
|
||
name: '我',
|
||
});
|
||
|
||
// 按 partyIndex 排序
|
||
participants.sort((a, b) => a.partyIndex - b.partyIndex);
|
||
|
||
activeKeygenSession = {
|
||
sessionId: sessionId,
|
||
partyIndex: result.party_index,
|
||
participants: participants,
|
||
threshold: {
|
||
t: result.session_info?.threshold_t || 0,
|
||
n: result.session_info?.threshold_n || 0,
|
||
},
|
||
walletName: walletName || result.session_info?.session_id || 'Shared Wallet',
|
||
encryptionPassword: '', // 不使用加密密码
|
||
};
|
||
|
||
console.log('Active keygen session set:', {
|
||
sessionId: activeKeygenSession.sessionId,
|
||
partyIndex: activeKeygenSession.partyIndex,
|
||
participantCount: activeKeygenSession.participants.length,
|
||
threshold: activeKeygenSession.threshold,
|
||
});
|
||
|
||
// 关键步骤:立即预订阅消息流
|
||
// 这确保在其他方开始发送 TSS 消息时,我们已经准备好接收和缓冲
|
||
// 即使 keygen 进程还没启动,消息也不会丢失
|
||
if (tssHandler && 'prepareForKeygen' in tssHandler) {
|
||
debugLog.info('tss', `Preparing for keygen: subscribing to messages for session ${sessionId}`);
|
||
(tssHandler as { prepareForKeygen: (sessionId: string, partyId: string) => void }).prepareForKeygen(sessionId, partyId);
|
||
}
|
||
|
||
// 方案 B: 检查 JoinSession 响应中的 session 状态
|
||
// 如果 session 已经是 in_progress,说明我们是最后一个加入的
|
||
// 此时 session_started 事件可能已经在 JoinSession 返回前到达(并被忽略)
|
||
// 所以我们需要直接触发 keygen,而不是等待 session_started 事件
|
||
//
|
||
// 注意:只检查 status === 'in_progress' 就足够了
|
||
// 因为 session 只有在所有参与者都加入后才会变成 in_progress
|
||
// 不需要额外检查参与者数量
|
||
const sessionStatus = result.session_info?.status;
|
||
|
||
debugLog.info('main', `JoinSession response: status=${sessionStatus}`);
|
||
|
||
if (sessionStatus === 'in_progress') {
|
||
// Session 已经开始,说明我们是最后一个加入的
|
||
// 直接触发 keygen,不需要等待 session_started 事件
|
||
debugLog.info('main', `Session already in_progress, triggering keygen immediately (Solution B)`);
|
||
|
||
// 使用 setImmediate 确保 activeKeygenSession 已完全设置
|
||
setImmediate(async () => {
|
||
const selectedParties = activeKeygenSession?.participants.map(p => p.partyId) || [];
|
||
await handleSessionStart({
|
||
eventType: 'session_started',
|
||
sessionId: sessionId,
|
||
thresholdN: result.session_info?.threshold_n || activeKeygenSession?.threshold.n || 0,
|
||
thresholdT: result.session_info?.threshold_t || activeKeygenSession?.threshold.t || 0,
|
||
selectedParties: selectedParties,
|
||
});
|
||
});
|
||
} else {
|
||
// Session 还未开始,等待 session_started 事件
|
||
debugLog.info('main', `Joined session ${sessionId}, waiting for session_started event to start keygen`);
|
||
}
|
||
}
|
||
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: '请先连接到消息路由器' };
|
||
}
|
||
|
||
// 动态计算 server-party 数量: persistent = n - t
|
||
// 例如: 2-of-3 -> persistent=1, 3-of-5 -> persistent=2, 4-of-7 -> persistent=3
|
||
// 这样平台备份方数量等于"允许丢失的份额数",确保用户丢失密钥后仍可恢复
|
||
const persistentCount = params.thresholdN - params.thresholdT;
|
||
const externalCount = params.thresholdT; // 用户持有的份额数量
|
||
|
||
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: persistentCount,
|
||
external_count: externalCount,
|
||
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 会话信息
|
||
// Note: gRPC response uses snake_case field names due to keepCase: true in proto-loader
|
||
const participants: Array<{ partyId: string; partyIndex: number; name: string }> = [];
|
||
|
||
// 添加发起方(自己)
|
||
participants.push({
|
||
partyId: partyId,
|
||
partyIndex: joinResult.party_index,
|
||
name: params.initiatorName || '发起者',
|
||
});
|
||
|
||
activeKeygenSession = {
|
||
sessionId: result.session_id,
|
||
partyIndex: joinResult.party_index,
|
||
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,
|
||
});
|
||
|
||
// 关键步骤:立即预订阅消息流
|
||
if (tssHandler && 'prepareForKeygen' in tssHandler) {
|
||
debugLog.info('tss', `Initiator preparing for keygen: subscribing to messages for session ${result.session_id}`);
|
||
(tssHandler as { prepareForKeygen: (sessionId: string, partyId: string) => void }).prepareForKeygen(result.session_id, partyId);
|
||
}
|
||
|
||
// 方案 B: 检查 JoinSession 响应中的 session 状态
|
||
// 发起方通常是第一个加入的,所以 session 不太可能已经是 in_progress
|
||
// 但为了代码一致性,这里也添加检查
|
||
const sessionStatus = joinResult.session_info?.status;
|
||
|
||
debugLog.info('main', `Initiator JoinSession response: status=${sessionStatus}`);
|
||
|
||
if (sessionStatus === 'in_progress') {
|
||
debugLog.info('main', `Session already in_progress, triggering keygen immediately (Solution B)`);
|
||
|
||
setImmediate(async () => {
|
||
const selectedParties = activeKeygenSession?.participants.map(p => p.partyId) || [];
|
||
await handleSessionStart({
|
||
eventType: 'session_started',
|
||
sessionId: result.session_id,
|
||
thresholdN: joinResult.session_info?.threshold_n || params.thresholdN,
|
||
thresholdT: joinResult.session_info?.threshold_t || params.thresholdT,
|
||
selectedParties: selectedParties,
|
||
});
|
||
});
|
||
} else {
|
||
debugLog.info('main', `Initiator joined session ${result.session_id}, waiting for session_started event to start keygen`);
|
||
}
|
||
} 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);
|
||
// 优先使用后端返回的 threshold,否则从 activeKeygenSession 获取
|
||
const threshold = {
|
||
t: result?.threshold_t || activeKeygenSession?.threshold?.t || 0,
|
||
n: result?.threshold_n || result?.total_parties || activeKeygenSession?.threshold?.n || 0,
|
||
};
|
||
const participants = result?.participants?.map((p, idx) => ({
|
||
partyId: p.party_id,
|
||
partyIndex: p.party_index,
|
||
name: activeKeygenSession?.participants?.find(ap => ap.partyId === p.party_id)?.name || `参与方 ${idx + 1}`,
|
||
status: p.status,
|
||
joinedAt: new Date().toISOString(),
|
||
})) || [];
|
||
|
||
return {
|
||
success: true,
|
||
session: {
|
||
sessionId: result?.session_id,
|
||
walletName: activeKeygenSession?.walletName || '',
|
||
status: result?.status,
|
||
completedParties: result?.completed_parties,
|
||
totalParties: result?.total_parties,
|
||
sessionType: result?.session_type,
|
||
publicKey: result?.public_key,
|
||
threshold: threshold,
|
||
participants: participants,
|
||
currentRound: 0,
|
||
totalRounds: 4,
|
||
},
|
||
};
|
||
} 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';
|
||
});
|
||
|
||
// ===========================================================================
|
||
// Co-Sign 相关 IPC 处理器
|
||
// ===========================================================================
|
||
|
||
// 创建 Co-Sign 会话
|
||
ipcMain.handle('cosign:createSession', async (_event, params: {
|
||
shareId: string;
|
||
sharePassword: string;
|
||
messageHash: string;
|
||
initiatorName?: string;
|
||
}) => {
|
||
try {
|
||
// 获取当前 party ID
|
||
const partyId = grpcClient?.getPartyId();
|
||
if (!partyId) {
|
||
return { success: false, error: '请先连接到消息路由器' };
|
||
}
|
||
|
||
// 从本地获取 share 信息
|
||
const share = database?.getShare(params.shareId, params.sharePassword);
|
||
if (!share) {
|
||
return { success: false, error: 'Share 不存在或密码错误' };
|
||
}
|
||
|
||
// 解析参与者信息
|
||
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 || '发起者',
|
||
});
|
||
|
||
if (!result?.session_id) {
|
||
return { success: false, error: '创建签名会话失败: 未返回会话ID' };
|
||
}
|
||
|
||
// 创建签名历史记录
|
||
database?.createSigningHistory({
|
||
shareId: params.shareId,
|
||
sessionId: result.session_id,
|
||
messageHash: params.messageHash,
|
||
});
|
||
|
||
// 发起方自动加入会话
|
||
const joinToken = result.join_token;
|
||
if (joinToken) {
|
||
console.log('[CO-SIGN] Initiator auto-joining session...');
|
||
const joinResult = await grpcClient?.joinSession(result.session_id, partyId, joinToken);
|
||
|
||
if (joinResult?.success) {
|
||
// 设置活跃的 Co-Sign 会话信息
|
||
const signParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = [];
|
||
|
||
// 添加发起方
|
||
signParticipants.push({
|
||
partyId: partyId,
|
||
partyIndex: joinResult.party_index,
|
||
name: params.initiatorName || '发起者',
|
||
});
|
||
|
||
activeCoSignSession = {
|
||
sessionId: result.session_id,
|
||
partyIndex: joinResult.party_index,
|
||
participants: signParticipants,
|
||
threshold: {
|
||
t: share.threshold_t,
|
||
n: share.threshold_n,
|
||
},
|
||
walletName: share.wallet_name,
|
||
messageHash: params.messageHash,
|
||
shareId: params.shareId,
|
||
sharePassword: params.sharePassword,
|
||
};
|
||
|
||
console.log('[CO-SIGN] Initiator active session set:', {
|
||
sessionId: activeCoSignSession.sessionId,
|
||
partyIndex: activeCoSignSession.partyIndex,
|
||
threshold: activeCoSignSession.threshold,
|
||
});
|
||
|
||
// 预订阅消息流
|
||
if (tssHandler && 'prepareForSign' in tssHandler) {
|
||
debugLog.info('tss', `Initiator preparing for sign: subscribing to messages for session ${result.session_id}`);
|
||
(tssHandler as TSSHandler).prepareForSign(result.session_id, partyId);
|
||
}
|
||
|
||
// 检查会话状态
|
||
const sessionStatus = joinResult.session_info?.status;
|
||
debugLog.info('main', `Initiator JoinSession response: status=${sessionStatus}`);
|
||
|
||
if (sessionStatus === 'in_progress') {
|
||
debugLog.info('main', 'Session already in_progress, triggering sign immediately');
|
||
setImmediate(async () => {
|
||
const selectedParties = activeCoSignSession?.participants.map(p => p.partyId) || [];
|
||
await handleCoSignStart({
|
||
eventType: 'session_started',
|
||
sessionId: result.session_id,
|
||
thresholdT: share.threshold_t,
|
||
thresholdN: share.threshold_n,
|
||
selectedParties: selectedParties,
|
||
});
|
||
});
|
||
}
|
||
} else {
|
||
console.warn('[CO-SIGN] Initiator failed to join session');
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
sessionId: result.session_id,
|
||
inviteCode: result.invite_code,
|
||
walletName: share.wallet_name,
|
||
expiresAt: result.expires_at,
|
||
};
|
||
} catch (error) {
|
||
return { success: false, error: (error as Error).message };
|
||
}
|
||
});
|
||
|
||
// 验证 Co-Sign 邀请码
|
||
ipcMain.handle('cosign:validateInviteCode', async (_event, { code }) => {
|
||
try {
|
||
debugLog.info('account', `Validating co-sign invite code: ${code}`);
|
||
const result = await accountClient?.getSignSessionByInviteCode(code);
|
||
|
||
if (!result?.session_id) {
|
||
return { success: false, error: '无效的邀请码' };
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
sessionInfo: {
|
||
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,
|
||
},
|
||
status: result.status,
|
||
currentParticipants: result.joined_count || 0,
|
||
parties: result.parties,
|
||
},
|
||
joinToken: result.join_token,
|
||
};
|
||
} catch (error) {
|
||
debugLog.error('account', `Failed to validate co-sign invite code: ${(error as Error).message}`);
|
||
return { success: false, error: (error as Error).message };
|
||
}
|
||
});
|
||
|
||
// 加入 Co-Sign 会话
|
||
ipcMain.handle('cosign:joinSession', async (_event, params: {
|
||
sessionId: string;
|
||
shareId: string;
|
||
sharePassword: string;
|
||
joinToken: string;
|
||
walletName?: string;
|
||
messageHash: string;
|
||
threshold: { t: number; n: number };
|
||
}) => {
|
||
try {
|
||
const partyId = grpcClient?.getPartyId();
|
||
if (!partyId) {
|
||
return { success: false, error: '请先连接到消息路由器' };
|
||
}
|
||
|
||
// 验证 share
|
||
const share = database?.getShare(params.shareId, params.sharePassword);
|
||
if (!share) {
|
||
return { success: false, error: 'Share 不存在或密码错误' };
|
||
}
|
||
|
||
debugLog.info('grpc', `Joining co-sign session: sessionId=${params.sessionId}, partyId=${partyId}`);
|
||
|
||
const result = await grpcClient?.joinSession(params.sessionId, partyId, params.joinToken);
|
||
if (result?.success) {
|
||
// 设置活跃的 Co-Sign 会话
|
||
const participants: Array<{ partyId: string; partyIndex: number; name: string }> = result.other_parties?.map((p: { party_id: string; party_index: number }, idx: number) => ({
|
||
partyId: p.party_id,
|
||
partyIndex: p.party_index,
|
||
name: `参与方 ${idx + 1}`,
|
||
})) || [];
|
||
|
||
// 添加自己
|
||
participants.push({
|
||
partyId: partyId,
|
||
partyIndex: result.party_index,
|
||
name: '我',
|
||
});
|
||
|
||
// 按 partyIndex 排序
|
||
participants.sort((a, b) => a.partyIndex - b.partyIndex);
|
||
|
||
activeCoSignSession = {
|
||
sessionId: params.sessionId,
|
||
partyIndex: result.party_index,
|
||
participants: participants,
|
||
threshold: params.threshold,
|
||
walletName: params.walletName || share.wallet_name,
|
||
messageHash: params.messageHash,
|
||
shareId: params.shareId,
|
||
sharePassword: params.sharePassword,
|
||
};
|
||
|
||
console.log('[CO-SIGN] Active session set:', {
|
||
sessionId: activeCoSignSession.sessionId,
|
||
partyIndex: activeCoSignSession.partyIndex,
|
||
participantCount: activeCoSignSession.participants.length,
|
||
threshold: activeCoSignSession.threshold,
|
||
});
|
||
|
||
// 创建签名历史记录
|
||
database?.createSigningHistory({
|
||
shareId: params.shareId,
|
||
sessionId: params.sessionId,
|
||
messageHash: params.messageHash,
|
||
});
|
||
|
||
// 预订阅消息流
|
||
if (tssHandler && 'prepareForSign' in tssHandler) {
|
||
debugLog.info('tss', `Preparing for sign: subscribing to messages for session ${params.sessionId}`);
|
||
(tssHandler as TSSHandler).prepareForSign(params.sessionId, partyId);
|
||
}
|
||
|
||
// 检查会话状态
|
||
const sessionStatus = result.session_info?.status;
|
||
debugLog.info('main', `JoinSession response: status=${sessionStatus}`);
|
||
|
||
if (sessionStatus === 'in_progress') {
|
||
debugLog.info('main', 'Session already in_progress, triggering sign immediately');
|
||
setImmediate(async () => {
|
||
const selectedParties = activeCoSignSession?.participants.map(p => p.partyId) || [];
|
||
await handleCoSignStart({
|
||
eventType: 'session_started',
|
||
sessionId: params.sessionId,
|
||
thresholdT: params.threshold.t,
|
||
thresholdN: params.threshold.n,
|
||
selectedParties: selectedParties,
|
||
});
|
||
});
|
||
}
|
||
|
||
return { success: true, data: result };
|
||
} else {
|
||
return { success: false, error: '加入会话失败' };
|
||
}
|
||
} catch (error) {
|
||
return { success: false, error: (error as Error).message };
|
||
}
|
||
});
|
||
|
||
// 获取 Co-Sign 会话状态
|
||
ipcMain.handle('cosign:getSessionStatus', async (_event, { sessionId }) => {
|
||
try {
|
||
const result = await accountClient?.getSignSessionStatus(sessionId);
|
||
return {
|
||
success: true,
|
||
session: {
|
||
sessionId: result?.session_id,
|
||
status: result?.status,
|
||
joinedCount: result?.joined_count,
|
||
threshold: {
|
||
t: activeCoSignSession?.threshold?.t || 0,
|
||
n: activeCoSignSession?.threshold?.n || 0,
|
||
},
|
||
participants: result?.parties?.map((p: { party_id: string; party_index: number }, idx: number) => ({
|
||
partyId: p.party_id,
|
||
partyIndex: p.party_index,
|
||
name: activeCoSignSession?.participants?.find(ap => ap.partyId === p.party_id)?.name || `参与方 ${idx + 1}`,
|
||
status: 'ready',
|
||
})) || [],
|
||
messageHash: activeCoSignSession?.messageHash || '',
|
||
walletName: activeCoSignSession?.walletName || '',
|
||
},
|
||
};
|
||
} catch (error) {
|
||
return { success: false, error: (error as Error).message };
|
||
}
|
||
});
|
||
|
||
// 订阅 Co-Sign 会话事件
|
||
ipcMain.on('cosign:subscribeSessionEvents', (_event, { sessionId }) => {
|
||
debugLog.debug('main', `Frontend subscribing to co-sign session events: ${sessionId}`);
|
||
|
||
// 获取并发送缓存的事件
|
||
const cachedEvents = getAndClearCachedEvents(sessionId);
|
||
if (cachedEvents.length > 0) {
|
||
debugLog.info('main', `Sending ${cachedEvents.length} cached events to frontend for co-sign session ${sessionId}`);
|
||
for (const event of cachedEvents) {
|
||
mainWindow?.webContents.send(`cosign:events:${sessionId}`, event);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 取消订阅 Co-Sign 会话事件
|
||
ipcMain.on('cosign:unsubscribeSessionEvents', (_event, { sessionId }) => {
|
||
debugLog.debug('main', `Frontend unsubscribing from co-sign session events: ${sessionId}`);
|
||
});
|
||
|
||
// ===========================================================================
|
||
// 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,
|
||
metadata: {
|
||
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 };
|
||
}
|
||
});
|
||
|
||
// 切换 Kava 网络 (主网/测试网)
|
||
ipcMain.handle('kava:switchNetwork', async (_event, { network }) => {
|
||
try {
|
||
if (network === 'testnet') {
|
||
kavaTxService?.switchToTestnet();
|
||
database?.setSetting('kava_network', 'testnet');
|
||
} else {
|
||
kavaTxService?.switchToMainnet();
|
||
database?.setSetting('kava_network', 'mainnet');
|
||
}
|
||
return { success: true, network };
|
||
} catch (error) {
|
||
return { success: false, error: (error as Error).message };
|
||
}
|
||
});
|
||
|
||
// 获取当前 Kava 网络
|
||
ipcMain.handle('kava:getNetwork', async () => {
|
||
const isTestnet = kavaTxService?.isTestnet() ?? false;
|
||
return { network: isTestnet ? 'testnet' : 'mainnet' };
|
||
});
|
||
|
||
// ===========================================================================
|
||
// 对话框相关
|
||
// ===========================================================================
|
||
|
||
// 选择目录
|
||
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();
|
||
});
|