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

437 lines
15 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 { useNavigate, useParams } from 'react-router-dom';
import styles from './Sign.module.css';
interface ShareItem {
id: string;
walletName: string;
publicKey: string;
threshold: { t: number; n: number };
createdAt: string;
metadata: {
participants: Array<{ partyId: string; name: string }>;
};
}
interface SigningSession {
sessionId: string;
walletName: string;
messageHash: string;
threshold: { t: number; n: number };
currentParticipants: number;
initiator: string;
}
export default function Sign() {
const { sessionId: urlSessionId } = useParams<{ sessionId?: string }>();
const navigate = useNavigate();
const [step, setStep] = useState<'select' | 'join' | 'signing' | 'completed' | 'failed'>('select');
const [shares, setShares] = useState<ShareItem[]>([]);
const [selectedShare, setSelectedShare] = useState<ShareItem | null>(null);
const [sessionCode, setSessionCode] = useState(urlSessionId || '');
const [password, setPassword] = useState('');
const [signingSession, setSigningSession] = useState<SigningSession | null>(null);
const [signature, setSignature] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [progress, setProgress] = useState({ current: 0, total: 0 });
// 加载本地保存的 shares
useEffect(() => {
loadShares();
}, []);
// 如果 URL 中有 sessionId自动进入加入流程
useEffect(() => {
if (urlSessionId && shares.length > 0) {
setStep('join');
}
}, [urlSessionId, shares]);
const loadShares = async () => {
try {
if (window.electronAPI) {
const result = await window.electronAPI.storage.listShares();
if (result.success && result.data) {
setShares(result.data as ShareItem[]);
}
}
} catch (error) {
console.error('Failed to load shares:', error);
}
};
const handleImportShare = async () => {
try {
if (window.electronAPI) {
const filePath = await window.electronAPI.dialog.selectFile([
{ name: 'Share Backup', extensions: ['dat', 'json'] }
]);
if (filePath) {
const importPassword = window.prompt('请输入备份文件的密码:');
if (!importPassword) return;
const result = await window.electronAPI.storage.importShare(filePath, importPassword);
if (result.success && result.share) {
await loadShares();
setSelectedShare(result.share as ShareItem);
} else {
setError(result.error || '导入失败');
}
}
}
} catch (err) {
setError('导入失败: ' + (err as Error).message);
}
};
const handleValidateSession = async () => {
if (!sessionCode.trim()) {
setError('请输入签名会话码');
return;
}
setIsLoading(true);
setError(null);
try {
const result = await window.electronAPI.grpc.validateSigningSession(sessionCode);
if (result.success && result.session) {
setSigningSession(result.session);
setStep('join');
} else {
setError(result.error || '无效的签名会话码');
}
} catch (err) {
setError('验证失败: ' + (err as Error).message);
} finally {
setIsLoading(false);
}
};
const handleJoinSigning = async () => {
if (!selectedShare) {
setError('请选择要使用的钱包份额');
return;
}
// 密码是可选的,如果创建时没有设置密码,这里也不需要输入
setIsLoading(true);
setError(null);
setStep('signing');
try {
// 获取解密后的 share
const shareData = await window.electronAPI.storage.getShare(selectedShare.id, password);
if (!shareData) {
setError('密码错误或份额数据损坏');
setStep('join');
setIsLoading(false);
return;
}
// 加入签名会话
const result = await window.electronAPI.grpc.joinSigningSession({
sessionId: signingSession!.sessionId,
shareId: selectedShare.id,
password: password,
});
if (result.success) {
// 订阅签名进度
window.electronAPI.grpc.subscribeSigningProgress(
signingSession!.sessionId,
(event) => {
if (event.type === 'progress') {
setProgress({ current: event.current || 0, total: event.total || 0 });
} else if (event.type === 'completed') {
setSignature(event.signature || '');
setStep('completed');
setIsLoading(false);
} else if (event.type === 'failed') {
setError(event.error || '签名失败');
setStep('failed');
setIsLoading(false);
}
}
);
} else {
setError(result.error || '加入签名会话失败');
setStep('join');
setIsLoading(false);
}
} catch (err) {
setError('签名过程出错: ' + (err as Error).message);
setStep('failed');
setIsLoading(false);
}
};
const handleCopySignature = async () => {
if (signature) {
try {
await navigator.clipboard.writeText(signature);
} catch (err) {
console.error('Failed to copy:', err);
}
}
};
const handlePaste = async () => {
try {
const text = await navigator.clipboard.readText();
setSessionCode(text.trim());
} catch (err) {
console.error('Failed to read clipboard:', err);
}
};
return (
<div className={styles.container}>
<div className={styles.card}>
<h1 className={styles.title}></h1>
<p className={styles.subtitle}>使</p>
{step === 'select' && (
<div className={styles.form}>
{/* 选择本地 Share */}
<div className={styles.section}>
<h3 className={styles.sectionTitle}></h3>
{shares.length === 0 ? (
<div className={styles.emptyShares}>
<p></p>
<button className={styles.linkButton} onClick={handleImportShare}>
</button>
</div>
) : (
<div className={styles.shareList}>
{shares.map((share) => (
<div
key={share.id}
className={`${styles.shareItem} ${selectedShare?.id === share.id ? styles.shareItemSelected : ''}`}
onClick={() => setSelectedShare(share)}
>
<div className={styles.shareInfo}>
<span className={styles.shareName}>{share.walletName}</span>
<span className={styles.shareThreshold}>
{share.threshold.t}-of-{share.threshold.n}
</span>
</div>
<code className={styles.shareKey}>
{share.publicKey.slice(0, 8)}...{share.publicKey.slice(-8)}
</code>
</div>
))}
<button className={styles.linkButton} onClick={handleImportShare}>
+
</button>
</div>
)}
</div>
{/* 输入签名会话码 */}
<div className={styles.inputGroup}>
<label className={styles.label}></label>
<div className={styles.inputWrapper}>
<input
type="text"
value={sessionCode}
onChange={(e) => setSessionCode(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={handleValidateSession}
disabled={isLoading || !selectedShare || !sessionCode.trim()}
>
{isLoading ? '验证中...' : '下一步'}
</button>
</div>
</div>
)}
{step === 'join' && signingSession && (
<div className={styles.form}>
{/* 显示签名会话信息 */}
<div className={styles.sessionInfo}>
<h3 className={styles.sectionTitle}></h3>
<div className={styles.infoGrid}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>{signingSession.walletName}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>{signingSession.initiator}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{signingSession.threshold.t}-of-{signingSession.threshold.n}
</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{signingSession.currentParticipants} / {signingSession.threshold.t}
</span>
</div>
</div>
<div className={styles.messageHashSection}>
<span className={styles.infoLabel}></span>
<code className={styles.messageHash}>{signingSession.messageHash}</code>
</div>
</div>
{/* 已选择的 Share */}
<div className={styles.selectedShare}>
<span className={styles.infoLabel}>使</span>
<div className={styles.shareItem}>
<div className={styles.shareInfo}>
<span className={styles.shareName}>{selectedShare?.walletName}</span>
</div>
</div>
</div>
{/* 输入密码 */}
<div className={styles.inputGroup}>
<label className={styles.label}></label>
<input
type="password"
value={password}
onChange={(e) => setPassword(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('select');
setSigningSession(null);
setError(null);
}}
disabled={isLoading}
>
</button>
<button
className={styles.primaryButton}
onClick={handleJoinSigning}
disabled={isLoading}
>
{isLoading ? '加入中...' : '参与签名'}
</button>
</div>
</div>
)}
{step === 'signing' && (
<div className={styles.signing}>
<div className={styles.spinner}></div>
<h3 className={styles.signingTitle}></h3>
<p className={styles.signingText}>
...
</p>
{progress.total > 0 && (
<div className={styles.progressSection}>
<div className={styles.progressBar}>
<div
className={styles.progressFill}
style={{ width: `${(progress.current / progress.total) * 100}%` }}
/>
</div>
<span className={styles.progressText}>
{progress.current} / {progress.total}
</span>
</div>
)}
</div>
)}
{step === 'completed' && signature && (
<div className={styles.form}>
<div className={styles.successIcon}></div>
<h3 className={styles.successTitle}></h3>
<div className={styles.signatureSection}>
<label className={styles.label}></label>
<div className={styles.signatureWrapper}>
<code className={styles.signature}>{signature}</code>
<button className={styles.copyButton} onClick={handleCopySignature}>
</button>
</div>
</div>
<div className={styles.actions}>
<button
className={styles.primaryButton}
onClick={() => navigate('/')}
>
</button>
</div>
</div>
)}
{step === 'failed' && (
<div className={styles.form}>
<div className={styles.failureIcon}>!</div>
<h3 className={styles.failureTitle}></h3>
<p className={styles.failureText}>{error}</p>
<div className={styles.actions}>
<button
className={styles.secondaryButton}
onClick={() => {
setStep('select');
setError(null);
}}
>
</button>
<button
className={styles.primaryButton}
onClick={() => navigate('/')}
>
</button>
</div>
</div>
)}
</div>
</div>
);
}