266 lines
8.7 KiB
TypeScript
266 lines
8.7 KiB
TypeScript
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>
|
||
);
|
||
}
|