269 lines
9.1 KiB
TypeScript
269 lines
9.1 KiB
TypeScript
import { useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { QRCodeSVG } from 'qrcode.react';
|
||
import styles from './Create.module.css';
|
||
|
||
interface CreateSessionResult {
|
||
success: boolean;
|
||
sessionId?: string;
|
||
inviteCode?: string;
|
||
error?: string;
|
||
}
|
||
|
||
export default function Create() {
|
||
const navigate = useNavigate();
|
||
|
||
const [walletName, setWalletName] = useState('');
|
||
const [thresholdT, setThresholdT] = useState(3);
|
||
const [thresholdN, setThresholdN] = useState(5);
|
||
const [participantName, setParticipantName] = useState('');
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [result, setResult] = useState<CreateSessionResult | null>(null);
|
||
const [step, setStep] = useState<'config' | 'creating' | 'created'>('config');
|
||
|
||
const handleCreateSession = async () => {
|
||
if (!walletName.trim()) {
|
||
setError('请输入钱包名称');
|
||
return;
|
||
}
|
||
if (!participantName.trim()) {
|
||
setError('请输入您的名称');
|
||
return;
|
||
}
|
||
if (thresholdT > thresholdN) {
|
||
setError('签名阈值不能大于参与方总数');
|
||
return;
|
||
}
|
||
if (thresholdT < 1) {
|
||
setError('签名阈值至少为 1');
|
||
return;
|
||
}
|
||
if (thresholdN < 2) {
|
||
setError('参与方总数至少为 2');
|
||
return;
|
||
}
|
||
|
||
setStep('creating');
|
||
setIsLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
const createResult = await window.electronAPI.grpc.createSession({
|
||
walletName: walletName.trim(),
|
||
thresholdT,
|
||
thresholdN,
|
||
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(`/session/${result.sessionId}`);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className={styles.container}>
|
||
<div className={styles.card}>
|
||
<h1 className={styles.title}>创建共管钱包</h1>
|
||
<p className={styles.subtitle}>3-of-5 混合托管模式 - 设置钱包参数并邀请参与方</p>
|
||
|
||
{step === 'config' && (
|
||
<div className={styles.form}>
|
||
{/* 混合托管说明 */}
|
||
<div className={styles.infoBox}>
|
||
<div className={styles.infoIcon}>ℹ️</div>
|
||
<div className={styles.infoContent}>
|
||
<strong>混合托管模式说明</strong>
|
||
<ul className={styles.infoList}>
|
||
<li><strong>5 个密钥份额</strong>: 2 个平台备份 + 3 个用户持有</li>
|
||
<li><strong>正常签名</strong>: 仅需 3 位用户共同签名</li>
|
||
<li><strong>密钥恢复</strong>: 允许 2 位用户丢失密钥,可使用平台备份轮换</li>
|
||
<li><strong>安全保障</strong>: 平台备份仅用于紧急恢复,不参与日常签名</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div className={styles.inputGroup}>
|
||
<label className={styles.label}>钱包名称</label>
|
||
<input
|
||
type="text"
|
||
value={walletName}
|
||
onChange={(e) => setWalletName(e.target.value)}
|
||
placeholder="为您的共管钱包命名"
|
||
className={styles.input}
|
||
disabled={isLoading}
|
||
/>
|
||
</div>
|
||
|
||
<div className={styles.inputGroup}>
|
||
<label className={styles.label}>阈值设置 (T-of-N)</label>
|
||
<div className={styles.thresholdConfig}>
|
||
<div className={styles.thresholdItem}>
|
||
<span className={styles.thresholdLabel}>签名阈值 (T)</span>
|
||
<div className={styles.numberInput}>
|
||
<button
|
||
className={styles.numberButton}
|
||
onClick={() => setThresholdT(Math.max(1, thresholdT - 1))}
|
||
disabled={thresholdT <= 1}
|
||
>
|
||
-
|
||
</button>
|
||
<span className={styles.numberValue}>{thresholdT}</span>
|
||
<button
|
||
className={styles.numberButton}
|
||
onClick={() => setThresholdT(Math.min(thresholdN, thresholdT + 1))}
|
||
disabled={thresholdT >= thresholdN}
|
||
>
|
||
+
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className={styles.thresholdDivider}>of</div>
|
||
<div className={styles.thresholdItem}>
|
||
<span className={styles.thresholdLabel}>参与方总数 (N)</span>
|
||
<div className={styles.numberInput}>
|
||
<button
|
||
className={styles.numberButton}
|
||
onClick={() => {
|
||
const newN = Math.max(2, thresholdN - 1);
|
||
setThresholdN(newN);
|
||
if (thresholdT > newN) setThresholdT(newN);
|
||
}}
|
||
disabled={thresholdN <= 2}
|
||
>
|
||
-
|
||
</button>
|
||
<span className={styles.numberValue}>{thresholdN}</span>
|
||
<button
|
||
className={styles.numberButton}
|
||
onClick={() => setThresholdN(thresholdN + 1)}
|
||
disabled={thresholdN >= 10}
|
||
>
|
||
+
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p className={styles.hint}>
|
||
需要 {thresholdT} 个参与方共同签名才能执行交易 (其中 2 个由平台托管用于备份)
|
||
</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}
|
||
>
|
||
创建会话
|
||
</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}>✓</div>
|
||
<h3 className={styles.successTitle}>会话创建成功</h3>
|
||
|
||
{/* QR Code for mobile scanning */}
|
||
<div className={styles.qrSection}>
|
||
<div className={styles.qrCodeWrapper}>
|
||
<QRCodeSVG
|
||
value={result.inviteCode || ''}
|
||
size={180}
|
||
level="M"
|
||
includeMargin={true}
|
||
bgColor="#ffffff"
|
||
fgColor="#000000"
|
||
/>
|
||
</div>
|
||
<p className={styles.qrHint}>使用手机 App 扫码加入</p>
|
||
</div>
|
||
|
||
<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>
|
||
);
|
||
}
|