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 { 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([]); const [loading, setLoading] = useState(true); const [selectedShare, setSelectedShare] = useState(null); const [showQrModal, setShowQrModal] = useState(false); // 转账相关状态 const [showTransferModal, setShowTransferModal] = useState(false); const [transferShare, setTransferShare] = useState(null); const [transferTo, setTransferTo] = useState(''); const [transferAmount, setTransferAmount] = useState(''); const [transferPassword, setTransferPassword] = useState(''); const [transferTokenType, setTransferTokenType] = useState('KAVA'); const [transferStep, setTransferStep] = useState<'input' | 'confirm' | 'preparing' | 'error'>('input'); const [transferError, setTransferError] = useState(null); const [preparedTx, setPreparedTx] = useState(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 => { 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 (

加载中...

); } return (

我的共管钱包

管理您参与的多方共管钱包 (3-of-5 混合托管)

{shares.length === 0 ? (
🔐

暂无共管钱包

您可以创建新的共管钱包,或加入他人发起的钱包创建

) : (
{shares.map((share) => (

{share.walletName}

{share.threshold.t}-of-{share.threshold.n}
{/* 地址区域 - 可点击显示二维码 */} {share.evmAddress && (
handleShowQr(share)} >
Kava EVM 地址 {formatAddress(share.evmAddress)} 点击查看完整二维码
)} {/* 余额显示 - 所有代币 */} {share.evmAddress && (
KAVA {share.balanceLoading ? ( 加载中... ) : ( <>{share.kavaBalance || '0'} )}
{GREEN_POINTS_TOKEN.name} {share.balanceLoading ? ( 加载中... ) : ( <>{share.greenPointsBalance || '0'} )}
{ENERGY_POINTS_TOKEN.name} {share.balanceLoading ? ( 加载中... ) : ( <>{share.energyPointsBalance || '0'} )}
{FUTURE_POINTS_TOKEN.name} {share.balanceLoading ? ( 加载中... ) : ( <>{share.futurePointsBalance || '0'} )}
)}
参与方 {(share.metadata?.participants || []).length} 人
创建时间 {formatDate(share.createdAt)}
{share.lastUsedAt && (
上次使用 {formatDate(share.lastUsedAt)}
)}
{share.evmAddress && ( )}
))}
)} {/* 转账模态框 */} {showTransferModal && transferShare && (
e.stopPropagation()}>

转账

{/* 钱包信息 */}
{transferShare.walletName}
KAVA: {transferShare.kavaBalance || '0'} | {GREEN_POINTS_TOKEN.name}: {transferShare.greenPointsBalance || '0'}
{ENERGY_POINTS_TOKEN.name}: {transferShare.energyPointsBalance || '0'} | {FUTURE_POINTS_TOKEN.name}: {transferShare.futurePointsBalance || '0'}
网络: Kava {getCurrentNetwork() === 'mainnet' ? '主网' : '测试网'}
{transferStep === 'input' && (
{/* 代币类型选择 */}
{/* 收款地址 */}
setTransferTo(e.target.value)} placeholder="0x..." className={styles.transferInput} />
{/* 转账金额 */}
setTransferAmount(e.target.value)} placeholder="0.0" className={styles.transferInput} />
{/* 钱包密码 */}
setTransferPassword(e.target.value)} placeholder="如果设置了密码,请输入" className={styles.transferInput} />
{transferError && (
{transferError}
)}
)} {transferStep === 'preparing' && (

正在准备交易...

)} {transferStep === 'confirm' && preparedTx && (

确认交易

转账类型 {TOKEN_CONFIG.getName(transferTokenType)}
收款地址 {formatAddress(transferTo, 8, 6)}
转账金额 {transferAmount} {TOKEN_CONFIG.getName(transferTokenType)}
预估 Gas 费 ~{formatGasFee(preparedTx.gasLimit, preparedTx.gasPrice)} KAVA
Nonce {preparedTx.nonce}
注意: 这是一个 {transferShare.threshold.t}-of-{transferShare.threshold.n} 共管钱包, 需要至少 {transferShare.threshold.t} 方参与签名才能完成交易。
{transferError && (
{transferError}
)}
)} {transferStep === 'error' && (

{transferError}

)}
)} {/* 二维码弹窗 */} {showQrModal && selectedShare && (
setShowQrModal(false)}>
e.stopPropagation()}>

{selectedShare.walletName}

Kava EVM 地址 {selectedShare.evmAddress}
在区块浏览器查看
)}
); }