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 (
- open()} className="btn btn-primary">
- {t('common.connectWallet')}
-
+
+ open()} className="btn btn-primary">
+ {t('common.connectWallet')}
+
+
+ {t('common.forceConnect')}
+
+
)
}
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
+
+
+
+
+
+ Try Again
+
+
+ Clear Cache & Reload
+
+
+
+
+ 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() {
- {isPending || isConfirming ? t('common.processing') : t('factory.create')}
+ {isProcessing ? t('common.processing') : t('factory.create')}
@@ -299,10 +323,10 @@ export function FactoryPanel() {
- {isPending || isConfirming ? t('common.processing') : t('factory.update')}
+ {isProcessing ? t('common.processing') : t('factory.update')}
>
@@ -328,7 +352,7 @@ export function FactoryPanel() {
OwnableUnauthorizedAccount
{t('test.updatePriceNotOwnerDesc')}
- runPermissionTest('update_price_not_owner')} disabled={isPending || isConfirming} className="btn btn-danger btn-sm">
+ runPermissionTest('update_price_not_owner')} disabled={isProcessing} className="btn btn-danger btn-sm">
{t('test.run')}
@@ -339,7 +363,7 @@ export function FactoryPanel() {
OwnableUnauthorizedAccount
{t('test.setManagerNotOwnerDesc')}
- runPermissionTest('set_manager_not_owner')} disabled={isPending || isConfirming} className="btn btn-danger btn-sm">
+ runPermissionTest('set_manager_not_owner')} disabled={isProcessing} className="btn btn-danger btn-sm">
{t('test.run')}
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() ? (
- {isPending || isConfirming ? t('common.processing') : t('lp.approveToken')}
+ {isProcessing ? t('common.processing') : t('lp.approveToken')}
) : (
- {isPending || isConfirming ? t('common.processing') : t('lp.addLiquidity')}
+ {isProcessing ? t('common.processing') : t('lp.addLiquidity')}
)}
@@ -613,18 +1144,18 @@ export function LPPanel() {
{needsYtLPApproval() ? (
- {isPending || isConfirming ? t('common.processing') : t('lp.approveYtLP')}
+ {isProcessing ? t('common.processing') : t('lp.approveYtLP')}
) : (
0}
+ disabled={isProcessing || !removeLiquidityForm.amount || getCooldownRemaining() > 0}
className="btn btn-primary"
>
- {isPending || isConfirming ? t('common.processing') : t('lp.removeLiquidity')}
+ {isProcessing ? t('common.processing') : t('lp.removeLiquidity')}
)}
@@ -693,10 +1224,10 @@ export function LPPanel() {
- {isPending || isConfirming ? t('common.processing') : t('lp.swap')}
+ {isProcessing ? t('common.processing') : t('lp.swap')}
)}
@@ -729,7 +1260,7 @@ export function LPPanel() {
InvalidAmount
{t('lp.testAddZeroDesc')}
- runBoundaryTest('add_zero')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('add_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -740,7 +1271,7 @@ export function LPPanel() {
ERC20InsufficientBalance
{t('lp.testAddExceedDesc')}
- runBoundaryTest('add_exceed_balance')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('add_exceed_balance')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -752,7 +1283,7 @@ export function LPPanel() {
InvalidAmount
{t('lp.testRemoveZeroDesc')}
- runBoundaryTest('remove_zero')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('remove_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -763,7 +1294,7 @@ export function LPPanel() {
ERC20InsufficientBalance
{t('lp.testRemoveExceedDesc')}
- runBoundaryTest('remove_exceed_balance')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('remove_exceed_balance')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -774,7 +1305,7 @@ export function LPPanel() {
InsufficientOutput
{t('lp.testRemoveHighMinoutDesc')}
- runBoundaryTest('remove_high_minout')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('remove_high_minout')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -786,7 +1317,7 @@ export function LPPanel() {
InvalidAmount
{t('lp.testSwapZeroDesc')}
- runBoundaryTest('swap_zero')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('swap_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -797,7 +1328,7 @@ export function LPPanel() {
MaySucceed
{t('lp.testSwapSameDesc')}
- runBoundaryTest('swap_same_token')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('swap_same_token')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -808,7 +1339,7 @@ export function LPPanel() {
ERC20InsufficientBalance
{t('lp.testSwapExceedDesc')}
- runBoundaryTest('swap_exceed_balance')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('swap_exceed_balance')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -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
+
+
+
+
+ {isProcessing ? '处理中...' : '设置价格 (forceUpdatePrice)'}
+
+
+
+
+
设置价差
+
+
+
+
setPriceConfigForm({ ...priceConfigForm, spreadBps: e.target.value })}
+ placeholder="20"
+ className="input"
+ />
+
+ 例如: 20 = 0.2%, 50 = 0.5%
+
+
+
+
+ {isProcessing ? '处理中...' : '设置价差 (setSpreadBasisPoints)'}
+
+
+
+
+
+ {/* 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)
+
+
+
+
+
+ {isProcessing ? '处理中...' : '设置白名单代币 (setWhitelistedToken)'}
+
+
+
+ {/* 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}
+
+
+
+
+ {isProcessing ? '处理中...' : '设置 (setWusdPriceSource)'}
+
+
+
+
+
+
+
完整配置步骤:
+
+ - YTPriceFeed: 设置 wusdPriceSource 为 WUSD 合约地址
+ - YTPriceFeed: 设置价格 (forceUpdatePrice)
+ - YTPriceFeed: 设置价差 (setSpreadBasisPoints) - 可选
+ - YTVault: 设置白名单代币 (setWhitelistedToken) - 包含 decimals, weight, maxUsdyAmount
+ - 配置完成后,刷新页面查看池子状态
+
+
+ 重要: 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)}
- {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() {
- {t('vault.yourYtBalance')}:
+ {t('vault.yourYtBalance')} ({selectedVault.name}):
{ytBalance ? formatUnits(ytBalance, 18) : '0'}
@@ -429,18 +614,18 @@ export function VaultPanel() {
{needsApproval ? (
- {isPending || isConfirming ? t('common.processing') : t('vault.approveWusd')}
+ {isProcessing ? t('common.processing') : t('vault.approveWusd')}
) : (
- {isPending || isConfirming ? t('common.processing') : t('vault.buy')}
+ {isProcessing ? t('common.processing') : t('vault.buy')}
)}
@@ -448,6 +633,17 @@ export function VaultPanel() {
{activeTab === 'sell' && (
+ {/* 赎回状态提示 */}
+
+ {t('vault.redeemStatus')}:
+ {canRedeem ? t('vault.redeemAvailable') : t('vault.redeemNotAvailable')}
+ {!canRedeem && timeUntilRedeem && (
+
+ ({t('vault.timeRemaining')}: {formatTimeRemaining(Number(timeUntilRedeem))})
+
+ )}
+
+
setSellAmount(e.target.value)}
placeholder={t('vault.enterYtAmount')}
className="input"
+ disabled={!canRedeem}
/>
{previewSellAmount && sellAmount && (
@@ -465,10 +662,10 @@ export function VaultPanel() {
)}
- {isPending || isConfirming ? t('common.processing') : t('vault.sell')}
+ {isProcessing ? t('common.processing') : !canRedeem ? t('vault.redeemLocked') : t('vault.sell')}
)}
@@ -500,7 +697,7 @@ export function VaultPanel() {
InvalidAmount
{t('test.buyZeroDesc')}
- runBoundaryTest('buy_zero')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('buy_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -511,7 +708,7 @@ export function VaultPanel() {
InvalidAmount
{t('test.sellZeroDesc')}
- runBoundaryTest('sell_zero')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('sell_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -522,7 +719,7 @@ export function VaultPanel() {
InsufficientWUSD
{t('test.buyExceedBalanceDesc')}
- runBoundaryTest('buy_exceed_balance')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('buy_exceed_balance')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -533,7 +730,7 @@ export function VaultPanel() {
InsufficientYTA
{t('test.sellExceedBalanceDesc')}
- runBoundaryTest('sell_exceed_balance')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('sell_exceed_balance')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -544,7 +741,7 @@ export function VaultPanel() {
HardCapExceeded
{t('test.buyExceedHardcapDesc')}
- runBoundaryTest('buy_exceed_hardcap')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('buy_exceed_hardcap')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -555,14 +752,14 @@ export function VaultPanel() {
StillInLockPeriod
{t('test.sellInLockDesc')}
- runBoundaryTest('sell_in_lock')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('sell_in_lock')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
- runBoundaryTest('max_approve')} disabled={isPending || isConfirming} className="btn btn-secondary">
+ runBoundaryTest('max_approve')} disabled={isProcessing} className="btn btn-secondary">
{t('test.maxApprove')}
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() {
- {isPending ? t('wusd.confirming') : isConfirming ? t('wusd.minting') : t('wusd.mint')}
+ {isProcessing ? (isPending ? t('wusd.confirming') : t('wusd.minting')) : t('wusd.mint')}
{/* 边界测试区域 */}
@@ -217,7 +271,7 @@ export function WUSDPanel() {
MaySucceed
{t('test.mintZeroDesc')}
- runBoundaryTest('mint_zero')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('mint_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
@@ -228,14 +282,14 @@ export function WUSDPanel() {
ERC20InsufficientBalance
{t('test.burnExceedDesc')}
- runBoundaryTest('burn_exceed')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
+ runBoundaryTest('burn_exceed')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
- runBoundaryTest('mint_10000')} disabled={isPending || isConfirming} className="btn btn-primary">
+ runBoundaryTest('mint_10000')} disabled={isProcessing} className="btn btn-primary">
{t('test.mint10000')}
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: {