import { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { QRCodeSVG } from 'qrcode.react'; import styles from './Session.module.css'; import { finalizeTransaction, broadcastTransaction, type PreparedTransaction, } from '../utils/transaction'; // 从 sessionStorage 获取的交易信息 interface TransactionInfo { preparedTx: PreparedTransaction; to: string; amount: string; from: string; walletName: string; } interface Participant { partyId: string; name: string; status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed'; } interface SessionState { sessionId: string; walletName: string; messageHash: string; inviteCode?: string; threshold: { t: number; n: number }; status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed'; participants: Participant[]; currentRound: number; totalRounds: number; signature?: string; error?: string; } export default function CoSignSession() { const { sessionId } = useParams<{ sessionId: string }>(); const navigate = useNavigate(); const [session, setSession] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // 交易广播相关状态 const [txInfo, setTxInfo] = useState(null); const [broadcastStep, setBroadcastStep] = useState<'idle' | 'broadcasting' | 'success' | 'error'>('idle'); const [txHash, setTxHash] = useState(null); const [broadcastError, setBroadcastError] = useState(null); const fetchSessionStatus = useCallback(async () => { if (!sessionId) return; try { const result = await window.electronAPI.cosign.getSessionStatus(sessionId); if (result.success && result.session) { setSession({ sessionId: result.session.sessionId || sessionId, walletName: result.session.walletName || '', messageHash: result.session.messageHash || '', inviteCode: result.session.inviteCode || '', threshold: result.session.threshold || { t: 0, n: 0 }, status: mapStatus(result.session.status), participants: (result.session.participants || []).map(p => ({ ...p, status: mapParticipantStatus(p.status), })), currentRound: 0, totalRounds: 9, // GG20 签名有 9 轮 }); } else { setError(result.error || '获取会话状态失败'); } } catch (err) { setError('获取会话状态失败'); } finally { setIsLoading(false); } }, [sessionId]); // 映射参与者状态 const mapParticipantStatus = (status: string): Participant['status'] => { switch (status) { case 'waiting': case 'pending': return 'waiting'; case 'ready': case 'joined': return 'ready'; case 'processing': case 'signing': return 'processing'; case 'completed': return 'completed'; case 'failed': return 'failed'; default: return 'waiting'; } }; // 映射后端状态到前端状态 const mapStatus = (status: string): 'waiting' | 'ready' | 'processing' | 'completed' | 'failed' => { switch (status) { case 'pending': case 'waiting': return 'waiting'; case 'all_joined': case 'ready': return 'ready'; case 'in_progress': case 'signing': return 'processing'; case 'completed': return 'completed'; case 'failed': case 'expired': return 'failed'; default: return 'waiting'; } }; // 加载交易信息 useEffect(() => { if (sessionId) { const storedTxInfo = sessionStorage.getItem(`tx_${sessionId}`); if (storedTxInfo) { try { const parsed = JSON.parse(storedTxInfo); // 恢复 bigint 类型 (Legacy 交易使用 gasPrice) if (parsed.preparedTx) { parsed.preparedTx.gasLimit = BigInt(parsed.preparedTx.gasLimit); parsed.preparedTx.gasPrice = BigInt(parsed.preparedTx.gasPrice); parsed.preparedTx.value = BigInt(parsed.preparedTx.value); } setTxInfo(parsed); } catch (err) { console.error('Failed to parse transaction info:', err); } } } }, [sessionId]); useEffect(() => { fetchSessionStatus(); // 订阅会话事件 const unsubscribe = window.electronAPI.cosign.subscribeSessionEvents( sessionId!, (event: any) => { console.log('[CoSignSession] Received event:', event); if (event.type === 'participant_joined') { setSession(prev => prev ? { ...prev, participants: event.participant ? [...prev.participants, event.participant] : prev.participants, } : null); // 刷新状态 fetchSessionStatus(); } else if (event.type === 'status_changed' || event.type === 'all_joined') { setSession(prev => prev ? { ...prev, status: event.status ? mapStatus(event.status) : prev.status, } : null); // 刷新状态 fetchSessionStatus(); } else if (event.type === 'progress') { setSession(prev => prev ? { ...prev, status: 'processing', currentRound: event.round || prev.currentRound, totalRounds: event.totalRounds || prev.totalRounds, } : null); } else if (event.type === 'completed') { setSession(prev => prev ? { ...prev, status: 'completed', signature: event.signature, } : null); } else if (event.type === 'failed' || event.type === 'sign_start_timeout') { setSession(prev => prev ? { ...prev, status: 'failed', error: event.error, } : null); } } ); return () => { unsubscribe(); }; }, [sessionId, fetchSessionStatus]); const getStatusText = (status: string) => { switch (status) { case 'waiting': return '等待参与方'; case 'ready': return '准备就绪'; case 'processing': return '签名进行中'; case 'completed': return '签名完成'; case 'failed': return '签名失败'; default: return status; } }; const getStatusClass = (status: string) => { switch (status) { case 'waiting': return styles.statusWaiting; case 'ready': return styles.statusReady; case 'processing': return styles.statusProcessing; case 'completed': return styles.statusCompleted; case 'failed': return styles.statusFailed; default: return ''; } }; const getParticipantStatusIcon = (status: string) => { switch (status) { case 'waiting': return '...'; case 'ready': return 'OK'; case 'processing': return '*'; case 'completed': return 'OK'; case 'failed': return 'X'; default: return '-'; } }; const handleCopySignature = async () => { if (session?.signature) { try { await navigator.clipboard.writeText(session.signature); } catch (err) { console.error('Failed to copy:', err); } } }; // 解析签名 const parseSignature = (signatureHex: string): { r: string; s: string; v: number } | null => { try { // 签名格式: r (32 bytes) + s (32 bytes) + v (1 byte) = 65 bytes const sig = signatureHex.startsWith('0x') ? signatureHex.slice(2) : signatureHex; if (sig.length !== 130) { console.error('Invalid signature length:', sig.length); return null; } const r = sig.slice(0, 64); const s = sig.slice(64, 128); const v = parseInt(sig.slice(128, 130), 16); // EIP-1559 recovery id is 0 or 1 const recoveryId = v >= 27 ? v - 27 : v; return { r, s, v: recoveryId }; } catch (err) { console.error('Failed to parse signature:', err); return null; } }; // 广播交易 const handleBroadcastTransaction = async () => { if (!session?.signature || !txInfo) return; setBroadcastStep('broadcasting'); setBroadcastError(null); try { // 解析签名 const parsedSig = parseSignature(session.signature); if (!parsedSig) { throw new Error('无法解析签名'); } // 构建最终交易 const signedTx = finalizeTransaction(txInfo.preparedTx, parsedSig); // 广播交易 const hash = await broadcastTransaction(signedTx); setTxHash(hash); setBroadcastStep('success'); // 清除 sessionStorage 中的交易信息 sessionStorage.removeItem(`tx_${sessionId}`); } catch (err) { setBroadcastError((err as Error).message); setBroadcastStep('error'); } }; // 获取区块浏览器交易 URL const getTxExplorerUrl = (hash: string): string => { // 从 transaction.ts 获取当前网络 const isTestnet = typeof window !== 'undefined' && window.localStorage?.getItem('kava_network') !== 'mainnet'; const baseUrl = isTestnet ? 'https://testnet.kavascan.com' : 'https://kavascan.com'; return `${baseUrl}/tx/${hash}`; }; if (isLoading) { return (

加载签名会话信息...

); } if (error || !session) { return (
!

加载失败

{error || '无法获取会话信息'}

); } return (

{session.walletName || '多方签名'}

会话 ID: {session.sessionId?.substring(0, 16)}...

{getStatusText(session.status)}
{/* 邀请码 - 等待状态时显示 */} {session.inviteCode && session.status === 'waiting' && (

邀请码

{/* QR Code */}
{session.inviteCode}

扫描二维码或分享邀请码给其他参与方加入签名

)} {/* 消息哈希 */} {session.messageHash && (

待签名消息

{session.messageHash}
)} {/* 进度部分 */} {session.status === 'processing' && (
签名进度 {session.currentRound} / {session.totalRounds}
)} {/* 参与方列表 */}

参与方 ({(session.participants || []).length} / {session.threshold?.t || 0})

{(session.participants || []).map((participant, index) => (
#{index + 1} {participant.name || `参与方 ${index + 1}`}
{getParticipantStatusIcon(participant.status)}
))} {Array.from({ length: Math.max(0, (session.threshold?.t || 0) - (session.participants || []).length) }).map((_, index) => (
#{(session.participants || []).length + index + 1} 等待加入...
...
))}
{/* 阈值信息 */} {session.threshold && (

签名阈值

{session.threshold.t}-of-{session.threshold.n} 需要 {session.threshold.t} 个参与方共同签名
)} {/* 完成状态 */} {session.status === 'completed' && session.signature && (

签名结果

{session.signature}

OK 签名已成功生成

{/* 交易广播部分 */} {txInfo && (

交易详情

收款地址 {txInfo.to.slice(0, 10)}...{txInfo.to.slice(-8)}
转账金额 {txInfo.amount} KAVA
{broadcastStep === 'idle' && ( )} {broadcastStep === 'broadcasting' && (

正在广播交易...

)} {broadcastStep === 'success' && txHash && (
OK

交易已成功广播!

{txHash}
在区块浏览器查看
)} {broadcastStep === 'error' && (
!

广播失败: {broadcastError}

)}
)}
)} {/* 失败状态 */} {session.status === 'failed' && session.error && (
! {session.error}
)}
); }