870 lines
33 KiB
TypeScript
870 lines
33 KiB
TypeScript
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>
|
||
);
|
||
}
|