rwadurian/backend/mpc-system/services/service-party-app/src/pages/CoSignJoin.tsx

411 lines
14 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 { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import styles from './Join.module.css';
interface Share {
id: string;
walletName: string;
publicKey: string;
sessionId: string;
threshold: { t: number; n: number };
}
interface SignPartyInfo {
party_id: string;
party_index: number;
}
interface SessionInfo {
sessionId: string;
keygenSessionId: string;
walletName: string;
messageHash: string;
threshold: { t: number; n: number };
status: string;
currentParticipants: number;
parties?: SignPartyInfo[];
}
interface ValidateResult {
success: boolean;
error?: string;
sessionInfo?: SessionInfo;
joinToken?: string;
}
export default function CoSignJoin() {
const { inviteCode } = useParams<{ inviteCode?: string }>();
const navigate = useNavigate();
const [code, setCode] = useState(inviteCode || '');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null);
const [joinToken, setJoinToken] = useState<string | null>(null);
const [step, setStep] = useState<'input' | 'select_share' | 'joining'>('input');
// Share 选择相关
const [shares, setShares] = useState<Share[]>([]);
const [selectedShareId, setSelectedShareId] = useState('');
const [sharePassword, setSharePassword] = useState('');
const [autoJoinAttempted, setAutoJoinAttempted] = useState(false);
// 加载本地 shares
useEffect(() => {
const loadShares = async () => {
try {
const result = await window.electronAPI.storage.listShares();
// 兼容不同返回格式
const shareList = Array.isArray(result) ? result : ((result as any)?.data || []);
console.log('[CoSignJoin] Loaded shares:', shareList.map((s: Share) => ({
id: s.id,
sessionId: s.sessionId,
walletName: s.walletName,
})));
setShares(shareList);
} catch (err) {
console.error('Failed to load shares:', err);
}
};
loadShares();
}, []);
// 如果 URL 中有邀请码,自动验证
useEffect(() => {
if (inviteCode) {
handleValidateCode(inviteCode);
}
}, [inviteCode]);
// 自动选择匹配的 share
useEffect(() => {
if (sessionInfo && shares.length > 0 && !selectedShareId) {
// 尝试找到匹配的 share基于 keygen session ID
const matchingShare = shares.find(s => s.sessionId === sessionInfo.keygenSessionId);
console.log('[CoSignJoin] Auto-select share check:', {
keygenSessionId: sessionInfo.keygenSessionId,
sharesSessionIds: shares.map(s => s.sessionId),
matchingShare: matchingShare ? { id: matchingShare.id, sessionId: matchingShare.sessionId } : null,
});
if (matchingShare) {
console.log('[CoSignJoin] Auto-selecting matching share:', matchingShare.id);
setSelectedShareId(matchingShare.id);
} else if (shares.length === 1) {
// 如果只有一个 share自动选择
console.log('[CoSignJoin] Auto-selecting only share:', shares[0].id);
setSelectedShareId(shares[0].id);
} else {
console.log('[CoSignJoin] No matching share found, user must select manually');
}
}
}, [sessionInfo, shares, selectedShareId]);
// 自动加入
useEffect(() => {
console.log('[CoSignJoin] Auto-join check:', {
step,
hasSessionInfo: !!sessionInfo,
hasJoinToken: !!joinToken,
selectedShareId,
autoJoinAttempted,
isLoading,
sharesCount: shares.length,
});
if (
step === 'select_share' &&
sessionInfo &&
joinToken &&
selectedShareId &&
!autoJoinAttempted &&
!isLoading
) {
// 找到匹配的 share 且未尝试过自动加入,则自动加入
const matchingShare = shares.find(s => s.sessionId === sessionInfo.keygenSessionId);
console.log('[CoSignJoin] Auto-join conditions met, checking share match:', {
keygenSessionId: sessionInfo.keygenSessionId,
matchingShareId: matchingShare?.id,
selectedShareId,
isMatch: matchingShare && matchingShare.id === selectedShareId,
});
if (matchingShare && matchingShare.id === selectedShareId) {
console.log('[CoSignJoin] Auto-joining session...');
setAutoJoinAttempted(true);
handleJoinSession();
} else {
console.log('[CoSignJoin] Share mismatch, not auto-joining');
}
}
}, [step, sessionInfo, joinToken, selectedShareId, autoJoinAttempted, isLoading, shares]);
const handleValidateCode = async (codeToValidate: string) => {
console.log('[CoSignJoin] handleValidateCode called:', codeToValidate);
if (!codeToValidate.trim()) {
setError('请输入邀请码');
return;
}
setIsLoading(true);
setError(null);
try {
const result: ValidateResult = await window.electronAPI.cosign.validateInviteCode(codeToValidate);
console.log('[CoSignJoin] validateInviteCode result:', {
success: result.success,
sessionInfo: result.sessionInfo,
hasJoinToken: !!result.joinToken,
joinTokenPreview: result.joinToken?.substring(0, 20) + '...',
error: result.error,
});
if (result.success && result.sessionInfo) {
setSessionInfo(result.sessionInfo);
if (result.joinToken) {
setJoinToken(result.joinToken);
} else {
console.warn('[CoSignJoin] WARNING: No joinToken in response!');
}
setStep('select_share');
} else {
setError(result.error || '无效的邀请码');
}
} catch (err) {
console.error('[CoSignJoin] validateInviteCode error:', err);
setError('验证邀请码失败,请检查网络连接');
} finally {
setIsLoading(false);
}
};
const handleJoinSession = async () => {
console.log('[CoSignJoin] handleJoinSession called:', {
hasSessionInfo: !!sessionInfo,
hasJoinToken: !!joinToken,
selectedShareId,
});
if (!sessionInfo) {
setError('会话信息不完整');
return;
}
if (!joinToken) {
setError('未获取到加入令牌,请重新验证邀请码');
return;
}
if (!selectedShareId) {
setError('请选择一个钱包');
return;
}
setStep('joining');
setIsLoading(true);
setError(null);
console.log('[CoSignJoin] Calling cosign.joinSession with:', {
sessionId: sessionInfo.sessionId,
shareId: selectedShareId,
walletName: sessionInfo.walletName,
messageHash: sessionInfo.messageHash,
threshold: sessionInfo.threshold,
parties: sessionInfo.parties,
});
try {
const result = await window.electronAPI.cosign.joinSession({
sessionId: sessionInfo.sessionId,
shareId: selectedShareId,
sharePassword: sharePassword,
joinToken: joinToken,
walletName: sessionInfo.walletName,
messageHash: sessionInfo.messageHash,
threshold: sessionInfo.threshold,
parties: sessionInfo.parties,
});
console.log('[CoSignJoin] joinSession result:', result);
if (result.success) {
navigate(`/cosign/session/${sessionInfo.sessionId}`);
} else {
setError(result.error || '加入会话失败');
setStep('select_share');
}
} catch (err) {
setError('加入会话失败,请重试');
setStep('select_share');
} finally {
setIsLoading(false);
}
};
const handlePaste = async () => {
try {
const text = await navigator.clipboard.readText();
setCode(text.trim());
} catch (err) {
console.error('Failed to read clipboard:', err);
}
};
const selectedShare = shares.find(s => s.id === selectedShareId);
return (
<div className={styles.container}>
<div className={styles.card}>
<h1 className={styles.title}></h1>
<p className={styles.subtitle}></p>
{step === 'input' && (
<div className={styles.form}>
<div className={styles.inputGroup}>
<label className={styles.label}></label>
<div className={styles.inputWrapper}>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="粘贴签名邀请码"
className={styles.input}
disabled={isLoading}
/>
<button
className={styles.pasteButton}
onClick={handlePaste}
disabled={isLoading}
>
</button>
</div>
</div>
{error && <div className={styles.error}>{error}</div>}
<div className={styles.actions}>
<button
className={styles.secondaryButton}
onClick={() => navigate('/')}
disabled={isLoading}
>
</button>
<button
className={styles.primaryButton}
onClick={() => handleValidateCode(code)}
disabled={isLoading || !code.trim()}
>
{isLoading ? '验证中...' : '下一步'}
</button>
</div>
</div>
)}
{step === 'select_share' && sessionInfo && (
<div className={styles.form}>
<div className={styles.sessionInfo}>
<h3 className={styles.sessionTitle}></h3>
<div className={styles.infoGrid}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>{sessionInfo.walletName}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{sessionInfo.threshold.t}-of-{sessionInfo.threshold.n}
</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue} style={{ fontFamily: 'monospace', fontSize: '12px' }}>
{sessionInfo.messageHash.substring(0, 16)}...
</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{sessionInfo.currentParticipants} / {sessionInfo.threshold.t}
</span>
</div>
</div>
</div>
{/* 选择本地 share */}
<div className={styles.inputGroup}>
<label className={styles.label}></label>
{shares.length === 0 ? (
<p className={styles.hint}></p>
) : (
<select
value={selectedShareId}
onChange={(e) => setSelectedShareId(e.target.value)}
className={styles.input}
disabled={isLoading}
>
<option value="">...</option>
{shares.map(share => (
<option key={share.id} value={share.id}>
{share.walletName} ({share.threshold.t}-of-{share.threshold.n})
{share.sessionId === sessionInfo.keygenSessionId ? ' [匹配]' : ''}
</option>
))}
</select>
)}
{selectedShare && (
<p className={styles.hint}>
: {selectedShare.publicKey.substring(0, 16)}...
</p>
)}
</div>
{/* 钱包密码 */}
<div className={styles.inputGroup}>
<label className={styles.label}> ()</label>
<input
type="password"
value={sharePassword}
onChange={(e) => setSharePassword(e.target.value)}
placeholder="如果设置了密码,请输入"
className={styles.input}
disabled={isLoading}
/>
</div>
{error && <div className={styles.error}>{error}</div>}
<div className={styles.actions}>
<button
className={styles.secondaryButton}
onClick={() => {
setStep('input');
setSessionInfo(null);
setJoinToken(null);
setSelectedShareId('');
setError(null);
setAutoJoinAttempted(false);
}}
disabled={isLoading}
>
</button>
<button
className={styles.primaryButton}
onClick={handleJoinSession}
disabled={isLoading || !selectedShareId}
>
{isLoading ? '加入中...' : '加入签名'}
</button>
</div>
</div>
)}
{step === 'joining' && (
<div className={styles.joining}>
<div className={styles.spinner}></div>
<p>...</p>
</div>
)}
</div>
</div>
);
}