From 3da0bf24d091887afecea882ef7bd4b0da4a53c7 Mon Sep 17 00:00:00 2001 From: Sofio Date: Tue, 16 Dec 2025 18:57:26 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E9=87=91=E5=BA=93?= =?UTF-8?q?=E4=BA=A4=E6=98=93=E5=92=8CLP=E6=B5=81=E5=8A=A8=E6=B1=A0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要更新: - 金库交易动态读取: 从合约动态获取所有金库地址和名称 - 边界值测试优化: 修复 buy_exceed_hardcap 计算逻辑和错误处理 - 新增 ErrorBoundary 组件: 全局错误边界处理 - vite 配置优化: 添加 optimizeDeps 解决动态导入问题 - 交易历史和连接按钮改进 技术改进: - 使用 useReadContract/useReadContracts 批量读取合约数据 - 改进 parseError 函数处理 ccip 模块加载失败 - 添加测试状态管理 (isTestRunning, testTypeRef) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/App.css | 114 +++ frontend/src/App.tsx | 21 +- frontend/src/components/ConnectButton.tsx | 49 +- frontend/src/components/ErrorBoundary.tsx | 100 ++ frontend/src/components/FactoryPanel.tsx | 40 +- frontend/src/components/LPPanel.tsx | 865 +++++++++++++++++- .../src/components/TransactionHistory.tsx | 34 +- frontend/src/components/VaultPanel.tsx | 383 ++++++-- frontend/src/components/WUSDPanel.tsx | 78 +- frontend/src/config/contracts.ts | 211 +++++ frontend/src/config/wagmi.ts | 13 + frontend/src/hooks/useTransactionHistory.ts | 41 +- frontend/src/i18n/locales/en.json | 16 +- frontend/src/i18n/locales/zh.json | 16 +- frontend/vite.config.ts | 24 + 15 files changed, 1826 insertions(+), 179 deletions(-) create mode 100644 frontend/src/components/ErrorBoundary.tsx diff --git a/frontend/src/App.css b/frontend/src/App.css index 436bf7f..0cb1691 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -600,6 +600,36 @@ body { font-size: 14px; } +/* Redeem status banner - prominently displayed in sell tab */ +.redeem-status-banner { + padding: 12px 16px; + border-radius: 8px; + margin-bottom: 16px; + font-size: 14px; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.redeem-status-banner.success { + background: #e8f5e9; + color: #2e7d32; + border: 1px solid #a5d6a7; +} + +.redeem-status-banner.warning { + background: #fff3e0; + color: #e65100; + border: 1px solid #ffcc80; +} + +.redeem-status-banner .time-remaining { + font-size: 13px; + margin-left: 4px; + opacity: 0.9; +} + /* Test result box */ .test-result-box { padding: 10px 14px; @@ -887,3 +917,87 @@ body { .btn-link:hover { color: #1565c0; } + +/* Error Boundary */ +.error-boundary { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: #f5f5f5; + padding: 20px; +} + +.error-content { + background: white; + padding: 40px; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + max-width: 500px; + text-align: center; +} + +.error-content h2 { + color: #d32f2f; + margin-bottom: 16px; + font-size: 24px; +} + +.error-message { + background: #ffebee; + color: #c62828; + padding: 12px; + border-radius: 6px; + font-family: monospace; + font-size: 13px; + margin-bottom: 20px; + word-break: break-word; +} + +.error-hint { + text-align: left; + background: #f5f5f5; + padding: 16px; + border-radius: 6px; + margin-bottom: 20px; +} + +.error-hint p { + font-weight: 600; + margin-bottom: 8px; + color: #333; +} + +.error-hint ul { + margin-left: 20px; + color: #666; +} + +.error-hint li { + margin-bottom: 4px; +} + +.error-actions { + display: flex; + gap: 12px; + justify-content: center; + margin-bottom: 20px; +} + +.error-tip { + color: #666; + font-size: 13px; + font-style: italic; +} + +/* Connect Buttons */ +.connect-buttons { + display: flex; + gap: 8px; + align-items: center; +} + +.connect-buttons .btn-sm { + padding: 6px 10px; + font-size: 11px; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d6e4f54..395f4a6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,6 +11,7 @@ import { FactoryPanel } from './components/FactoryPanel' import { LPPanel } from './components/LPPanel' import { ToastProvider } from './components/Toast' import { TransactionProvider } from './context/TransactionContext' +import { ErrorBoundary } from './components/ErrorBoundary' import './App.css' type Tab = 'wusd' | 'vault' | 'factory' | 'lp' @@ -73,15 +74,17 @@ function AppContent() { function App() { return ( - - - - - - - - - + + + + + + + + + + + ) } diff --git a/frontend/src/components/ConnectButton.tsx b/frontend/src/components/ConnectButton.tsx index c1ed55f..592a3ff 100644 --- a/frontend/src/components/ConnectButton.tsx +++ b/frontend/src/components/ConnectButton.tsx @@ -2,6 +2,26 @@ import { useTranslation } from 'react-i18next' import { useWeb3Modal } from '@web3modal/wagmi/react' import { useAccount, useDisconnect } from 'wagmi' +// 清理 WalletConnect 相关缓存和损坏的数据 +const clearAllCache = () => { + const keysToRemove: string[] = [] + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key && ( + key.startsWith('wc@') || + key.startsWith('wagmi') || + key.startsWith('@w3m') || + key.includes('walletconnect') || + key.includes('WalletConnect') || + key === 'yt_asset_tx_history' // 也清除可能损坏的交易历史 + )) { + keysToRemove.push(key) + } + } + keysToRemove.forEach(key => localStorage.removeItem(key)) + sessionStorage.clear() +} + export function ConnectButton() { const { t } = useTranslation() const { open } = useWeb3Modal() @@ -12,11 +32,23 @@ export function ConnectButton() { return `${addr.slice(0, 6)}...${addr.slice(-4)}` } + // 断开连接并清理缓存 + const handleDisconnect = () => { + disconnect() + clearAllCache() + } + + // 重置连接(清理缓存后刷新) + const handleReset = () => { + clearAllCache() + window.location.reload() + } + if (isConnected && address) { return (
{formatAddress(address)} -
@@ -24,8 +56,17 @@ export function ConnectButton() { } return ( - +
+ + +
) } diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..ddd713f --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,100 @@ +import { Component } from 'react' +import type { ErrorInfo, ReactNode } from 'react' + +interface Props { + children: ReactNode +} + +interface State { + hasError: boolean + error: Error | null +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { hasError: false, error: null } + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo) + } + + handleReload = () => { + // Clear all potentially corrupted storage + try { + // Clear wagmi + localStorage.removeItem('wagmi.store') + localStorage.removeItem('wagmi.connected') + localStorage.removeItem('wagmi.wallet') + // Clear WalletConnect + const keysToRemove: string[] = [] + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + if (key && ( + key.startsWith('wc@') || + key.startsWith('wagmi') || + key.startsWith('@w3m') || + key.includes('walletconnect') || + key === 'yt_asset_tx_history' + )) { + keysToRemove.push(key) + } + } + keysToRemove.forEach(key => localStorage.removeItem(key)) + sessionStorage.clear() + } catch (e) { + console.error('Failed to clear storage:', e) + } + window.location.reload() + } + + handleReset = () => { + this.setState({ hasError: false, error: null }) + } + + render() { + if (this.state.hasError) { + return ( +
+
+

Something went wrong

+

+ {typeof this.state.error?.message === 'string' + ? this.state.error.message + : JSON.stringify(this.state.error?.message) || 'Unknown error'} +

+ +
+

Common causes:

+
    +
  • Multiple wallet extensions conflict
  • +
  • Network connection issues
  • +
  • Wallet state corruption
  • +
+
+ +
+ + +
+ +

+ Tip: If the problem persists, try disabling other wallet extensions and keep only one. +

+
+
+ ) + } + + return this.props.children + } +} diff --git a/frontend/src/components/FactoryPanel.tsx b/frontend/src/components/FactoryPanel.tsx index 5762f3b..427ff14 100644 --- a/frontend/src/components/FactoryPanel.tsx +++ b/frontend/src/components/FactoryPanel.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' import { parseUnits, formatUnits } from 'viem' -import { CONTRACTS, FACTORY_ABI } from '../config/contracts' +import { CONTRACTS, GAS_CONFIG, FACTORY_ABI } from '../config/contracts' export function FactoryPanel() { const { t } = useTranslation() @@ -47,7 +47,7 @@ export function FactoryPanel() { functionName: 'defaultHardCap', }) - const { writeContract, data: hash, isPending, reset } = useWriteContract() + const { writeContract, data: hash, isPending, reset, error: writeError, status: writeStatus } = useWriteContract() const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash, @@ -60,6 +60,15 @@ export function FactoryPanel() { } }, [isSuccess]) + // 处理写入错误,重置状态 + useEffect(() => { + if (writeError) { + console.error('Factory write error:', writeError) + // 重置状态,允许用户重新操作 + reset() + } + }, [writeError]) + const handleCreateVault = () => { const redemptionTimestamp = Math.floor(new Date(createForm.redemptionTime).getTime() / 1000) @@ -77,6 +86,9 @@ export function FactoryPanel() { parseUnits(createForm.initialWusdPrice, 18), parseUnits(createForm.initialYtPrice, 18), ], + gas: GAS_CONFIG.VERY_COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -90,6 +102,9 @@ export function FactoryPanel() { parseUnits(priceForm.wusdPrice, 18), parseUnits(priceForm.ytPrice, 18), ], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -104,6 +119,9 @@ export function FactoryPanel() { abi: FACTORY_ABI, functionName: 'updateVaultPrices', args: [testVault as `0x${string}`, parseUnits('1', 18), parseUnits('1', 18)], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'set_manager_not_owner': @@ -112,6 +130,9 @@ export function FactoryPanel() { abi: FACTORY_ABI, functionName: 'setVaultManager', args: [testVault as `0x${string}`, address as `0x${string}`], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break } @@ -122,6 +143,9 @@ export function FactoryPanel() { const isOwner = address && owner && address.toLowerCase() === owner.toLowerCase() + // 计算按钮是否应该禁用 - 排除错误状态 + const isProcessing = (isPending || isConfirming) && writeStatus !== 'error' + if (!isConnected) { return (
@@ -251,10 +275,10 @@ export function FactoryPanel() {
@@ -299,10 +323,10 @@ export function FactoryPanel() { @@ -328,7 +352,7 @@ export function FactoryPanel() { OwnableUnauthorizedAccount

{t('test.updatePriceNotOwnerDesc')}

- @@ -339,7 +363,7 @@ export function FactoryPanel() { OwnableUnauthorizedAccount

{t('test.setManagerNotOwnerDesc')}

- diff --git a/frontend/src/components/LPPanel.tsx b/frontend/src/components/LPPanel.tsx index f4b21f4..ccb3f73 100644 --- a/frontend/src/components/LPPanel.tsx +++ b/frontend/src/components/LPPanel.tsx @@ -4,10 +4,14 @@ import { useAccount, useReadContract, useWriteContract, useWaitForTransactionRec import { parseUnits, formatUnits } from 'viem' import { CONTRACTS, + GAS_CONFIG, YT_REWARD_ROUTER_ABI, YT_LP_TOKEN_ABI, YT_POOL_MANAGER_ABI, - WUSD_ABI + YT_VAULT_ABI, + YT_PRICE_FEED_ABI, + WUSD_ABI, + YT_ASSET_VAULT_ABI } from '../config/contracts' import { useTransactions } from '../context/TransactionContext' import type { TransactionType } from '../context/TransactionContext' @@ -61,6 +65,28 @@ export function LPPanel() { }) const [activeTab, setActiveTab] = useState<'add' | 'remove' | 'swap'>('add') const [showBoundaryTest, setShowBoundaryTest] = useState(false) + const [showAdminConfig, setShowAdminConfig] = useState(false) + const [priceConfigForm, setPriceConfigForm] = useState<{ + token: string + price: string + spreadBps: string + }>({ + token: CONTRACTS.VAULTS.YT_A, + price: '1', // 价格,单位是 USD + spreadBps: '20', // 价差,单位是 bps (20 = 0.2%) + }) + const [vaultConfigForm, setVaultConfigForm] = useState<{ + token: string + tokenDecimals: string + tokenWeight: string + maxUsdyAmount: string + }>({ + token: CONTRACTS.VAULTS.YT_A, + tokenDecimals: '18', + tokenWeight: '10000', // 权重,10000 = 100% + maxUsdyAmount: '1000000', // 最大限额 USDY + }) + const [wusdPriceSourceInput, setWusdPriceSourceInput] = useState(CONTRACTS.WUSD) // Read pool data const { data: ytLPBalance, refetch: refetchBalance } = useReadContract({ @@ -76,6 +102,35 @@ export function LPPanel() { functionName: 'totalSupply', }) + // 读取用户各代币余额 + const { data: ytABalance } = useReadContract({ + address: CONTRACTS.VAULTS.YT_A, + abi: WUSD_ABI, + functionName: 'balanceOf', + args: address ? [address] : undefined, + }) + + const { data: ytBBalance } = useReadContract({ + address: CONTRACTS.VAULTS.YT_B, + abi: WUSD_ABI, + functionName: 'balanceOf', + args: address ? [address] : undefined, + }) + + const { data: ytCBalance } = useReadContract({ + address: CONTRACTS.VAULTS.YT_C, + abi: WUSD_ABI, + functionName: 'balanceOf', + args: address ? [address] : undefined, + }) + + const { data: wusdBalanceLP } = useReadContract({ + address: CONTRACTS.WUSD, + abi: WUSD_ABI, + functionName: 'balanceOf', + args: address ? [address] : undefined, + }) + const { data: ytLPPrice } = useReadContract({ address: CONTRACTS.YT_REWARD_ROUTER, abi: YT_REWARD_ROUTER_ABI, @@ -89,12 +144,186 @@ export function LPPanel() { args: [true], }) - // Gov not currently used but may be needed later - // const { data: gov } = useReadContract({ - // address: CONTRACTS.YT_REWARD_ROUTER, - // abi: YT_REWARD_ROUTER_ABI, - // functionName: 'gov', - // }) + // 检查各代币是否被 LP 池白名单 + const { data: isYtAWhitelisted } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'whitelistedTokens', + args: [CONTRACTS.VAULTS.YT_A], + }) + + const { data: isYtBWhitelisted } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'whitelistedTokens', + args: [CONTRACTS.VAULTS.YT_B], + }) + + const { data: isYtCWhitelisted } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'whitelistedTokens', + args: [CONTRACTS.VAULTS.YT_C], + }) + + const { data: isWusdWhitelisted } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'whitelistedTokens', + args: [CONTRACTS.WUSD], + }) + + // 读取当前选中代币在池子中的数量 + const { data: poolAmountYtA } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'poolAmounts', + args: [CONTRACTS.VAULTS.YT_A], + }) + + // 读取当前选中代币的价格 + const { data: tokenPriceYtA } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getMinPrice', + args: [CONTRACTS.VAULTS.YT_A], + }) + + // 读取代币权重 + const { data: tokenWeightYtA } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'tokenWeights', + args: [CONTRACTS.VAULTS.YT_A], + }) + + // 读取代币精度配置 + const { data: tokenDecimalsYtA } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'tokenDecimals', + args: [CONTRACTS.VAULTS.YT_A], + }) + + // 读取最大 USDY 限额 + const { data: maxUsdyAmountYtA } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'maxUsdyAmounts', + args: [CONTRACTS.VAULTS.YT_A], + }) + + // 读取 YTVault 管理员地址 + const { data: vaultGov } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'gov', + }) + + // 读取 YTVault 的 priceFeed 地址 + const { data: vaultPriceFeed } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'priceFeed', + }) + + // 直接从 YTPriceFeed 读取 YT-A 价格 (getMinPrice) + const { data: priceFeedYtAPrice } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'getMinPrice', + args: [CONTRACTS.VAULTS.YT_A], + }) + + // 直接从 YTPriceFeed 读取 YT-A lastPrice (原始设置的价格) + const { data: priceFeedYtALastPrice } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'lastPrice', + args: [CONTRACTS.VAULTS.YT_A], + }) + + // 直接从 YTPriceFeed 读取 YT-A spreadBasisPoints + const { data: priceFeedYtASpread } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'spreadBasisPoints', + args: [CONTRACTS.VAULTS.YT_A], + }) + + // 读取 YTPriceFeed 管理员地址 + const { data: priceFeedGov } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'gov', + }) + + // 读取 wusdPriceSource + const { data: wusdPriceSource } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'wusdPriceSource', + }) + + // 读取 maxPriceChangeBps + const { data: maxPriceChangeBps } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'maxPriceChangeBps', + }) + + // 直接读取 YT-A 合约的 ytPrice (旧的18位精度) + const { data: ytAContractYtPrice } = useReadContract({ + address: CONTRACTS.VAULTS.YT_A, + abi: YT_ASSET_VAULT_ABI, + functionName: 'ytPrice', + }) + + // 直接读取 YT-A 合约的 assetPrice (YTPriceFeed 期望的30位精度接口) + const { data: ytAContractAssetPrice } = useReadContract({ + address: CONTRACTS.VAULTS.YT_A, + abi: YT_ASSET_VAULT_ABI, + functionName: 'assetPrice', + }) + + // 读取 WUSD 的 getMinPrice + const { data: wusdGetMinPrice } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'getMinPrice', + args: [CONTRACTS.WUSD], + }) + + // 使用 getPriceInfo 获取完整价格信息 + const { data: ytAPriceInfo } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'getPriceInfo', + args: [CONTRACTS.VAULTS.YT_A], + }) + + // 读取 YTPriceFeed 配置的 WUSD 地址 + const { data: priceFeedWusdAddress } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'WUSD', + }) + + // 读取选中代币在 YTPriceFeed 中的价格 + const { data: selectedTokenPrice } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'getMinPrice', + args: [priceConfigForm.token as `0x${string}`], + }) + + // 读取选中代币的价差 + const { data: selectedTokenSpread } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'spreadBasisPoints', + args: [priceConfigForm.token as `0x${string}`], + }) const { data: cooldownDuration } = useReadContract({ address: CONTRACTS.YT_POOL_MANAGER, @@ -125,15 +354,36 @@ export function LPPanel() { args: address ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined, }) - const { writeContract, data: hash, isPending, error: writeError, reset } = useWriteContract() + const { writeContract, data: hash, isPending, error: writeError, reset, status: writeStatus } = useWriteContract() const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({ hash, }) + // 超时自动重置机制 - 解决WalletConnect通信超时问题 + useEffect(() => { + let timeoutId: NodeJS.Timeout | null = null + if (isPending && !hash) { + // 如果30秒内没有响应,自动重置 + timeoutId = setTimeout(() => { + console.log('Transaction timeout, resetting state...') + if (pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { status: 'failed', error: 'Transaction timeout' }) + pendingTxRef.current = null + } + showToast('error', t('toast.txTimeout') || 'Transaction timeout') + reset() + }, 30000) + } + return () => { + if (timeoutId) clearTimeout(timeoutId) + } + }, [isPending, hash]) + // Handle transaction submission useEffect(() => { - if (hash && pendingTxRef.current) { + // 验证 hash 是有效的交易哈希字符串 + if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) { updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' }) showToast('info', t('toast.txSubmitted')) } @@ -162,10 +412,12 @@ export function LPPanel() { if (isError && pendingTxRef.current) { updateTransaction(pendingTxRef.current.id, { status: 'failed', - error: txError?.message || 'Transaction failed' + error: typeof txError?.message === 'string' ? txError.message : 'Transaction failed' }) showToast('error', t('toast.txFailed')) pendingTxRef.current = null + // 重置状态,允许用户重新操作 + reset() } }, [isError]) @@ -178,16 +430,31 @@ export function LPPanel() { updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg }) pendingTxRef.current = null } + // 重置状态,允许用户重新操作 + reset() } }, [writeError]) const parseError = (error: any): string => { - const msg = error?.message || error?.toString() || 'Unknown error' + // 确保获取字符串形式的错误信息 + let msg = 'Unknown error' + if (typeof error === 'string') { + msg = error + } else if (error?.shortMessage) { + msg = error.shortMessage + } else if (typeof error?.message === 'string') { + msg = error.message + } else if (error?.message) { + msg = JSON.stringify(error.message) + } else if (error) { + try { msg = JSON.stringify(error) } catch { msg = String(error) } + } + if (msg.includes('User rejected') || msg.includes('user rejected')) { return t('toast.userRejected') } - if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance')) { - return t('toast.insufficientBalance') + if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance') || msg.includes('less than block base fee')) { + return t('toast.insufficientBalance') + ' (Gas)' } if (msg.includes('CooldownNotPassed')) { return t('lp.cooldownNotPassed') @@ -220,6 +487,9 @@ export function LPPanel() { abi: WUSD_ABI, functionName: 'approve', args: [CONTRACTS.YT_REWARD_ROUTER, parseUnits('1000000000', 18)], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -232,6 +502,9 @@ export function LPPanel() { abi: YT_LP_TOKEN_ABI, functionName: 'approve', args: [CONTRACTS.YT_REWARD_ROUTER, parseUnits('1000000000', 18)], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -244,12 +517,15 @@ export function LPPanel() { const minUsdy = BigInt(0) const minYtLP = BigInt(0) - recordTx('buy', addLiquidityForm.amount, getTokenSymbol(addLiquidityForm.token)) + recordTx('addLiquidity', addLiquidityForm.amount, getTokenSymbol(addLiquidityForm.token)) writeContract({ address: CONTRACTS.YT_REWARD_ROUTER, abi: YT_REWARD_ROUTER_ABI, functionName: 'addLiquidity', args: [addLiquidityForm.token as `0x${string}`, amount, minUsdy, minYtLP], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -260,12 +536,15 @@ export function LPPanel() { // For simplicity, set minOut to 0 const minOut = BigInt(0) - recordTx('sell', removeLiquidityForm.amount, 'ytLP') + recordTx('removeLiquidity', removeLiquidityForm.amount, 'ytLP') writeContract({ address: CONTRACTS.YT_REWARD_ROUTER, abi: YT_REWARD_ROUTER_ABI, functionName: 'removeLiquidity', args: [removeLiquidityForm.token as `0x${string}`, ytLPAmount, minOut, address], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -276,7 +555,7 @@ export function LPPanel() { // For simplicity, set minOut to 0 const minOut = BigInt(0) - recordTx('buy', swapForm.amount, getTokenSymbol(swapForm.tokenIn)) + recordTx('swap', swapForm.amount, `${getTokenSymbol(swapForm.tokenIn)} -> ${getTokenSymbol(swapForm.tokenOut)}`) writeContract({ address: CONTRACTS.YT_REWARD_ROUTER, abi: YT_REWARD_ROUTER_ABI, @@ -288,6 +567,9 @@ export function LPPanel() { minOut, address ], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -296,6 +578,78 @@ export function LPPanel() { return token?.symbol || 'Unknown' } + // 设置代币价格 (管理员功能) + const handleSetPrice = () => { + if (!address || !priceConfigForm.price) return + // 价格精度是 30 位小数,例如 $1.00 = 1e30 + const priceValue = parseUnits(priceConfigForm.price, 30) + recordTx('test', priceConfigForm.price, 'SetPrice') + writeContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'forceUpdatePrice', + args: [priceConfigForm.token as `0x${string}`, priceValue], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 设置代币价差 (管理员功能) + const handleSetSpread = () => { + if (!address || !priceConfigForm.spreadBps) return + const spreadValue = BigInt(priceConfigForm.spreadBps) + recordTx('test', priceConfigForm.spreadBps, 'SetSpread') + writeContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'setSpreadBasisPoints', + args: [priceConfigForm.token as `0x${string}`, spreadValue], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 设置 YTVault 白名单代币配置 (管理员功能) + const handleSetWhitelistedToken = () => { + if (!address) return + const decimals = BigInt(vaultConfigForm.tokenDecimals) + const weight = BigInt(vaultConfigForm.tokenWeight) + const maxUsdy = parseUnits(vaultConfigForm.maxUsdyAmount, 18) + recordTx('test', vaultConfigForm.maxUsdyAmount, 'SetWhitelistedToken') + writeContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'setWhitelistedToken', + args: [ + vaultConfigForm.token as `0x${string}`, + decimals, + weight, + maxUsdy, + false, // isStable + ], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 设置 WUSD 价格源 (管理员功能) + const handleSetWusdPriceSource = () => { + if (!address || !wusdPriceSourceInput) return + recordTx('test', wusdPriceSourceInput, 'SetWusdPriceSource') + writeContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'setWusdPriceSource', + args: [wusdPriceSourceInput as `0x${string}`], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + // Boundary test function const runBoundaryTest = (testType: string) => { if (!address) return @@ -309,6 +663,9 @@ export function LPPanel() { abi: YT_REWARD_ROUTER_ABI, functionName: 'addLiquidity', args: [CONTRACTS.VAULTS.YT_A, BigInt(0), BigInt(0), BigInt(0)], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'add_exceed_balance': @@ -318,6 +675,9 @@ export function LPPanel() { abi: YT_REWARD_ROUTER_ABI, functionName: 'addLiquidity', args: [CONTRACTS.VAULTS.YT_A, parseUnits('999999999', 18), BigInt(0), BigInt(0)], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'remove_zero': @@ -327,6 +687,9 @@ export function LPPanel() { abi: YT_REWARD_ROUTER_ABI, functionName: 'removeLiquidity', args: [CONTRACTS.VAULTS.YT_A, BigInt(0), BigInt(0), address], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'remove_exceed_balance': @@ -337,6 +700,9 @@ export function LPPanel() { abi: YT_REWARD_ROUTER_ABI, functionName: 'removeLiquidity', args: [CONTRACTS.VAULTS.YT_A, exceedAmount, BigInt(0), address], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'remove_high_minout': @@ -349,6 +715,9 @@ export function LPPanel() { abi: YT_REWARD_ROUTER_ABI, functionName: 'removeLiquidity', args: [CONTRACTS.VAULTS.YT_A, testAmount, parseUnits('999999999', 18), address], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'swap_zero': @@ -358,6 +727,9 @@ export function LPPanel() { abi: YT_REWARD_ROUTER_ABI, functionName: 'swapYT', args: [CONTRACTS.VAULTS.YT_A, CONTRACTS.VAULTS.YT_B, BigInt(0), BigInt(0), address], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'swap_same_token': @@ -367,6 +739,9 @@ export function LPPanel() { abi: YT_REWARD_ROUTER_ABI, functionName: 'swapYT', args: [CONTRACTS.VAULTS.YT_A, CONTRACTS.VAULTS.YT_A, parseUnits('1', 18), BigInt(0), address], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'swap_exceed_balance': @@ -376,6 +751,9 @@ export function LPPanel() { abi: YT_REWARD_ROUTER_ABI, functionName: 'swapYT', args: [CONTRACTS.VAULTS.YT_A, CONTRACTS.VAULTS.YT_B, parseUnits('999999999', 18), BigInt(0), address], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break } @@ -397,9 +775,12 @@ export function LPPanel() { } const needsApproval = () => { - if (!tokenAllowance || !addLiquidityForm.amount) return false + // 如果没有输入金额,不需要显示授权按钮 + if (!addLiquidityForm.amount) return false try { const amount = parseUnits(addLiquidityForm.amount, 18) + // 如果 tokenAllowance 为 undefined(数据还在加载)或为 0n,需要授权 + if (tokenAllowance === undefined || tokenAllowance === null) return true return tokenAllowance < amount } catch { return false @@ -407,15 +788,21 @@ export function LPPanel() { } const needsYtLPApproval = () => { - if (!ytLPAllowance || !removeLiquidityForm.amount) return false + // 如果没有输入金额,不需要显示授权按钮 + if (!removeLiquidityForm.amount) return false try { const amount = parseUnits(removeLiquidityForm.amount, 18) + // 如果 ytLPAllowance 为 undefined(数据还在加载)或为 0n,需要授权 + if (ytLPAllowance === undefined || ytLPAllowance === null) return true return ytLPAllowance < amount } catch { return false } } + // 计算按钮是否应该禁用 - 排除错误状态 + const isProcessing = (isPending || isConfirming) && writeStatus !== 'error' + if (!isConnected) { return (
@@ -461,6 +848,142 @@ export function LPPanel() {
+ {/* 用户代币余额及白名单状态 */} +
+

{t('lp.yourTokenBalances')}

+
+
+ YT-A {isYtAWhitelisted ? '✓' : } + {ytABalance ? formatUnits(ytABalance, 18) : '0'} +
+
+ YT-B {isYtBWhitelisted ? '✓' : } + {ytBBalance ? formatUnits(ytBBalance, 18) : '0'} +
+
+ YT-C {isYtCWhitelisted ? '✓' : } + {ytCBalance ? formatUnits(ytCBalance, 18) : '0'} +
+
+ WUSD {isWusdWhitelisted ? '✓' : } + {wusdBalanceLP ? formatUnits(wusdBalanceLP, 18) : '0'} +
+
+

+ ✓ = {t('lp.whitelisted')} | = {t('lp.notWhitelisted')} +

+
+ + {/* 池子状态调试信息 */} +
+

池子状态 (调试)

+
+
+ YT-A 池中数量: + {poolAmountYtA !== undefined ? formatUnits(poolAmountYtA, 18) : '加载中...'} +
+
+ YT-A 代币价格: + {tokenPriceYtA !== undefined ? formatUnits(tokenPriceYtA, 30) : '加载中...'} +
+
+ YT-A 权重: + {tokenWeightYtA !== undefined ? tokenWeightYtA.toString() : '加载中...'} +
+
+ YT-A 白名单: + + {isYtAWhitelisted === undefined ? '加载中...' : (isYtAWhitelisted ? '是 ✓' : '否 ✗')} + +
+
+ YT-A tokenDecimals: + 0n ? '#4caf50' : '#f44336' }}> + {tokenDecimalsYtA !== undefined ? tokenDecimalsYtA.toString() : '加载中...'} + {tokenDecimalsYtA !== undefined && tokenDecimalsYtA === 0n && ' (未配置!)'} + +
+
+ YT-A maxUsdyAmounts: + 0n ? '#4caf50' : '#f44336' }}> + {maxUsdyAmountYtA !== undefined ? formatUnits(maxUsdyAmountYtA, 18) : '加载中...'} + {maxUsdyAmountYtA !== undefined && maxUsdyAmountYtA === 0n && ' (未配置!)'} + +
+
+
+

+ YTVault Gov: {vaultGov || '加载中...'} +

+

+ YTVault priceFeed: {vaultPriceFeed || '加载中...'} + {vaultPriceFeed && CONTRACTS.YT_PRICE_FEED.toLowerCase() !== (vaultPriceFeed as string).toLowerCase() && ( + + [不匹配! 应为: {CONTRACTS.YT_PRICE_FEED}] + + )} +

+

+ YTPriceFeed.lastPrice(YT-A): {priceFeedYtALastPrice !== undefined ? `$${formatUnits(priceFeedYtALastPrice, 30)}` : '加载中...'} + {priceFeedYtALastPrice !== undefined && priceFeedYtALastPrice === 0n && ( + [价格为0! 需要 forceUpdatePrice] + )} +

+

+ YT-A合约.ytPrice: {ytAContractYtPrice !== undefined ? `$${formatUnits(ytAContractYtPrice, 18)}` : '加载中...'} + {ytAContractYtPrice !== undefined && ytAContractYtPrice === 0n && ( + [价格为0! 这是根本原因!] + )} +

+

+ YTPriceFeed.spreadBasisPoints(YT-A): {priceFeedYtASpread !== undefined ? `${priceFeedYtASpread.toString()} bps (${Number(priceFeedYtASpread) / 100}%)` : '加载中...'} +

+

+ YTPriceFeed.wusdPriceSource: {wusdPriceSource || '加载中...'} +

+

+ YTPriceFeed.maxPriceChangeBps: {maxPriceChangeBps !== undefined ? `${maxPriceChangeBps.toString()} bps (${Number(maxPriceChangeBps) / 100}%)` : '加载中...'} +

+

+ YTPriceFeed.getMinPrice(YT-A): {priceFeedYtAPrice !== undefined ? `$${formatUnits(priceFeedYtAPrice, 30)}` : '加载失败'} +

+
+
+

+ addLiquidity 失败诊断: +

+
    +
  • YT-A合约.ytPrice (18位): {ytAContractYtPrice !== undefined ? `$${formatUnits(ytAContractYtPrice, 18)}` : '加载失败'}
  • +
  • + YT-A合约.assetPrice (30位): {ytAContractAssetPrice !== undefined ? `$${formatUnits(ytAContractAssetPrice, 30)} OK` : '不存在! YT合约需要实现assetPrice()接口'} +
  • +
  • YTPriceFeed.lastPrice(YT-A): {priceFeedYtALastPrice !== undefined ? (priceFeedYtALastPrice === 0n ? '0 [需要设置!]' : `$${formatUnits(priceFeedYtALastPrice, 30)} OK`) : '加载失败'}
  • +
  • YTPriceFeed.getMinPrice(YT-A): {priceFeedYtAPrice !== undefined ? `$${formatUnits(priceFeedYtAPrice, 30)} OK` : '加载失败'}
  • +
  • YTPriceFeed.getMinPrice(WUSD): {wusdGetMinPrice !== undefined ? `$${formatUnits(wusdGetMinPrice, 30)} OK` : '加载失败'}
  • +
  • YTVault.getMinPrice(YT-A): {tokenPriceYtA !== undefined ? (tokenPriceYtA === 0n ? '0' : `$${formatUnits(tokenPriceYtA, 30)} OK`) : '加载失败'}
  • +
  • YTPriceFeed.WUSD: {priceFeedWusdAddress ? {priceFeedWusdAddress as string} : '加载中...'}
  • +
  • WUSD地址匹配: {priceFeedWusdAddress && CONTRACTS.WUSD.toLowerCase() === (priceFeedWusdAddress as string).toLowerCase() ? 'OK' : `不匹配! 应为: ${CONTRACTS.WUSD}`}
  • +
  • priceFeed 地址匹配: {vaultPriceFeed && CONTRACTS.YT_PRICE_FEED.toLowerCase() === (vaultPriceFeed as string).toLowerCase() ? 'OK' : '不匹配'}
  • +
+
+ + {/* getPriceInfo 完整诊断 */} + {ytAPriceInfo && ( +
+

+ YTPriceFeed.getPriceInfo(YT-A): +

+
    +
  • currentPrice: ${formatUnits((ytAPriceInfo as any)[0], 30)}
  • +
  • cachedPrice: ${formatUnits((ytAPriceInfo as any)[1], 30)}
  • +
  • maxPrice: ${formatUnits((ytAPriceInfo as any)[2], 30)}
  • +
  • minPrice: ${formatUnits((ytAPriceInfo as any)[3], 30)}
  • +
  • spread: {(ytAPriceInfo as any)[4]?.toString()} bps
  • +
+
+ )} +
+ {/* Tab Navigation */}
+ {/* 显示当前授权额度 */} +
+ 当前授权额度 ({getTokenSymbol(addLiquidityForm.token)}): + BigInt(0) ? '#4caf50' : '#f44336' }}> + {tokenAllowance !== undefined ? formatUnits(tokenAllowance, 18) : '加载中...'} + +
+ {needsApproval() ? ( ) : ( )} @@ -613,18 +1144,18 @@ export function LPPanel() { {needsYtLPApproval() ? ( ) : ( )} @@ -693,10 +1224,10 @@ export function LPPanel() { )} @@ -729,7 +1260,7 @@ export function LPPanel() { InvalidAmount

{t('lp.testAddZeroDesc')}

- @@ -740,7 +1271,7 @@ export function LPPanel() { ERC20InsufficientBalance

{t('lp.testAddExceedDesc')}

- @@ -752,7 +1283,7 @@ export function LPPanel() { InvalidAmount

{t('lp.testRemoveZeroDesc')}

- @@ -763,7 +1294,7 @@ export function LPPanel() { ERC20InsufficientBalance

{t('lp.testRemoveExceedDesc')}

- @@ -774,7 +1305,7 @@ export function LPPanel() { InsufficientOutput

{t('lp.testRemoveHighMinoutDesc')}

- @@ -786,7 +1317,7 @@ export function LPPanel() { InvalidAmount

{t('lp.testSwapZeroDesc')}

- @@ -797,7 +1328,7 @@ export function LPPanel() { MaySucceed

{t('lp.testSwapSameDesc')}

- @@ -808,7 +1339,7 @@ export function LPPanel() { ERC20InsufficientBalance

{t('lp.testSwapExceedDesc')}

- @@ -817,6 +1348,268 @@ export function LPPanel() { )} + {/* 管理员配置区域 - YTPriceFeed */} +
+
setShowAdminConfig(!showAdminConfig)} style={{ cursor: 'pointer' }}> +

管理员配置 (YTPriceFeed) {showAdminConfig ? '[-]' : '[+]'}

+
+ + {showAdminConfig && ( + <> +
+

+ 注意: 这些是管理员功能,需要 Gov 权限才能执行 +

+

+ PriceFeed Gov: {priceFeedGov || '加载中...'} +

+

+ 你的地址: {address || '未连接'} +

+

+ {address && priceFeedGov && address.toLowerCase() === (priceFeedGov as string).toLowerCase() + ? '✓ 你是管理员,可以执行配置' + : '✗ 你不是管理员,配置可能会失败'} +

+
+ +
+
+ + +
+ +
+ +
+ {selectedTokenPrice !== undefined + ? `$${formatUnits(selectedTokenPrice, 30)}` + : '加载中... (可能未配置)'} +
+
+ +
+ +
+ {selectedTokenSpread !== undefined + ? `${selectedTokenSpread.toString()} bps (${Number(selectedTokenSpread) / 100}%)` + : '加载中...'} +
+
+
+ +
+

设置价格

+
+
+ + setPriceConfigForm({ ...priceConfigForm, price: e.target.value })} + placeholder="1.0" + className="input" + step="0.01" + /> +

+ 例如: 1 = $1.00, 1.05 = $1.05 +

+
+
+ +
+
+ +

设置价差

+
+
+ + setPriceConfigForm({ ...priceConfigForm, spreadBps: e.target.value })} + placeholder="20" + className="input" + /> +

+ 例如: 20 = 0.2%, 50 = 0.5% +

+
+
+ +
+
+
+ + {/* YTVault 配置 */} +
+

YTVault 代币配置

+
+

+ YTVault Gov: {vaultGov || '加载中...'} +

+

+ {address && vaultGov && address.toLowerCase() === (vaultGov as string).toLowerCase() + ? '✓ 你是 YTVault 管理员' + : '✗ 你不是 YTVault 管理员'} +

+
+ +
+
+ + +
+ +
+ + setVaultConfigForm({ ...vaultConfigForm, tokenDecimals: e.target.value })} + placeholder="18" + className="input" + /> +

+ 通常为 18 (标准 ERC20) +

+
+ +
+ + setVaultConfigForm({ ...vaultConfigForm, tokenWeight: e.target.value })} + placeholder="10000" + className="input" + /> +

+ 权重 (如 10000 = 100%) +

+
+ +
+ + setVaultConfigForm({ ...vaultConfigForm, maxUsdyAmount: e.target.value })} + placeholder="1000000" + className="input" + /> +

+ 最大 USDY 限额 (如 1000000) +

+
+
+ + +
+ + {/* WUSD 价格源配置 */} +
+

YTPriceFeed WUSD 价格源配置

+
+

+ 当前 wusdPriceSource: {wusdPriceSource || '加载中...'} +

+

+ 配置的 WUSD 地址: {priceFeedWusdAddress || '加载中...'} +

+

+ {wusdPriceSource && CONTRACTS.WUSD.toLowerCase() === (wusdPriceSource as string).toLowerCase() + ? '✓ wusdPriceSource 配置正确' + : '✗ wusdPriceSource 配置可能有误,应该设置为 WUSD 合约地址'} +

+
+ +
+
+ + setWusdPriceSourceInput(e.target.value)} + placeholder={CONTRACTS.WUSD} + className="input" + style={{ fontSize: '12px' }} + /> +

+ 通常应设置为 WUSD 合约地址: {CONTRACTS.WUSD} +

+
+
+ +
+
+
+ +
+

完整配置步骤:

+
    +
  1. YTPriceFeed: 设置 wusdPriceSource 为 WUSD 合约地址
  2. +
  3. YTPriceFeed: 设置价格 (forceUpdatePrice)
  4. +
  5. YTPriceFeed: 设置价差 (setSpreadBasisPoints) - 可选
  6. +
  7. YTVault: 设置白名单代币 (setWhitelistedToken) - 包含 decimals, weight, maxUsdyAmount
  8. +
  9. 配置完成后,刷新页面查看池子状态
  10. +
+

+ 重要: YT代币合约必须实现 assetPrice() 接口 (30位精度),供 YTPriceFeed 读取价格! +

+

+ 注意: 如果诊断显示 "assetPrice 不存在",说明 YT 合约没有实现该接口,需要升级合约 +

+
+ + )} +
+ {/* Transaction History */} diff --git a/frontend/src/components/TransactionHistory.tsx b/frontend/src/components/TransactionHistory.tsx index ec54fa6..29a3548 100644 --- a/frontend/src/components/TransactionHistory.tsx +++ b/frontend/src/components/TransactionHistory.tsx @@ -19,6 +19,9 @@ export function TransactionHistory({ transactions, onClear }: Props) { create_vault: 'Create Vault', update_price: 'Update Price', test: 'Test', + addLiquidity: 'Add Liquidity', + removeLiquidity: 'Remove Liquidity', + swap: 'Swap', } return labels[type] || type } @@ -36,7 +39,10 @@ export function TransactionHistory({ transactions, onClear }: Props) { return date.toLocaleString() } - const shortenHash = (hash: string) => { + const shortenHash = (hash: string | undefined | null) => { + if (!hash || typeof hash !== 'string' || hash.length < 14) { + return hash || '-' + } return `${hash.slice(0, 8)}...${hash.slice(-6)}` } @@ -60,7 +66,7 @@ export function TransactionHistory({ transactions, onClear }: Props) {
- {transactions.slice(0, 10).map((tx) => ( + {transactions.filter(tx => tx && tx.id).slice(0, 10).map((tx) => (
{getTypeLabel(tx.type)} @@ -68,18 +74,26 @@ export function TransactionHistory({ transactions, onClear }: Props) { {formatTime(tx.timestamp)}
- - {shortenHash(tx.hash)} - + {tx.hash && typeof tx.hash === 'string' && tx.hash.startsWith('0x') ? ( + + {shortenHash(tx.hash)} + + ) : ( + - + )} {tx.status === 'success' ? '✓' : tx.status === 'failed' ? '✗' : '...'}
- {tx.error &&
{tx.error}
} + {tx.error && ( +
+ {typeof tx.error === 'string' ? tx.error : JSON.stringify(tx.error)} +
+ )}
))}
diff --git a/frontend/src/components/VaultPanel.tsx b/frontend/src/components/VaultPanel.tsx index 7cc3116..eb839e2 100644 --- a/frontend/src/components/VaultPanel.tsx +++ b/frontend/src/components/VaultPanel.tsx @@ -1,14 +1,15 @@ import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' +import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' import { parseUnits, formatUnits, maxUint256 } from 'viem' -import { CONTRACTS, VAULT_ABI, WUSD_ABI, FACTORY_ABI } from '../config/contracts' +import { CONTRACTS, GAS_CONFIG, VAULT_ABI, WUSD_ABI, FACTORY_ABI } from '../config/contracts' import { useTransactions } from '../context/TransactionContext' import type { TransactionType } from '../context/TransactionContext' import { useToast } from './Toast' import { TransactionHistory } from './TransactionHistory' -const VAULTS = [ +// 默认金库列表(作为fallback) +const DEFAULT_VAULTS = [ { name: 'YT-A', address: CONTRACTS.VAULTS.YT_A }, { name: 'YT-B', address: CONTRACTS.VAULTS.YT_B }, { name: 'YT-C', address: CONTRACTS.VAULTS.YT_C }, @@ -19,7 +20,8 @@ export function VaultPanel() { const { address, isConnected } = useAccount() const { transactions, addTransaction, updateTransaction, clearHistory } = useTransactions() const { showToast } = useToast() - const [selectedVault, setSelectedVault] = useState(VAULTS[0]) + const [vaultList, setVaultList] = useState<{ name: string; address: string }[]>(DEFAULT_VAULTS) + const [selectedVault, setSelectedVault] = useState<{ name: string; address: string }>(DEFAULT_VAULTS[0]) const [buyAmount, setBuyAmount] = useState('') const [sellAmount, setSellAmount] = useState('') const [activeTab, setActiveTab] = useState<'buy' | 'sell'>('buy') @@ -27,6 +29,44 @@ export function VaultPanel() { const [testResult, setTestResult] = useState<{ type: 'success' | 'error' | 'pending', msg: string } | null>(null) const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null) + // 从合约动态读取所有金库地址 + const { data: allVaultsData } = useReadContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'getAllVaults', + }) + + // 为每个金库读取 symbol + const vaultSymbolContracts = allVaultsData?.map((vaultAddr) => ({ + address: vaultAddr as `0x${string}`, + abi: VAULT_ABI, + functionName: 'symbol' as const, + })) || [] + + const { data: vaultSymbolsData } = useReadContracts({ + contracts: vaultSymbolContracts, + }) + + // 当获取到金库地址和名称后,更新金库列表 + useEffect(() => { + if (allVaultsData && allVaultsData.length > 0) { + const newVaultList = allVaultsData.map((vaultAddr, index) => { + // 尝试从读取结果获取 symbol + const symbolResult = vaultSymbolsData?.[index] + const symbol = symbolResult?.status === 'success' ? symbolResult.result as string : `Vault ${index + 1}` + return { + name: symbol, + address: vaultAddr, + } + }) + setVaultList(newVaultList) + // 如果当前选中的金库不在新列表中,选择第一个 + if (!newVaultList.find(v => v.address.toLowerCase() === selectedVault.address.toLowerCase())) { + setSelectedVault(newVaultList[0]) + } + } + }, [allVaultsData, vaultSymbolsData]) + const { data: vaultInfo, refetch: refetchVaultInfo } = useReadContract({ address: selectedVault.address as `0x${string}`, abi: VAULT_ABI, @@ -105,15 +145,36 @@ export function VaultPanel() { functionName: 'owner', }) - const { writeContract, data: hash, isPending, reset, error: writeError } = useWriteContract() + const { writeContract, data: hash, isPending, reset, error: writeError, status: writeStatus } = useWriteContract() const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({ hash, }) + // 超时自动重置机制 - 解决WalletConnect通信超时问题 + useEffect(() => { + let timeoutId: NodeJS.Timeout | null = null + if (isPending && !hash) { + // 如果30秒内没有响应,自动重置 + timeoutId = setTimeout(() => { + console.log('Transaction timeout, resetting state...') + if (pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { status: 'failed', error: 'Transaction timeout' }) + pendingTxRef.current = null + } + showToast('error', t('toast.txTimeout') || 'Transaction timeout') + reset() + }, 30000) + } + return () => { + if (timeoutId) clearTimeout(timeoutId) + } + }, [isPending, hash]) + // 处理交易提交 useEffect(() => { - if (hash && pendingTxRef.current) { + // 验证 hash 是有效的交易哈希字符串 + if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) { updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' }) showToast('info', t('toast.txSubmitted')) } @@ -140,12 +201,15 @@ export function VaultPanel() { // 处理交易失败 useEffect(() => { if (isError && pendingTxRef.current) { + const errMsg = typeof txError?.message === 'string' ? txError.message : 'Transaction failed' updateTransaction(pendingTxRef.current.id, { status: 'failed', - error: txError?.message || 'Transaction failed' + error: errMsg }) showToast('error', t('toast.txFailed')) pendingTxRef.current = null + // 重置状态,允许用户重新操作 + reset() } }, [isError]) @@ -158,21 +222,59 @@ export function VaultPanel() { updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg }) pendingTxRef.current = null } + // 重置状态,允许用户重新操作 + reset() } }, [writeError]) // 解析错误信息 const parseError = (error: any): string => { - const msg = error?.message || error?.toString() || 'Unknown error' + // 确保获取字符串形式的错误信息 + let msg = 'Unknown error' + if (typeof error === 'string') { + msg = error + } else if (error?.shortMessage) { + msg = error.shortMessage + } else if (typeof error?.message === 'string') { + msg = error.message + } else if (error?.message) { + msg = JSON.stringify(error.message) + } else if (error) { + try { msg = JSON.stringify(error) } catch { msg = String(error) } + } + + // 忽略动态模块加载错误,尝试获取真正的合约错误 + if (msg.includes('Failed to fetch dynamically imported module') || msg.includes('ccip')) { + // 尝试从 cause 中获取真正的错误 + if (error?.cause) { + return parseError(error.cause) + } + // 尝试从 error.error 中获取 + if (error?.error) { + return parseError(error.error) + } + return 'Contract call failed' + } + if (msg.includes('User rejected') || msg.includes('user rejected')) { return t('toast.userRejected') } - if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance')) { - return t('toast.insufficientBalance') + if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance') || msg.includes('less than block base fee')) { + return t('toast.insufficientBalance') + ' (Gas)' } - // 提取合约错误 - const match = msg.match(/error[:\s]+(\w+)/i) - if (match) return match[1] + + // 提取合约自定义错误名称 (如 InvalidAmount, HardCapExceeded 等) + const customErrorMatch = msg.match(/reverted with custom error '(\w+)\(/i) + if (customErrorMatch) return customErrorMatch[1] + + // 提取 revert 原因 + const revertMatch = msg.match(/reverted with reason string '([^']+)'/i) + if (revertMatch) return revertMatch[1] + + // 提取一般错误名称 + const errorMatch = msg.match(/error[:\s]+(\w+)/i) + if (errorMatch) return errorMatch[1] + return msg.slice(0, 100) } @@ -197,6 +299,9 @@ export function VaultPanel() { abi: WUSD_ABI, functionName: 'approve', args: [selectedVault.address as `0x${string}`, parseUnits(buyAmount, 18)], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -208,6 +313,9 @@ export function VaultPanel() { abi: VAULT_ABI, functionName: 'depositYT', args: [parseUnits(buyAmount, 18)], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -219,83 +327,160 @@ export function VaultPanel() { abi: VAULT_ABI, functionName: 'withdrawYT', args: [parseUnits(sellAmount, 18)], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } + // 边界测试是否正在运行 + const [isTestRunning, setIsTestRunning] = useState(false) + const testTypeRef = useRef(null) + // 边界测试函数 const runBoundaryTest = (testType: string) => { setTestResult({ type: 'pending', msg: t('test.running') }) - try { - switch (testType) { - case 'buy_zero': - writeContract({ - address: selectedVault.address as `0x${string}`, - abi: VAULT_ABI, - functionName: 'depositYT', - args: [BigInt(0)], - }) - break - case 'sell_zero': - writeContract({ - address: selectedVault.address as `0x${string}`, - abi: VAULT_ABI, - functionName: 'withdrawYT', - args: [BigInt(0)], - }) - break - case 'buy_exceed_balance': - const exceedBuy = wusdBalance ? wusdBalance + parseUnits('999999', 18) : parseUnits('999999999', 18) - writeContract({ - address: selectedVault.address as `0x${string}`, - abi: VAULT_ABI, - functionName: 'depositYT', - args: [exceedBuy], - }) - break - case 'sell_exceed_balance': - const exceedSell = ytBalance ? ytBalance + parseUnits('999999', 18) : parseUnits('999999999', 18) - writeContract({ - address: selectedVault.address as `0x${string}`, - abi: VAULT_ABI, - functionName: 'withdrawYT', - args: [exceedSell], - }) - break - case 'buy_exceed_hardcap': - const hardCap = vaultInfo ? vaultInfo[4] : parseUnits('999999999', 18) - writeContract({ - address: selectedVault.address as `0x${string}`, - abi: VAULT_ABI, - functionName: 'depositYT', - args: [hardCap + parseUnits('1', 18)], - }) - break - case 'sell_in_lock': - writeContract({ - address: selectedVault.address as `0x${string}`, - abi: VAULT_ABI, - functionName: 'withdrawYT', - args: [parseUnits('1', 18)], - }) - break - case 'max_approve': - writeContract({ - address: CONTRACTS.WUSD, - abi: WUSD_ABI, - functionName: 'approve', - args: [selectedVault.address as `0x${string}`, maxUint256], - }) - break - } - } catch (err: any) { - setTestResult({ type: 'error', msg: err.message || 'Error' }) + setIsTestRunning(true) + testTypeRef.current = testType + + // 记录测试交易 + recordTx('test', undefined, testType) + + switch (testType) { + case 'buy_zero': + writeContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'depositYT', + args: [BigInt(0)], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + break + case 'sell_zero': + writeContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'withdrawYT', + args: [BigInt(0)], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + break + case 'buy_exceed_balance': + // 余额 + 999999,确保超过实际余额 + const exceedBuy = wusdBalance ? wusdBalance + parseUnits('999999', 18) : parseUnits('999999999', 18) + writeContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'depositYT', + args: [exceedBuy], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + break + case 'sell_exceed_balance': + // 余额 + 999999,确保超过实际余额 + const exceedSell = ytBalance ? ytBalance + parseUnits('999999', 18) : parseUnits('999999999', 18) + writeContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'withdrawYT', + args: [exceedSell], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + break + case 'buy_exceed_hardcap': + // 正确计算:(hardCap - totalAssets) + 额外金额 = 超过剩余空间 + // vaultInfo[0] = totalAssets, vaultInfo[4] = hardCap + const totalAssets = vaultInfo ? vaultInfo[0] : BigInt(0) + const hardCap = vaultInfo ? vaultInfo[4] : parseUnits('10000000', 18) + // 计算剩余空间,然后尝试存入比剩余空间更多的金额 + const remainingSpace = hardCap > totalAssets ? hardCap - totalAssets : BigInt(0) + // 存入 剩余空间 + 100,确保超过 hardCap + const exceedHardcapAmount = remainingSpace + parseUnits('100', 18) + writeContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'depositYT', + args: [exceedHardcapAmount], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + break + case 'sell_in_lock': + // 尝试在锁定期卖出 1 YT + writeContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'withdrawYT', + args: [parseUnits('1', 18)], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + break + case 'max_approve': + writeContract({ + address: CONTRACTS.WUSD, + abi: WUSD_ABI, + functionName: 'approve', + args: [selectedVault.address as `0x${string}`, maxUint256], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + break } } + // 边界测试结果处理 - 监听 writeError 更新 testResult + useEffect(() => { + if (isTestRunning && writeError) { + const errorMsg = parseError(writeError) + setTestResult({ type: 'error', msg: `[${testTypeRef.current}] ${errorMsg}` }) + setIsTestRunning(false) + testTypeRef.current = null + } + }, [writeError, isTestRunning]) + + // 边界测试成功处理 + useEffect(() => { + if (isTestRunning && isSuccess) { + setTestResult({ type: 'success', msg: `[${testTypeRef.current}] 交易成功 (预期应失败)` }) + setIsTestRunning(false) + testTypeRef.current = null + } + }, [isSuccess, isTestRunning]) + const needsApproval = buyAmount && wusdAllowance !== undefined ? parseUnits(buyAmount, 18) > wusdAllowance : false + // 格式化剩余时间 + const formatTimeRemaining = (seconds: number): string => { + if (seconds <= 0) return '0s' + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const mins = Math.floor((seconds % 3600) / 60) + const secs = seconds % 60 + const parts = [] + if (days > 0) parts.push(`${days}d`) + if (hours > 0) parts.push(`${hours}h`) + if (mins > 0) parts.push(`${mins}m`) + if (secs > 0 || parts.length === 0) parts.push(`${secs}s`) + return parts.join(' ') + } + + // 计算按钮是否应该禁用 - 排除错误状态 + const isProcessing = (isPending || isConfirming) && writeStatus !== 'error' + if (!isConnected) { return (
@@ -314,12 +499,12 @@ export function VaultPanel() { setSellAmount(e.target.value)} placeholder={t('vault.enterYtAmount')} className="input" + disabled={!canRedeem} />
{previewSellAmount && sellAmount && ( @@ -465,10 +662,10 @@ export function VaultPanel() { )} )} @@ -500,7 +697,7 @@ export function VaultPanel() { InvalidAmount

{t('test.buyZeroDesc')}

- @@ -511,7 +708,7 @@ export function VaultPanel() { InvalidAmount

{t('test.sellZeroDesc')}

- @@ -522,7 +719,7 @@ export function VaultPanel() { InsufficientWUSD

{t('test.buyExceedBalanceDesc')}

- @@ -533,7 +730,7 @@ export function VaultPanel() { InsufficientYTA

{t('test.sellExceedBalanceDesc')}

- @@ -544,7 +741,7 @@ export function VaultPanel() { HardCapExceeded

{t('test.buyExceedHardcapDesc')}

- @@ -555,14 +752,14 @@ export function VaultPanel() { StillInLockPeriod

{t('test.sellInLockDesc')}

-
-
diff --git a/frontend/src/components/WUSDPanel.tsx b/frontend/src/components/WUSDPanel.tsx index d24f032..d626557 100644 --- a/frontend/src/components/WUSDPanel.tsx +++ b/frontend/src/components/WUSDPanel.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' import { parseUnits, formatUnits } from 'viem' -import { CONTRACTS, WUSD_ABI } from '../config/contracts' +import { CONTRACTS, GAS_CONFIG, WUSD_ABI } from '../config/contracts' import { useTransactions } from '../context/TransactionContext' import type { TransactionType } from '../context/TransactionContext' import { useToast } from './Toast' @@ -36,15 +36,36 @@ export function WUSDPanel() { functionName: 'decimals', }) - const { writeContract, data: hash, isPending, error: writeError } = useWriteContract() + const { writeContract, data: hash, isPending, error: writeError, reset, status: writeStatus } = useWriteContract() const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({ hash, }) + // 超时自动重置机制 - 解决WalletConnect通信超时问题 + useEffect(() => { + let timeoutId: NodeJS.Timeout | null = null + if (isPending && !hash) { + // 如果30秒内没有响应,自动重置 + timeoutId = setTimeout(() => { + console.log('Transaction timeout, resetting state...') + if (pendingTxRef.current) { + updateTransaction(pendingTxRef.current.id, { status: 'failed', error: 'Transaction timeout' }) + pendingTxRef.current = null + } + showToast('error', t('toast.txTimeout') || 'Transaction timeout') + reset() + }, 30000) + } + return () => { + if (timeoutId) clearTimeout(timeoutId) + } + }, [isPending, hash]) + // 处理交易提交 useEffect(() => { - if (hash && pendingTxRef.current) { + // 验证 hash 是有效的交易哈希字符串 + if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) { updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' }) showToast('info', t('toast.txSubmitted')) } @@ -66,12 +87,15 @@ export function WUSDPanel() { // 处理交易失败 useEffect(() => { if (isError && pendingTxRef.current) { + const errMsg = typeof txError?.message === 'string' ? txError.message : 'Transaction failed' updateTransaction(pendingTxRef.current.id, { status: 'failed', - error: txError?.message || 'Transaction failed' + error: errMsg }) showToast('error', t('toast.txFailed')) pendingTxRef.current = null + // 重置状态,允许用户重新操作 + reset() } }, [isError]) @@ -84,16 +108,31 @@ export function WUSDPanel() { updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg }) pendingTxRef.current = null } + // 重置状态,允许用户重新操作 + reset() } }, [writeError]) const parseError = (error: any): string => { - const msg = error?.message || error?.toString() || 'Unknown error' + // 确保获取字符串形式的错误信息 + let msg = 'Unknown error' + if (typeof error === 'string') { + msg = error + } else if (error?.shortMessage) { + msg = error.shortMessage + } else if (typeof error?.message === 'string') { + msg = error.message + } else if (error?.message) { + msg = JSON.stringify(error.message) + } else if (error) { + try { msg = JSON.stringify(error) } catch { msg = String(error) } + } + if (msg.includes('User rejected') || msg.includes('user rejected')) { return t('toast.userRejected') } - if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance')) { - return t('toast.insufficientBalance') + if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance') || msg.includes('less than block base fee')) { + return t('toast.insufficientBalance') + ' (Gas)' } const match = msg.match(/error[:\s]+(\w+)/i) if (match) return match[1] @@ -111,6 +150,9 @@ export function WUSDPanel() { pendingTxRef.current = { id, type, amount } } + // 计算按钮是否应该禁用 - 排除错误状态 + const isProcessing = (isPending || isConfirming) && writeStatus !== 'error' + const handleMint = async () => { if (!address || !mintAmount || !decimals) return recordTx('mint', mintAmount, 'WUSD') @@ -119,6 +161,9 @@ export function WUSDPanel() { abi: WUSD_ABI, functionName: 'mint', args: [address, parseUnits(mintAmount, decimals)], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) } @@ -133,6 +178,9 @@ export function WUSDPanel() { abi: WUSD_ABI, functionName: 'mint', args: [address, BigInt(0)], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'burn_exceed': @@ -142,6 +190,9 @@ export function WUSDPanel() { abi: WUSD_ABI, functionName: 'burn', args: [address, exceedAmount], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break case 'mint_10000': @@ -151,6 +202,9 @@ export function WUSDPanel() { abi: WUSD_ABI, functionName: 'mint', args: [address, parseUnits('10000', decimals)], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, }) break } @@ -194,10 +248,10 @@ export function WUSDPanel() { {/* 边界测试区域 */} @@ -217,7 +271,7 @@ export function WUSDPanel() { MaySucceed

{t('test.mintZeroDesc')}

- @@ -228,14 +282,14 @@ export function WUSDPanel() { ERC20InsufficientBalance

{t('test.burnExceedDesc')}

-
-
diff --git a/frontend/src/config/contracts.ts b/frontend/src/config/contracts.ts index 15cd647..6c524f3 100644 --- a/frontend/src/config/contracts.ts +++ b/frontend/src/config/contracts.ts @@ -1,3 +1,47 @@ +// Gas配置 - 用于解决Arbitrum测试网gas预估问题 +// YT Asset Vault ABI - 用于读取 YT 代币本身的价格 +// 注意: YT代币需要实现 assetPrice() 接口(30位精度),供 YTPriceFeed 读取 +export const YT_ASSET_VAULT_ABI = [ + { + inputs: [], + name: 'ytPrice', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'wusdPrice', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + // YTPriceFeed 期望的接口:assetPrice() - 30位精度 + { + inputs: [], + name: 'assetPrice', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +] as const + +export const GAS_CONFIG = { + // 简单操作(approve等) + SIMPLE: BigInt(150000), + // 标准操作(单一合约调用) + STANDARD: BigInt(500000), + // 复杂操作(涉及多个合约) + COMPLEX: BigInt(1000000), + // 非常复杂的操作 + VERY_COMPLEX: BigInt(2000000), + // Gas价格设置 (单位: wei) + // maxFeePerGas: 最大愿意支付的gas价格 (30 Mwei, 比baseFee ~20M稍高) + MAX_FEE_PER_GAS: BigInt(30000000), + // maxPriorityFeePerGas: 给矿工的小费 + MAX_PRIORITY_FEE_PER_GAS: BigInt(1000000), +} + export const CONTRACTS = { FACTORY: '0x6DaB73519DbaFf23F36FEd24110e2ef5Cfc8aAC9' as const, WUSD: '0x939cf46F7A4d05da2a37213E7379a8b04528F590' as const, @@ -12,6 +56,7 @@ export const CONTRACTS = { YT_POOL_MANAGER: '0x14246886a1E1202cb6b5a2db793eF3359d536302' as const, YT_VAULT: '0x19982e5145ca5401A1084c0BF916c0E0bB343Af9' as const, USDY: '0x631Bd6834C50f6d2B07035c9253b4a19132E888c' as const, + YT_PRICE_FEED: '0x0f2d930EE73972132E3a36b7eD6F709Af6E5B879' as const, } export const FACTORY_ABI = [ @@ -689,6 +734,128 @@ export const YT_POOL_MANAGER_ABI = [ } ] as const +// YT PriceFeed ABI - for price configuration +export const YT_PRICE_FEED_ABI = [ + { + inputs: [ + { internalType: 'address', name: '_token', type: 'address' }, + { internalType: 'uint256', name: '_price', type: 'uint256' } + ], + name: 'forceUpdatePrice', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { internalType: 'address', name: '_token', type: 'address' }, + { internalType: 'uint256', name: '_spreadBasisPoints', type: 'uint256' } + ], + name: 'setSpreadBasisPoints', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: '_token', type: 'address' }], + name: 'getMinPrice', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: '_token', type: 'address' }], + name: 'getMaxPrice', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + // 获取完整价格信息 - 诊断用 + { + inputs: [{ internalType: 'address', name: '_token', type: 'address' }], + name: 'getPriceInfo', + outputs: [ + { internalType: 'uint256', name: 'currentPrice', type: 'uint256' }, + { internalType: 'uint256', name: 'cachedPrice', type: 'uint256' }, + { internalType: 'uint256', name: 'maxPrice', type: 'uint256' }, + { internalType: 'uint256', name: 'minPrice', type: 'uint256' }, + { internalType: 'uint256', name: 'spread', type: 'uint256' } + ], + stateMutability: 'view', + type: 'function' + }, + // WUSD 地址 + { + inputs: [], + name: 'WUSD', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'spreadBasisPoints', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'lastPrice', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'gov', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'wusdPriceSource', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'maxPriceChangeBps', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + // 设置 WUSD 价格源 + { + inputs: [{ internalType: 'address', name: '_wusdPriceSource', type: 'address' }], + name: 'setWusdPriceSource', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // 设置稳定币 + { + inputs: [ + { internalType: 'address', name: '_token', type: 'address' }, + { internalType: 'bool', name: '_isStable', type: 'bool' } + ], + name: 'setStableToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // 检查是否是稳定币 + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'stableTokens', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function' + } +] as const + // YT Vault ABI - for pool info export const YT_VAULT_ABI = [ { @@ -763,5 +930,49 @@ export const YT_VAULT_ABI = [ outputs: [{ internalType: 'address', name: '', type: 'address' }], stateMutability: 'view', type: 'function' + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'tokenDecimals', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'maxUsdyAmounts', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + // 读取 priceFeed 地址 + { + inputs: [], + name: 'priceFeed', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function' + }, + // 管理员配置函数 - 设置白名单代币(包含配置) + { + inputs: [ + { internalType: 'address', name: '_token', type: 'address' }, + { internalType: 'uint256', name: '_decimals', type: 'uint256' }, + { internalType: 'uint256', name: '_weight', type: 'uint256' }, + { internalType: 'uint256', name: '_maxUsdyAmount', type: 'uint256' }, + { internalType: 'bool', name: '_isStable', type: 'bool' } + ], + name: 'setWhitelistedToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // 清除白名单代币 + { + inputs: [{ internalType: 'address', name: '_token', type: 'address' }], + name: 'clearWhitelistedToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' } ] as const diff --git a/frontend/src/config/wagmi.ts b/frontend/src/config/wagmi.ts index a715667..39d005c 100644 --- a/frontend/src/config/wagmi.ts +++ b/frontend/src/config/wagmi.ts @@ -21,6 +21,19 @@ export const config = defaultWagmiConfig({ chains, projectId, metadata, + // 启用coinbase钱包连接器 + enableCoinbase: true, + // 启用injected钱包(MetaMask, TokenPocket等浏览器扩展) + enableInjected: true, + // 禁用WalletConnect(因为CSP问题) + enableWalletConnect: false, + // 禁用Email和社交登录(需要iframe) + auth: { + email: false, + socials: [], + showWallets: true, + walletFeatures: true, + }, }) createWeb3Modal({ diff --git a/frontend/src/hooks/useTransactionHistory.ts b/frontend/src/hooks/useTransactionHistory.ts index 6a2ed33..c31cacc 100644 --- a/frontend/src/hooks/useTransactionHistory.ts +++ b/frontend/src/hooks/useTransactionHistory.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' -export type TransactionType = 'mint' | 'burn' | 'buy' | 'sell' | 'approve' | 'create_vault' | 'update_price' | 'test' +export type TransactionType = 'mint' | 'burn' | 'buy' | 'sell' | 'approve' | 'create_vault' | 'update_price' | 'test' | 'addLiquidity' | 'removeLiquidity' | 'swap' export interface TransactionRecord { id: string @@ -25,10 +25,31 @@ export function useTransactionHistory() { try { const stored = localStorage.getItem(STORAGE_KEY) if (stored) { - setTransactions(JSON.parse(stored)) + const parsed = JSON.parse(stored) + // 过滤并修复损坏的记录 + const valid = Array.isArray(parsed) + ? parsed + .filter((tx: any) => tx && typeof tx === 'object' && tx.id && typeof tx.id === 'string') + .map((tx: any) => ({ + ...tx, + // 确保所有字段都是正确类型 + id: String(tx.id), + type: String(tx.type || 'test'), + hash: (typeof tx.hash === 'string' && tx.hash.startsWith('0x')) ? tx.hash : '', + timestamp: Number(tx.timestamp) || Date.now(), + status: ['pending', 'success', 'failed'].includes(tx.status) ? tx.status : 'failed', + amount: tx.amount ? String(tx.amount) : undefined, + token: tx.token ? String(tx.token) : undefined, + vault: tx.vault ? String(tx.vault) : undefined, + error: tx.error ? (typeof tx.error === 'string' ? tx.error : JSON.stringify(tx.error)) : undefined, + })) + : [] + setTransactions(valid) } } catch (e) { console.error('Failed to load transaction history:', e) + // 清除损坏的数据 + localStorage.removeItem(STORAGE_KEY) } }, []) @@ -44,7 +65,13 @@ export function useTransactionHistory() { // 添加新交易 const addTransaction = (tx: Omit) => { const newTx: TransactionRecord = { - ...tx, + type: tx.type, + hash: (typeof tx.hash === 'string' && tx.hash.startsWith('0x')) ? tx.hash : '', + status: tx.status, + amount: tx.amount ? String(tx.amount) : undefined, + token: tx.token ? String(tx.token) : undefined, + vault: tx.vault ? String(tx.vault) : undefined, + error: tx.error ? (typeof tx.error === 'string' ? tx.error : JSON.stringify(tx.error)) : undefined, id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, timestamp: Date.now(), } @@ -58,6 +85,14 @@ export function useTransactionHistory() { // 更新交易状态 const updateTransaction = (id: string, updates: Partial) => { + // 确保 hash 是字符串 + if (updates.hash && typeof updates.hash !== 'string') { + updates.hash = '' + } + // 确保 error 是字符串 + if (updates.error && typeof updates.error !== 'string') { + updates.error = JSON.stringify(updates.error) + } setTransactions(prev => { const updated = prev.map(tx => tx.id === id ? { ...tx, ...updates } : tx diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 5730fd9..3664be1 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2,6 +2,9 @@ "common": { "connectWallet": "Connect Wallet", "disconnect": "Disconnect", + "connecting": "Connecting", + "forceConnect": "Reset", + "forceConnectHint": "Clear cache and reconnect if normal connection fails", "processing": "Processing...", "confirm": "Confirm", "cancel": "Cancel", @@ -57,7 +60,12 @@ "youWillReceive": "You will receive", "approveWusd": "Approve WUSD", "buy": "Buy YT", - "sell": "Sell YT" + "sell": "Sell YT", + "redeemStatus": "Redeem Status", + "redeemAvailable": "Available", + "redeemNotAvailable": "Not Available", + "redeemLocked": "Redemption Locked", + "timeRemaining": "Time Remaining" }, "factory": { "title": "Factory Management", @@ -99,6 +107,7 @@ "txSubmitted": "Transaction submitted", "txSuccess": "Transaction successful", "txFailed": "Transaction failed", + "txTimeout": "Transaction timeout, please try again", "copySuccess": "Copied to clipboard", "walletError": "Wallet error", "networkError": "Network error", @@ -195,6 +204,9 @@ "testSwapSame": "Swap Same Token", "testSwapSameDesc": "Test swapYT(YT-A, YT-A, amount, 0, receiver)", "testSwapExceed": "Swap Exceed Balance", - "testSwapExceedDesc": "Swap amount exceeding token balance" + "testSwapExceedDesc": "Swap amount exceeding token balance", + "yourTokenBalances": "Your Token Balances", + "whitelisted": "Whitelisted", + "notWhitelisted": "Not Whitelisted" } } diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 70e94a4..1c9225c 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -2,6 +2,9 @@ "common": { "connectWallet": "连接钱包", "disconnect": "断开连接", + "connecting": "连接中", + "forceConnect": "重置连接", + "forceConnectHint": "如果普通连接失败,点此清除缓存后重新连接", "processing": "处理中...", "confirm": "确认", "cancel": "取消", @@ -57,7 +60,12 @@ "youWillReceive": "你将收到", "approveWusd": "授权 WUSD", "buy": "买入 YT", - "sell": "卖出 YT" + "sell": "卖出 YT", + "redeemStatus": "赎回状态", + "redeemAvailable": "可以赎回", + "redeemNotAvailable": "暂不可赎回", + "redeemLocked": "赎回锁定中", + "timeRemaining": "剩余时间" }, "factory": { "title": "工厂管理", @@ -99,6 +107,7 @@ "txSubmitted": "交易已提交", "txSuccess": "交易成功", "txFailed": "交易失败", + "txTimeout": "交易超时,请重试", "copySuccess": "已复制到剪贴板", "walletError": "钱包错误", "networkError": "网络错误", @@ -195,6 +204,9 @@ "testSwapSame": "相同代币互换", "testSwapSameDesc": "测试 swapYT(YT-A, YT-A, amount, 0, receiver)", "testSwapExceed": "互换超过余额", - "testSwapExceedDesc": "互换金额超过代币余额" + "testSwapExceedDesc": "互换金额超过代币余额", + "yourTokenBalances": "你的代币余额", + "whitelisted": "已白名单", + "notWhitelisted": "未白名单" } } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 3f0fa5a..bb19002 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -10,6 +10,30 @@ export default defineConfig({ strictPort: true, allowedHosts: ['maxfight.vip', 'localhost', '127.0.0.1'], }, + // 优化依赖预构建 + optimizeDeps: { + include: [ + 'wagmi', + 'viem', + 'viem/actions', + 'viem/chains', + '@tanstack/react-query', + 'react', + 'react-dom', + 'react-i18next', + 'i18next', + ], + // 强制预构建这些依赖,避免动态导入问题 + esbuildOptions: { + target: 'esnext', + }, + }, + // 解析配置 + resolve: { + alias: { + // 解决 viem 动态导入问题 + }, + }, build: { rollupOptions: { output: {