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

266 lines
8.7 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 } from 'react-router-dom';
import styles from './Create.module.css';
interface Share {
id: string;
walletName: string;
publicKey: string;
threshold: { t: number; n: number };
createdAt: string;
}
interface CreateCoSignResult {
success: boolean;
sessionId?: string;
inviteCode?: string;
error?: string;
}
export default function CoSignCreate() {
const navigate = useNavigate();
const [shares, setShares] = useState<Share[]>([]);
const [selectedShareId, setSelectedShareId] = useState('');
const [sharePassword, setSharePassword] = useState('');
const [messageHash, setMessageHash] = useState('');
const [participantName, setParticipantName] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<CreateCoSignResult | null>(null);
const [step, setStep] = useState<'config' | 'creating' | 'created'>('config');
// 加载本地 shares
useEffect(() => {
const loadShares = async () => {
try {
const result = await window.electronAPI.storage.listShares();
// 兼容不同返回格式
const shareList = Array.isArray(result) ? result : ((result as any)?.data || []);
setShares(shareList);
if (shareList.length > 0) {
setSelectedShareId(shareList[0].id);
}
} catch (err) {
console.error('Failed to load shares:', err);
}
};
loadShares();
}, []);
const handleCreateSession = async () => {
if (!selectedShareId) {
setError('请选择一个钱包');
return;
}
if (!messageHash.trim()) {
setError('请输入待签名的消息哈希');
return;
}
if (!/^[0-9a-fA-F]{64}$/.test(messageHash.trim())) {
setError('消息哈希必须是 64 位十六进制字符串 (32 字节)');
return;
}
if (!participantName.trim()) {
setParticipantName('发起者');
}
setStep('creating');
setIsLoading(true);
setError(null);
try {
const createResult = await window.electronAPI.cosign.createSession({
shareId: selectedShareId,
sharePassword: sharePassword,
messageHash: messageHash.trim().toLowerCase(),
initiatorName: participantName.trim() || '发起者',
});
if (createResult.success) {
setResult(createResult);
setStep('created');
} else {
setError(createResult.error || '创建签名会话失败');
setStep('config');
}
} catch (err) {
setError('创建签名会话失败,请检查网络连接');
setStep('config');
} finally {
setIsLoading(false);
}
};
const handleCopyInviteCode = async () => {
if (result?.inviteCode) {
try {
await navigator.clipboard.writeText(result.inviteCode);
} catch (err) {
console.error('Failed to copy:', err);
}
}
};
const handleGoToSession = () => {
if (result?.sessionId) {
navigate(`/cosign/session/${result.sessionId}`);
}
};
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 === 'config' && (
<div className={styles.form}>
{/* 签名说明 */}
<div className={styles.infoBox}>
<div className={styles.infoIcon}>i</div>
<div className={styles.infoContent}>
<strong></strong>
<ul className={styles.infoList}>
<li><strong></strong>: 使</li>
<li><strong></strong>: SHA256 </li>
<li><strong></strong>: </li>
<li><strong></strong>: </li>
</ul>
</div>
</div>
{/* 选择钱包 */}
<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}
>
{shares.map(share => (
<option key={share.id} value={share.id}>
{share.walletName} ({share.threshold.t}-of-{share.threshold.n})
</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>
{/* 消息哈希 */}
<div className={styles.inputGroup}>
<label className={styles.label}> (Hex)</label>
<input
type="text"
value={messageHash}
onChange={(e) => setMessageHash(e.target.value)}
placeholder="64位十六进制字符串如: a1b2c3d4..."
className={styles.input}
disabled={isLoading}
/>
<p className={styles.hint}>
SHA256 (32 = 64 )
</p>
</div>
{/* 参与者名称 */}
<div className={styles.inputGroup}>
<label className={styles.label}></label>
<input
type="text"
value={participantName}
onChange={(e) => setParticipantName(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={() => navigate('/')}
disabled={isLoading}
>
</button>
<button
className={styles.primaryButton}
onClick={handleCreateSession}
disabled={isLoading || shares.length === 0}
>
</button>
</div>
</div>
)}
{step === 'creating' && (
<div className={styles.creating}>
<div className={styles.spinner}></div>
<p>...</p>
</div>
)}
{step === 'created' && result && (
<div className={styles.form}>
<div className={styles.successIcon}>OK</div>
<h3 className={styles.successTitle}></h3>
<div className={styles.inviteSection}>
<label className={styles.label}></label>
<div className={styles.inviteCodeWrapper}>
<code className={styles.inviteCode}>{result.inviteCode}</code>
<button
className={styles.copyButton}
onClick={handleCopyInviteCode}
>
</button>
</div>
<p className={styles.hint}>
使
</p>
</div>
<div className={styles.actions}>
<button
className={styles.primaryButton}
onClick={handleGoToSession}
>
</button>
</div>
</div>
)}
</div>
</div>
);
}