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

870 lines
33 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, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { QRCodeSVG } from 'qrcode.react';
import styles from './Home.module.css';
import { deriveEvmAddress, formatAddress, getKavaExplorerUrl } from '../utils/address';
import {
prepareTransaction,
isValidAddress,
isValidAmount,
getCurrentNetwork,
getCurrentRpcUrl,
getGasPrice,
fetchGreenPointsBalance,
fetchEnergyPointsBalance,
fetchFuturePointsBalance,
GREEN_POINTS_TOKEN,
ENERGY_POINTS_TOKEN,
FUTURE_POINTS_TOKEN,
TOKEN_CONFIG,
type PreparedTransaction,
type TokenType,
} from '../utils/transaction';
interface ShareItem {
id: string;
walletName: string;
publicKey: string;
threshold: { t: number; n: number };
createdAt: string;
lastUsedAt?: string;
metadata: {
participants: Array<{ partyId: string; name: string }>;
};
}
interface ShareWithAddress extends ShareItem {
evmAddress?: string;
kavaBalance?: string;
greenPointsBalance?: string;
energyPointsBalance?: string;
futurePointsBalance?: string;
balanceLoading?: boolean;
}
/**
* 获取 KAVA 代币余额
* @param address EVM 地址
* @returns 余额字符串 (格式化后的 KAVA 数量)
*/
async function fetchKavaBalance(address: string): Promise<string> {
try {
const rpcUrl = getCurrentRpcUrl();
const response = await fetch(rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_getBalance',
params: [address, 'latest'],
id: 1,
}),
});
const data = await response.json();
if (data.result) {
// 将 wei 转换为 KAVA (18 位小数)
const balanceWei = BigInt(data.result);
const balanceKava = Number(balanceWei) / 1e18;
// 格式化显示,最多 6 位小数
return balanceKava.toFixed(balanceKava < 0.000001 && balanceKava > 0 ? 8 : 6).replace(/\.?0+$/, '') || '0';
}
return '0';
} catch (error) {
console.error('Failed to fetch KAVA balance:', error);
return '0';
}
}
export default function Home() {
const navigate = useNavigate();
const [shares, setShares] = useState<ShareWithAddress[]>([]);
const [loading, setLoading] = useState(true);
const [selectedShare, setSelectedShare] = useState<ShareWithAddress | null>(null);
const [showQrModal, setShowQrModal] = useState(false);
// 转账相关状态
const [showTransferModal, setShowTransferModal] = useState(false);
const [transferShare, setTransferShare] = useState<ShareWithAddress | null>(null);
const [transferTo, setTransferTo] = useState('');
const [transferAmount, setTransferAmount] = useState('');
const [transferPassword, setTransferPassword] = useState('');
const [transferTokenType, setTransferTokenType] = useState<TokenType>('KAVA');
const [transferStep, setTransferStep] = useState<'input' | 'confirm' | 'preparing' | 'error'>('input');
const [transferError, setTransferError] = useState<string | null>(null);
const [preparedTx, setPreparedTx] = useState<PreparedTransaction | null>(null);
const [isCalculatingMax, setIsCalculatingMax] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);
// 获取当前选择代币的余额
const getTokenBalance = (share: ShareWithAddress | null, tokenType: TokenType): string => {
if (!share) return '0';
switch (tokenType) {
case 'KAVA':
return share.kavaBalance || '0';
case 'GREEN_POINTS':
return share.greenPointsBalance || '0';
case 'ENERGY_POINTS':
return share.energyPointsBalance || '0';
case 'FUTURE_POINTS':
return share.futurePointsBalance || '0';
}
};
// 计算扣除 Gas 费后的最大可转账金额
const calculateMaxAmount = async () => {
if (!transferShare?.evmAddress) return;
setIsCalculatingMax(true);
try {
if (TOKEN_CONFIG.isERC20(transferTokenType)) {
// For ERC-20 token transfers, use the full token balance (gas is paid in KAVA)
const balance = getTokenBalance(transferShare, transferTokenType);
setTransferAmount(balance);
setTransferError(null);
} else {
// For KAVA transfers, deduct gas fee
const balance = parseFloat(transferShare.kavaBalance || '0');
if (balance <= 0) {
setTransferAmount('0');
return;
}
// 获取当前 gas 价格
const { maxFeePerGas } = await getGasPrice();
// 简单转账的 gas 限制是 21000
const gasLimit = BigInt(21000);
const gasFee = maxFeePerGas * gasLimit;
// 转换为 KAVA (18 位小数)
const gasFeeKava = Number(gasFee) / 1e18;
// 计算最大可转账金额 = 余额 - Gas费
const maxAmount = balance - gasFeeKava;
if (maxAmount <= 0) {
setTransferError('余额不足以支付 Gas 费');
setTransferAmount('0');
} else {
// 保留 6 位小数,向下取整避免精度问题
const formattedMax = Math.floor(maxAmount * 1000000) / 1000000;
setTransferAmount(formattedMax.toString());
setTransferError(null);
}
}
} catch (error) {
console.error('Failed to calculate max amount:', error);
if (TOKEN_CONFIG.isERC20(transferTokenType)) {
setTransferAmount(getTokenBalance(transferShare, transferTokenType));
} else {
// 如果获取 Gas 失败,使用默认估算 (1 gwei * 21000)
const defaultGasFee = 0.000021; // ~21000 * 1 gwei
const balance = parseFloat(transferShare.kavaBalance || '0');
const maxAmount = Math.max(0, balance - defaultGasFee);
const formattedMax = Math.floor(maxAmount * 1000000) / 1000000;
setTransferAmount(formattedMax.toString());
}
} finally {
setIsCalculatingMax(false);
}
};
const deriveAddresses = useCallback(async (shareList: ShareItem[]): Promise<ShareWithAddress[]> => {
const sharesWithAddresses: ShareWithAddress[] = [];
for (const share of shareList) {
try {
const evmAddress = await deriveEvmAddress(share.publicKey);
sharesWithAddresses.push({ ...share, evmAddress, balanceLoading: true });
} catch (error) {
console.warn(`Failed to derive address for share ${share.id}:`, error);
sharesWithAddresses.push({ ...share });
}
}
return sharesWithAddresses;
}, []);
// 单独获取所有钱包的余额 (KAVA 和 绿积分)
const fetchAllBalances = useCallback(async (sharesWithAddrs: ShareWithAddress[]) => {
const updatedShares = await Promise.all(
sharesWithAddrs.map(async (share) => {
if (share.evmAddress) {
// Fetch all balances in parallel
const [kavaBalance, greenPointsBalance, energyPointsBalance, futurePointsBalance] = await Promise.all([
fetchKavaBalance(share.evmAddress),
fetchGreenPointsBalance(share.evmAddress),
fetchEnergyPointsBalance(share.evmAddress),
fetchFuturePointsBalance(share.evmAddress),
]);
return { ...share, kavaBalance, greenPointsBalance, energyPointsBalance, futurePointsBalance, balanceLoading: false };
}
return { ...share, balanceLoading: false };
})
);
setShares(updatedShares);
}, []);
useEffect(() => {
loadShares();
}, []);
const loadShares = async () => {
try {
let shareList: ShareItem[] = [];
// 检测是否在 Electron 环境中
if (window.electronAPI) {
const result = await window.electronAPI.storage.listShares();
if (result.success && result.data) {
shareList = result.data as ShareItem[];
}
} else {
// 浏览器环境,使用 localStorage 或 API
const stored = localStorage.getItem('shares');
if (stored) {
shareList = JSON.parse(stored);
}
}
// 为每个 share 派生 EVM 地址
const sharesWithAddresses = await deriveAddresses(shareList);
setShares(sharesWithAddresses);
// 异步获取所有余额 (不阻塞 UI)
fetchAllBalances(sharesWithAddresses);
} catch (error) {
console.error('Failed to load shares:', error);
} finally {
setLoading(false);
}
};
const handleExport = async (id: string) => {
const password = window.prompt('请输入密码以导出备份文件:');
if (!password) return;
try {
if (window.electronAPI) {
// 先让用户选择保存位置
const savePath = await window.electronAPI.dialog.saveFile(
`share-backup-${id.slice(0, 8)}.dat`,
[{ name: 'Share Backup', extensions: ['dat'] }]
);
if (!savePath) return;
const result = await window.electronAPI.storage.exportShare(id, password);
if (result.success && result.data) {
// 通过 IPC 写入文件
const blob = new Blob([result.data], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `share-backup-${id.slice(0, 8)}.dat`;
a.click();
URL.revokeObjectURL(url);
alert('备份文件导出成功!');
} else {
alert('导出失败: ' + (result.error || '未知错误'));
}
}
} catch (error) {
alert('导出失败: ' + (error as Error).message);
}
};
const handleDelete = async (id: string) => {
const confirmed = window.confirm('确定要删除这个钱包吗?删除后无法恢复,请确保已备份。');
if (!confirmed) return;
try {
if (window.electronAPI) {
const result = await window.electronAPI.storage.deleteShare(id);
if (result.success) {
setShares(shares.filter((s) => s.id !== id));
} else {
alert('删除失败: ' + (result.error || '未知错误'));
}
}
} catch (error) {
alert('删除失败: ' + (error as Error).message);
}
};
const handleShowQr = (share: ShareWithAddress) => {
setSelectedShare(share);
setShowQrModal(true);
};
const handleCopyAddress = async (address: string) => {
try {
await navigator.clipboard.writeText(address);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy address:', err);
}
};
// 打开转账模态框
const handleOpenTransfer = (share: ShareWithAddress) => {
setTransferShare(share);
setTransferTo('');
setTransferAmount('');
setTransferPassword('');
setTransferTokenType('KAVA');
setTransferStep('input');
setTransferError(null);
setPreparedTx(null);
setShowTransferModal(true);
};
// 关闭转账模态框
const handleCloseTransfer = () => {
setShowTransferModal(false);
setTransferShare(null);
setPreparedTx(null);
};
// 验证转账输入
const validateTransferInput = (): string | null => {
if (!transferTo.trim()) {
return '请输入收款地址';
}
if (!isValidAddress(transferTo.trim())) {
return '收款地址格式无效';
}
if (!transferAmount.trim()) {
return '请输入转账金额';
}
if (!isValidAmount(transferAmount.trim())) {
return '转账金额无效';
}
const amount = parseFloat(transferAmount);
const balance = parseFloat(getTokenBalance(transferShare, transferTokenType));
if (amount > balance) {
return '余额不足';
}
return null;
};
// 准备交易
const handlePrepareTransaction = async () => {
const error = validateTransferInput();
if (error) {
setTransferError(error);
return;
}
setTransferStep('preparing');
setTransferError(null);
try {
const prepared = await prepareTransaction({
from: transferShare!.evmAddress!,
to: transferTo.trim().toLowerCase(),
value: transferAmount.trim(),
tokenType: transferTokenType,
});
setPreparedTx(prepared);
setTransferStep('confirm');
} catch (err) {
setTransferError('准备交易失败: ' + (err as Error).message);
setTransferStep('error');
}
};
// 发起签名会话
const handleInitiateCoSign = async () => {
if (!preparedTx || !transferShare) return;
try {
// 调用 co-sign API 创建签名会话
const result = await window.electronAPI.cosign.createSession({
shareId: transferShare.id,
sharePassword: transferPassword,
messageHash: preparedTx.signHash,
initiatorName: '发起者',
});
if (result.success && result.sessionId) {
// 保存交易信息到 sessionStorage以便签名完成后使用
// 注意: BigInt 无法直接 JSON 序列化,需要转换为字符串
const txToStore = {
preparedTx: {
...preparedTx,
gasLimit: preparedTx.gasLimit.toString(),
gasPrice: preparedTx.gasPrice.toString(),
value: preparedTx.value.toString(),
},
to: transferTo,
amount: transferAmount,
from: transferShare.evmAddress,
walletName: transferShare.walletName,
tokenType: transferTokenType,
};
sessionStorage.setItem(`tx_${result.sessionId}`, JSON.stringify(txToStore));
// 关闭模态框并跳转到签名会话页面
handleCloseTransfer();
navigate(`/cosign/session/${result.sessionId}`);
} else {
setTransferError(result.error || '创建签名会话失败');
setTransferStep('error');
}
} catch (err) {
setTransferError('创建签名会话失败: ' + (err as Error).message);
setTransferStep('error');
}
};
// 格式化 gas 费用显示
const formatGasFee = (gasLimit: bigint, gasPrice: bigint): string => {
const maxFee = gasLimit * gasPrice;
const feeKava = Number(maxFee) / 1e18;
return feeKava.toFixed(8).replace(/\.?0+$/, '');
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
if (loading) {
return (
<div className={styles.loading}>
<div className={styles.spinner} />
<p>...</p>
</div>
);
}
return (
<div className={styles.container}>
<header className={styles.header}>
<h1 className={styles.title}></h1>
<p className={styles.subtitle}> (3-of-5 )</p>
</header>
{shares.length === 0 ? (
<div className={styles.empty}>
<div className={styles.emptyIcon}>🔐</div>
<h2 className={styles.emptyTitle}></h2>
<p className={styles.emptyText}>
</p>
<div className={styles.emptyActions}>
<button
className={styles.primaryButton}
onClick={() => navigate('/create')}
>
</button>
<button
className={styles.secondaryButton}
onClick={() => navigate('/join')}
>
</button>
</div>
</div>
) : (
<div className={styles.grid}>
{shares.map((share) => (
<div key={share.id} className={styles.card}>
<div className={styles.cardHeader}>
<h3 className={styles.cardTitle}>{share.walletName}</h3>
<span className={styles.threshold}>
{share.threshold.t}-of-{share.threshold.n}
</span>
</div>
<div className={styles.cardBody}>
{/* 地址区域 - 可点击显示二维码 */}
{share.evmAddress && (
<div
className={styles.addressSection}
onClick={() => handleShowQr(share)}
>
<div className={styles.qrPreview}>
<QRCodeSVG
value={share.evmAddress}
size={80}
level="M"
includeMargin={false}
/>
</div>
<div className={styles.addressInfo}>
<span className={styles.addressLabel}>Kava EVM </span>
<code className={styles.addressValue}>
{formatAddress(share.evmAddress)}
</code>
<span className={styles.addressHint}></span>
</div>
</div>
)}
{/* 余额显示 - 所有代币 */}
{share.evmAddress && (
<div className={styles.balanceSection}>
<div className={styles.balanceRow}>
<span className={styles.balanceLabel}>KAVA</span>
<span className={styles.balanceValue}>
{share.balanceLoading ? (
<span className={styles.balanceLoading}>...</span>
) : (
<>{share.kavaBalance || '0'}</>
)}
</span>
</div>
<div className={styles.balanceRow}>
<span className={styles.balanceLabel} style={{ color: '#4CAF50' }}>{GREEN_POINTS_TOKEN.name}</span>
<span className={styles.balanceValue} style={{ color: '#4CAF50' }}>
{share.balanceLoading ? (
<span className={styles.balanceLoading}>...</span>
) : (
<>{share.greenPointsBalance || '0'}</>
)}
</span>
</div>
<div className={styles.balanceRow}>
<span className={styles.balanceLabel} style={{ color: '#2196F3' }}>{ENERGY_POINTS_TOKEN.name}</span>
<span className={styles.balanceValue} style={{ color: '#2196F3' }}>
{share.balanceLoading ? (
<span className={styles.balanceLoading}>...</span>
) : (
<>{share.energyPointsBalance || '0'}</>
)}
</span>
</div>
<div className={styles.balanceRow}>
<span className={styles.balanceLabel} style={{ color: '#9C27B0' }}>{FUTURE_POINTS_TOKEN.name}</span>
<span className={styles.balanceValue} style={{ color: '#9C27B0' }}>
{share.balanceLoading ? (
<span className={styles.balanceLoading}>...</span>
) : (
<>{share.futurePointsBalance || '0'}</>
)}
</span>
</div>
</div>
)}
<div className={styles.infoRow}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{(share.metadata?.participants || []).length}
</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{formatDate(share.createdAt)}
</span>
</div>
{share.lastUsedAt && (
<div className={styles.infoRow}>
<span className={styles.infoLabel}>使</span>
<span className={styles.infoValue}>
{formatDate(share.lastUsedAt)}
</span>
</div>
)}
</div>
<div className={styles.cardFooter}>
{share.evmAddress && (
<button
className={`${styles.actionButton} ${styles.transferButton}`}
onClick={() => handleOpenTransfer(share)}
>
</button>
)}
<button
className={styles.actionButton}
onClick={() => handleExport(share.id)}
>
</button>
<button
className={`${styles.actionButton} ${styles.dangerButton}`}
onClick={() => handleDelete(share.id)}
>
</button>
</div>
</div>
))}
</div>
)}
{/* 转账模态框 */}
{showTransferModal && transferShare && (
<div className={styles.modalOverlay} onClick={handleCloseTransfer}>
<div className={styles.transferModal} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}></h2>
<button
className={styles.modalClose}
onClick={handleCloseTransfer}
>
×
</button>
</div>
<div className={styles.modalBody}>
{/* 钱包信息 */}
<div className={styles.transferWalletInfo}>
<div className={styles.transferWalletName}>{transferShare.walletName}</div>
<div className={styles.transferWalletBalance}>
KAVA: {transferShare.kavaBalance || '0'} | <span style={{color: '#4CAF50'}}>{GREEN_POINTS_TOKEN.name}: {transferShare.greenPointsBalance || '0'}</span>
</div>
<div className={styles.transferWalletBalance}>
<span style={{color: '#2196F3'}}>{ENERGY_POINTS_TOKEN.name}: {transferShare.energyPointsBalance || '0'}</span> | <span style={{color: '#9C27B0'}}>{FUTURE_POINTS_TOKEN.name}: {transferShare.futurePointsBalance || '0'}</span>
</div>
<div className={styles.transferNetwork}>
网络: Kava {getCurrentNetwork() === 'mainnet' ? '主网' : '测试网'}
</div>
</div>
{transferStep === 'input' && (
<div className={styles.transferForm}>
{/* 代币类型选择 */}
<div className={styles.transferInputGroup}>
<label className={styles.transferLabel}></label>
<div className={styles.tokenTypeSelector}>
<button
className={`${styles.tokenTypeButton} ${transferTokenType === 'KAVA' ? styles.tokenTypeActive : ''}`}
onClick={() => { setTransferTokenType('KAVA'); setTransferAmount(''); }}
>
KAVA
</button>
<button
className={`${styles.tokenTypeButton} ${transferTokenType === 'GREEN_POINTS' ? styles.tokenTypeActive : ''}`}
onClick={() => { setTransferTokenType('GREEN_POINTS'); setTransferAmount(''); }}
style={transferTokenType === 'GREEN_POINTS' ? { backgroundColor: '#4CAF50', borderColor: '#4CAF50' } : {}}
>
{GREEN_POINTS_TOKEN.name}
</button>
</div>
<div className={styles.tokenTypeSelector} style={{ marginTop: '8px' }}>
<button
className={`${styles.tokenTypeButton} ${transferTokenType === 'ENERGY_POINTS' ? styles.tokenTypeActive : ''}`}
onClick={() => { setTransferTokenType('ENERGY_POINTS'); setTransferAmount(''); }}
style={transferTokenType === 'ENERGY_POINTS' ? { backgroundColor: '#2196F3', borderColor: '#2196F3' } : {}}
>
{ENERGY_POINTS_TOKEN.name}
</button>
<button
className={`${styles.tokenTypeButton} ${transferTokenType === 'FUTURE_POINTS' ? styles.tokenTypeActive : ''}`}
onClick={() => { setTransferTokenType('FUTURE_POINTS'); setTransferAmount(''); }}
style={transferTokenType === 'FUTURE_POINTS' ? { backgroundColor: '#9C27B0', borderColor: '#9C27B0' } : {}}
>
{FUTURE_POINTS_TOKEN.name}
</button>
</div>
</div>
{/* 收款地址 */}
<div className={styles.transferInputGroup}>
<label className={styles.transferLabel}></label>
<input
type="text"
value={transferTo}
onChange={(e) => setTransferTo(e.target.value)}
placeholder="0x..."
className={styles.transferInput}
/>
</div>
{/* 转账金额 */}
<div className={styles.transferInputGroup}>
<label className={styles.transferLabel}>
({TOKEN_CONFIG.getName(transferTokenType)})
</label>
<div className={styles.transferAmountWrapper}>
<input
type="text"
value={transferAmount}
onChange={(e) => setTransferAmount(e.target.value)}
placeholder="0.0"
className={styles.transferInput}
/>
<button
className={styles.maxButton}
onClick={calculateMaxAmount}
disabled={isCalculatingMax}
>
{isCalculatingMax ? '...' : 'MAX'}
</button>
</div>
</div>
{/* 钱包密码 */}
<div className={styles.transferInputGroup}>
<label className={styles.transferLabel}> ()</label>
<input
type="password"
value={transferPassword}
onChange={(e) => setTransferPassword(e.target.value)}
placeholder="如果设置了密码,请输入"
className={styles.transferInput}
/>
</div>
{transferError && (
<div className={styles.transferError}>{transferError}</div>
)}
<div className={styles.transferActions}>
<button
className={styles.secondaryButton}
onClick={handleCloseTransfer}
>
</button>
<button
className={styles.primaryButton}
onClick={handlePrepareTransaction}
>
</button>
</div>
</div>
)}
{transferStep === 'preparing' && (
<div className={styles.transferPreparing}>
<div className={styles.spinner}></div>
<p>...</p>
</div>
)}
{transferStep === 'confirm' && preparedTx && (
<div className={styles.transferConfirm}>
<h3 className={styles.confirmTitle}></h3>
<div className={styles.confirmDetails}>
<div className={styles.confirmRow}>
<span className={styles.confirmLabel}></span>
<span className={styles.confirmValue} style={TOKEN_CONFIG.isERC20(transferTokenType) ? { color: transferTokenType === 'GREEN_POINTS' ? '#4CAF50' : transferTokenType === 'ENERGY_POINTS' ? '#2196F3' : '#9C27B0' } : {}}>
{TOKEN_CONFIG.getName(transferTokenType)}
</span>
</div>
<div className={styles.confirmRow}>
<span className={styles.confirmLabel}></span>
<span className={styles.confirmValue}>{formatAddress(transferTo, 8, 6)}</span>
</div>
<div className={styles.confirmRow}>
<span className={styles.confirmLabel}></span>
<span className={styles.confirmValue} style={TOKEN_CONFIG.isERC20(transferTokenType) ? { color: transferTokenType === 'GREEN_POINTS' ? '#4CAF50' : transferTokenType === 'ENERGY_POINTS' ? '#2196F3' : '#9C27B0' } : {}}>
{transferAmount} {TOKEN_CONFIG.getName(transferTokenType)}
</span>
</div>
<div className={styles.confirmRow}>
<span className={styles.confirmLabel}> Gas </span>
<span className={styles.confirmValue}>
~{formatGasFee(preparedTx.gasLimit, preparedTx.gasPrice)} KAVA
</span>
</div>
<div className={styles.confirmRow}>
<span className={styles.confirmLabel}>Nonce</span>
<span className={styles.confirmValue}>{preparedTx.nonce}</span>
</div>
</div>
<div className={styles.confirmNote}>
<strong>:</strong> {transferShare.threshold.t}-of-{transferShare.threshold.n}
{transferShare.threshold.t}
</div>
{transferError && (
<div className={styles.transferError}>{transferError}</div>
)}
<div className={styles.transferActions}>
<button
className={styles.secondaryButton}
onClick={() => setTransferStep('input')}
>
</button>
<button
className={styles.primaryButton}
onClick={handleInitiateCoSign}
>
</button>
</div>
</div>
)}
{transferStep === 'error' && (
<div className={styles.transferError}>
<p>{transferError}</p>
<button
className={styles.secondaryButton}
onClick={() => setTransferStep('input')}
>
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* 二维码弹窗 */}
{showQrModal && selectedShare && (
<div className={styles.modalOverlay} onClick={() => setShowQrModal(false)}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{selectedShare.walletName}</h2>
<button
className={styles.modalClose}
onClick={() => setShowQrModal(false)}
>
×
</button>
</div>
<div className={styles.modalBody}>
<div className={styles.qrContainer}>
<QRCodeSVG
value={selectedShare.evmAddress || ''}
size={240}
level="H"
includeMargin={true}
/>
</div>
<div className={styles.fullAddress}>
<span className={styles.addressLabelLarge}>Kava EVM </span>
<code className={styles.addressValueLarge}>
{selectedShare.evmAddress}
</code>
</div>
<div className={styles.modalActions}>
<button
className={styles.primaryButton}
onClick={() => handleCopyAddress(selectedShare.evmAddress || '')}
>
{copySuccess ? '✓ 已复制' : '复制地址'}
</button>
<a
href={getKavaExplorerUrl(selectedShare.evmAddress || '', getCurrentNetwork() === 'testnet')}
target="_blank"
rel="noopener noreferrer"
className={styles.secondaryButton}
>
</a>
</div>
</div>
</div>
</div>
)}
</div>
);
}