322 lines
10 KiB
TypeScript
322 lines
10 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import styles from './co-managed-wallet.module.scss';
|
||
import { cn } from '@/utils/helpers';
|
||
import ThresholdConfig from './ThresholdConfig';
|
||
import InviteQRCode from './InviteQRCode';
|
||
import ParticipantList from './ParticipantList';
|
||
import SessionProgress from './SessionProgress';
|
||
import WalletResult from './WalletResult';
|
||
|
||
interface Participant {
|
||
partyId: string;
|
||
name: string;
|
||
status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed';
|
||
joinedAt?: string;
|
||
}
|
||
|
||
interface SessionState {
|
||
sessionId: string;
|
||
inviteCode: string;
|
||
inviteUrl: string;
|
||
status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed';
|
||
participants: Participant[];
|
||
currentRound: number;
|
||
totalRounds: number;
|
||
publicKey?: string;
|
||
error?: string;
|
||
createdAt?: string;
|
||
}
|
||
|
||
interface CreateWalletModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
}
|
||
|
||
type Step = 'config' | 'invite' | 'progress' | 'result';
|
||
|
||
/**
|
||
* 创建共管钱包向导对话框
|
||
*/
|
||
export default function CreateWalletModal({ isOpen, onClose }: CreateWalletModalProps) {
|
||
const [step, setStep] = useState<Step>('config');
|
||
const [walletName, setWalletName] = useState('');
|
||
const [thresholdT, setThresholdT] = useState(2);
|
||
const [thresholdN, setThresholdN] = useState(3);
|
||
const [initiatorName, setInitiatorName] = useState('');
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [session, setSession] = useState<SessionState | null>(null);
|
||
|
||
// 重置状态
|
||
useEffect(() => {
|
||
if (!isOpen) {
|
||
setStep('config');
|
||
setWalletName('');
|
||
setThresholdT(2);
|
||
setThresholdN(3);
|
||
setInitiatorName('');
|
||
setIsLoading(false);
|
||
setError(null);
|
||
setSession(null);
|
||
}
|
||
}, [isOpen]);
|
||
|
||
// 创建会话
|
||
const handleCreateSession = async () => {
|
||
if (!walletName.trim()) {
|
||
setError('请输入钱包名称');
|
||
return;
|
||
}
|
||
if (!initiatorName.trim()) {
|
||
setError('请输入您的名称');
|
||
return;
|
||
}
|
||
if (thresholdT > thresholdN) {
|
||
setError('签名阈值不能大于参与方总数');
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
// TODO: 调用 API 创建会话
|
||
// const result = await coManagedWalletService.createSession({
|
||
// walletName: walletName.trim(),
|
||
// thresholdT,
|
||
// thresholdN,
|
||
// initiatorName: initiatorName.trim(),
|
||
// });
|
||
|
||
// 模拟创建成功
|
||
const mockSession: SessionState = {
|
||
sessionId: 'session-' + Date.now(),
|
||
inviteCode: 'ABCD-1234-EFGH',
|
||
inviteUrl: `https://app.rwadurian.com/join/ABCD-1234-EFGH`,
|
||
status: 'waiting',
|
||
participants: [
|
||
{
|
||
partyId: 'party-1',
|
||
name: initiatorName.trim(),
|
||
status: 'ready',
|
||
joinedAt: new Date().toISOString(),
|
||
},
|
||
],
|
||
currentRound: 0,
|
||
totalRounds: 3,
|
||
createdAt: new Date().toISOString(),
|
||
};
|
||
|
||
setSession(mockSession);
|
||
setStep('invite');
|
||
} catch (err) {
|
||
setError('创建会话失败,请重试');
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 开始密钥生成
|
||
const handleStartKeygen = async () => {
|
||
if (!session) return;
|
||
|
||
setStep('progress');
|
||
setSession(prev => prev ? { ...prev, status: 'processing' } : null);
|
||
|
||
// TODO: 监听会话状态更新
|
||
// 模拟进度更新
|
||
for (let i = 1; i <= 3; i++) {
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
setSession(prev => prev ? { ...prev, currentRound: i } : null);
|
||
}
|
||
|
||
// 模拟完成
|
||
setSession(prev => prev ? {
|
||
...prev,
|
||
status: 'completed',
|
||
publicKey: '0x1234567890abcdef1234567890abcdef12345678',
|
||
} : null);
|
||
setStep('result');
|
||
};
|
||
|
||
// 渲染步骤指示器
|
||
const renderStepIndicator = () => {
|
||
const steps = [
|
||
{ key: 'config', label: '配置' },
|
||
{ key: 'invite', label: '邀请' },
|
||
{ key: 'progress', label: '生成' },
|
||
{ key: 'result', label: '完成' },
|
||
];
|
||
|
||
const currentIndex = steps.findIndex(s => s.key === step);
|
||
|
||
return (
|
||
<div className={styles.createWalletModal__steps}>
|
||
{steps.map((s, index) => (
|
||
<div
|
||
key={s.key}
|
||
className={cn(
|
||
styles.createWalletModal__step,
|
||
index < currentIndex && styles['createWalletModal__step--completed'],
|
||
index === currentIndex && styles['createWalletModal__step--active']
|
||
)}
|
||
>
|
||
<div className={styles.createWalletModal__stepNumber}>{index + 1}</div>
|
||
<span className={styles.createWalletModal__stepLabel}>{s.label}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
if (!isOpen) return null;
|
||
|
||
return (
|
||
<div className={styles.modalOverlay} onClick={onClose}>
|
||
<div className={styles.createWalletModal} onClick={e => e.stopPropagation()}>
|
||
<div className={styles.createWalletModal__header}>
|
||
<h2 className={styles.createWalletModal__title}>创建共管钱包</h2>
|
||
<button
|
||
type="button"
|
||
className={styles.createWalletModal__closeBtn}
|
||
onClick={onClose}
|
||
aria-label="关闭"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
{renderStepIndicator()}
|
||
|
||
<div className={styles.createWalletModal__content}>
|
||
{step === 'config' && (
|
||
<div className={styles.createWalletModal__form}>
|
||
<div className={styles.createWalletModal__field}>
|
||
<label className={styles.createWalletModal__label}>钱包名称</label>
|
||
<input
|
||
type="text"
|
||
value={walletName}
|
||
onChange={e => setWalletName(e.target.value)}
|
||
placeholder="为您的共管钱包命名"
|
||
className={styles.createWalletModal__input}
|
||
disabled={isLoading}
|
||
/>
|
||
</div>
|
||
|
||
<div className={styles.createWalletModal__field}>
|
||
<label className={styles.createWalletModal__label}>阈值设置 (T-of-N)</label>
|
||
<ThresholdConfig
|
||
thresholdT={thresholdT}
|
||
thresholdN={thresholdN}
|
||
onThresholdTChange={setThresholdT}
|
||
onThresholdNChange={setThresholdN}
|
||
disabled={isLoading}
|
||
/>
|
||
<p className={styles.createWalletModal__hint}>
|
||
需要 {thresholdT} 个参与方共同签名才能执行交易
|
||
</p>
|
||
</div>
|
||
|
||
<div className={styles.createWalletModal__field}>
|
||
<label className={styles.createWalletModal__label}>您的名称</label>
|
||
<input
|
||
type="text"
|
||
value={initiatorName}
|
||
onChange={e => setInitiatorName(e.target.value)}
|
||
placeholder="输入您的名称(其他参与者可见)"
|
||
className={styles.createWalletModal__input}
|
||
disabled={isLoading}
|
||
/>
|
||
</div>
|
||
|
||
{error && <p className={styles.createWalletModal__error}>{error}</p>}
|
||
|
||
<div className={styles.createWalletModal__actions}>
|
||
<button
|
||
type="button"
|
||
className={styles.createWalletModal__cancelBtn}
|
||
onClick={onClose}
|
||
disabled={isLoading}
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={styles.createWalletModal__submitBtn}
|
||
onClick={handleCreateSession}
|
||
disabled={isLoading}
|
||
>
|
||
{isLoading ? '创建中...' : '创建会话'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{step === 'invite' && session && (
|
||
<div className={styles.createWalletModal__invite}>
|
||
<InviteQRCode
|
||
inviteCode={session.inviteCode}
|
||
inviteUrl={session.inviteUrl}
|
||
/>
|
||
|
||
<ParticipantList
|
||
participants={session.participants}
|
||
totalRequired={thresholdN}
|
||
/>
|
||
|
||
<div className={styles.createWalletModal__actions}>
|
||
<button
|
||
type="button"
|
||
className={styles.createWalletModal__cancelBtn}
|
||
onClick={() => setStep('config')}
|
||
>
|
||
返回修改
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={styles.createWalletModal__submitBtn}
|
||
onClick={handleStartKeygen}
|
||
disabled={session.participants.length < thresholdN}
|
||
>
|
||
{session.participants.length < thresholdN
|
||
? `等待参与方 (${session.participants.length}/${thresholdN})`
|
||
: '开始生成密钥'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{step === 'progress' && session && (
|
||
<div className={styles.createWalletModal__progress}>
|
||
<SessionProgress
|
||
status={session.status}
|
||
currentRound={session.currentRound}
|
||
totalRounds={session.totalRounds}
|
||
error={session.error}
|
||
/>
|
||
|
||
<ParticipantList
|
||
participants={session.participants}
|
||
totalRequired={thresholdN}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{step === 'result' && session && session.publicKey && (
|
||
<WalletResult
|
||
walletName={walletName}
|
||
publicKey={session.publicKey}
|
||
threshold={{ t: thresholdT, n: thresholdN }}
|
||
participants={session.participants}
|
||
createdAt={session.createdAt || new Date().toISOString()}
|
||
onClose={onClose}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|