feat: 优化金库交易和LP流动池功能
主要更新: - 金库交易动态读取: 从合约动态获取所有金库地址和名称 - 边界值测试优化: 修复 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,6 +74,7 @@ function AppContent() {
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<WagmiProvider config={config}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>
|
||||
@@ -82,6 +84,7 @@ function App() {
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="connect-info">
|
||||
<span className="address">{formatAddress(address)}</span>
|
||||
<button onClick={() => disconnect()} className="btn btn-secondary">
|
||||
<button onClick={handleDisconnect} className="btn btn-secondary">
|
||||
{t('common.disconnect')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -24,8 +56,17 @@ export function ConnectButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="connect-buttons">
|
||||
<button onClick={() => open()} className="btn btn-primary">
|
||||
{t('common.connectWallet')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="btn btn-secondary btn-sm"
|
||||
title={t('common.forceConnectHint')}
|
||||
>
|
||||
{t('common.forceConnect')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
100
frontend/src/components/ErrorBoundary.tsx
Normal file
100
frontend/src/components/ErrorBoundary.tsx
Normal file
@@ -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<Props, State> {
|
||||
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 (
|
||||
<div className="error-boundary">
|
||||
<div className="error-content">
|
||||
<h2>Something went wrong</h2>
|
||||
<p className="error-message">
|
||||
{typeof this.state.error?.message === 'string'
|
||||
? this.state.error.message
|
||||
: JSON.stringify(this.state.error?.message) || 'Unknown error'}
|
||||
</p>
|
||||
|
||||
<div className="error-hint">
|
||||
<p>Common causes:</p>
|
||||
<ul>
|
||||
<li>Multiple wallet extensions conflict</li>
|
||||
<li>Network connection issues</li>
|
||||
<li>Wallet state corruption</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="error-actions">
|
||||
<button onClick={this.handleReset} className="btn btn-secondary">
|
||||
Try Again
|
||||
</button>
|
||||
<button onClick={this.handleReload} className="btn btn-primary">
|
||||
Clear Cache & Reload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="error-tip">
|
||||
Tip: If the problem persists, try disabling other wallet extensions and keep only one.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="panel">
|
||||
@@ -251,10 +275,10 @@ export function FactoryPanel() {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreateVault}
|
||||
disabled={isPending || isConfirming}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{isPending || isConfirming ? t('common.processing') : t('factory.create')}
|
||||
{isProcessing ? t('common.processing') : t('factory.create')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -299,10 +323,10 @@ export function FactoryPanel() {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleUpdatePrices}
|
||||
disabled={isPending || isConfirming || !priceForm.vault}
|
||||
disabled={isProcessing || !priceForm.vault}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{isPending || isConfirming ? t('common.processing') : t('factory.update')}
|
||||
{isProcessing ? t('common.processing') : t('factory.update')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@@ -328,7 +352,7 @@ export function FactoryPanel() {
|
||||
<span className="test-error">OwnableUnauthorizedAccount</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.updatePriceNotOwnerDesc')}</p>
|
||||
<button onClick={() => runPermissionTest('update_price_not_owner')} disabled={isPending || isConfirming} className="btn btn-danger btn-sm">
|
||||
<button onClick={() => runPermissionTest('update_price_not_owner')} disabled={isProcessing} className="btn btn-danger btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -339,7 +363,7 @@ export function FactoryPanel() {
|
||||
<span className="test-error">OwnableUnauthorizedAccount</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.setManagerNotOwnerDesc')}</p>
|
||||
<button onClick={() => runPermissionTest('set_manager_not_owner')} disabled={isPending || isConfirming} className="btn btn-danger btn-sm">
|
||||
<button onClick={() => runPermissionTest('set_manager_not_owner')} disabled={isProcessing} className="btn btn-danger btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
</button>
|
||||
</div>
|
||||
<div className="tx-list">
|
||||
{transactions.slice(0, 10).map((tx) => (
|
||||
{transactions.filter(tx => tx && tx.id).slice(0, 10).map((tx) => (
|
||||
<div key={tx.id} className={`tx-item ${getStatusClass(tx.status)}`}>
|
||||
<div className="tx-info">
|
||||
<span className="tx-type">{getTypeLabel(tx.type)}</span>
|
||||
@@ -68,6 +74,7 @@ export function TransactionHistory({ transactions, onClear }: Props) {
|
||||
<span className="tx-time">{formatTime(tx.timestamp)}</span>
|
||||
</div>
|
||||
<div className="tx-hash">
|
||||
{tx.hash && typeof tx.hash === 'string' && tx.hash.startsWith('0x') ? (
|
||||
<a
|
||||
href={`https://sepolia.arbiscan.io/tx/${tx.hash}`}
|
||||
target="_blank"
|
||||
@@ -75,11 +82,18 @@ export function TransactionHistory({ transactions, onClear }: Props) {
|
||||
>
|
||||
{shortenHash(tx.hash)}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-muted">-</span>
|
||||
)}
|
||||
<span className={`tx-status ${tx.status}`}>
|
||||
{tx.status === 'success' ? '✓' : tx.status === 'failed' ? '✗' : '...'}
|
||||
</span>
|
||||
</div>
|
||||
{tx.error && <div className="tx-error">{tx.error}</div>}
|
||||
{tx.error && (
|
||||
<div className="tx-error">
|
||||
{typeof tx.error === 'string' ? tx.error : JSON.stringify(tx.error)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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,13 +327,25 @@ 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<string | null>(null)
|
||||
|
||||
// 边界测试函数
|
||||
const runBoundaryTest = (testType: string) => {
|
||||
setTestResult({ type: 'pending', msg: t('test.running') })
|
||||
try {
|
||||
setIsTestRunning(true)
|
||||
testTypeRef.current = testType
|
||||
|
||||
// 记录测试交易
|
||||
recordTx('test', undefined, testType)
|
||||
|
||||
switch (testType) {
|
||||
case 'buy_zero':
|
||||
writeContract({
|
||||
@@ -233,6 +353,9 @@ export function VaultPanel() {
|
||||
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':
|
||||
@@ -241,41 +364,66 @@ export function VaultPanel() {
|
||||
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':
|
||||
const hardCap = vaultInfo ? vaultInfo[4] : parseUnits('999999999', 18)
|
||||
// 正确计算:(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: [hardCap + parseUnits('1', 18)],
|
||||
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':
|
||||
@@ -284,18 +432,55 @@ export function VaultPanel() {
|
||||
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
|
||||
}
|
||||
} catch (err: any) {
|
||||
setTestResult({ type: 'error', msg: err.message || 'Error' })
|
||||
}
|
||||
|
||||
// 边界测试结果处理 - 监听 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 (
|
||||
<div className="panel">
|
||||
@@ -314,12 +499,12 @@ export function VaultPanel() {
|
||||
<select
|
||||
value={selectedVault.address}
|
||||
onChange={(e) => {
|
||||
const vault = VAULTS.find(v => v.address === e.target.value)
|
||||
const vault = vaultList.find(v => v.address === e.target.value)
|
||||
if (vault) setSelectedVault(vault)
|
||||
}}
|
||||
className="input"
|
||||
>
|
||||
{VAULTS.map((vault) => (
|
||||
{vaultList.map((vault) => (
|
||||
<option key={vault.address} value={vault.address}>
|
||||
{vault.name}
|
||||
</option>
|
||||
@@ -389,7 +574,7 @@ export function VaultPanel() {
|
||||
<strong>{wusdBalance ? formatUnits(wusdBalance, 18) : '0'}</strong>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span>{t('vault.yourYtBalance')}:</span>
|
||||
<span>{t('vault.yourYtBalance')} ({selectedVault.name}):</span>
|
||||
<strong>{ytBalance ? formatUnits(ytBalance, 18) : '0'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@@ -429,18 +614,18 @@ export function VaultPanel() {
|
||||
{needsApproval ? (
|
||||
<button
|
||||
onClick={handleApprove}
|
||||
disabled={isPending || isConfirming}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
{isPending || isConfirming ? t('common.processing') : t('vault.approveWusd')}
|
||||
{isProcessing ? t('common.processing') : t('vault.approveWusd')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleBuy}
|
||||
disabled={isPending || isConfirming || !buyAmount}
|
||||
disabled={isProcessing || !buyAmount}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{isPending || isConfirming ? t('common.processing') : t('vault.buy')}
|
||||
{isProcessing ? t('common.processing') : t('vault.buy')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -448,6 +633,17 @@ export function VaultPanel() {
|
||||
|
||||
{activeTab === 'sell' && (
|
||||
<div className="trade-section">
|
||||
{/* 赎回状态提示 */}
|
||||
<div className={`redeem-status-banner ${canRedeem ? 'success' : 'warning'}`}>
|
||||
<span>{t('vault.redeemStatus')}: </span>
|
||||
<strong>{canRedeem ? t('vault.redeemAvailable') : t('vault.redeemNotAvailable')}</strong>
|
||||
{!canRedeem && timeUntilRedeem && (
|
||||
<span className="time-remaining">
|
||||
({t('vault.timeRemaining')}: {formatTimeRemaining(Number(timeUntilRedeem))})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>{t('vault.ytAmount')}</label>
|
||||
<input
|
||||
@@ -456,6 +652,7 @@ export function VaultPanel() {
|
||||
onChange={(e) => setSellAmount(e.target.value)}
|
||||
placeholder={t('vault.enterYtAmount')}
|
||||
className="input"
|
||||
disabled={!canRedeem}
|
||||
/>
|
||||
</div>
|
||||
{previewSellAmount && sellAmount && (
|
||||
@@ -465,10 +662,10 @@ export function VaultPanel() {
|
||||
)}
|
||||
<button
|
||||
onClick={handleSell}
|
||||
disabled={isPending || isConfirming || !sellAmount}
|
||||
disabled={isProcessing || !sellAmount || !canRedeem}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{isPending || isConfirming ? t('common.processing') : t('vault.sell')}
|
||||
{isProcessing ? t('common.processing') : !canRedeem ? t('vault.redeemLocked') : t('vault.sell')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -500,7 +697,7 @@ export function VaultPanel() {
|
||||
<span className="test-error">InvalidAmount</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.buyZeroDesc')}</p>
|
||||
<button onClick={() => runBoundaryTest('buy_zero')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
|
||||
<button onClick={() => runBoundaryTest('buy_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -511,7 +708,7 @@ export function VaultPanel() {
|
||||
<span className="test-error">InvalidAmount</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.sellZeroDesc')}</p>
|
||||
<button onClick={() => runBoundaryTest('sell_zero')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
|
||||
<button onClick={() => runBoundaryTest('sell_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -522,7 +719,7 @@ export function VaultPanel() {
|
||||
<span className="test-error">InsufficientWUSD</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.buyExceedBalanceDesc')}</p>
|
||||
<button onClick={() => runBoundaryTest('buy_exceed_balance')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
|
||||
<button onClick={() => runBoundaryTest('buy_exceed_balance')} disabled={isProcessing} className="btn btn-warning btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -533,7 +730,7 @@ export function VaultPanel() {
|
||||
<span className="test-error">InsufficientYTA</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.sellExceedBalanceDesc')}</p>
|
||||
<button onClick={() => runBoundaryTest('sell_exceed_balance')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
|
||||
<button onClick={() => runBoundaryTest('sell_exceed_balance')} disabled={isProcessing} className="btn btn-warning btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -544,7 +741,7 @@ export function VaultPanel() {
|
||||
<span className="test-error">HardCapExceeded</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.buyExceedHardcapDesc')}</p>
|
||||
<button onClick={() => runBoundaryTest('buy_exceed_hardcap')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
|
||||
<button onClick={() => runBoundaryTest('buy_exceed_hardcap')} disabled={isProcessing} className="btn btn-warning btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -555,14 +752,14 @@ export function VaultPanel() {
|
||||
<span className="test-error">StillInLockPeriod</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.sellInLockDesc')}</p>
|
||||
<button onClick={() => runBoundaryTest('sell_in_lock')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
|
||||
<button onClick={() => runBoundaryTest('sell_in_lock')} disabled={isProcessing} className="btn btn-warning btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="quick-actions" style={{ marginTop: '16px' }}>
|
||||
<button onClick={() => runBoundaryTest('max_approve')} disabled={isPending || isConfirming} className="btn btn-secondary">
|
||||
<button onClick={() => runBoundaryTest('max_approve')} disabled={isProcessing} className="btn btn-secondary">
|
||||
{t('test.maxApprove')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
<button
|
||||
onClick={handleMint}
|
||||
disabled={isPending || isConfirming || !mintAmount}
|
||||
disabled={isProcessing || !mintAmount}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{isPending ? t('wusd.confirming') : isConfirming ? t('wusd.minting') : t('wusd.mint')}
|
||||
{isProcessing ? (isPending ? t('wusd.confirming') : t('wusd.minting')) : t('wusd.mint')}
|
||||
</button>
|
||||
|
||||
{/* 边界测试区域 */}
|
||||
@@ -217,7 +271,7 @@ export function WUSDPanel() {
|
||||
<span className="test-error">MaySucceed</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.mintZeroDesc')}</p>
|
||||
<button onClick={() => runBoundaryTest('mint_zero')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
|
||||
<button onClick={() => runBoundaryTest('mint_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -228,14 +282,14 @@ export function WUSDPanel() {
|
||||
<span className="test-error">ERC20InsufficientBalance</span>
|
||||
</div>
|
||||
<p className="test-desc">{t('test.burnExceedDesc')}</p>
|
||||
<button onClick={() => runBoundaryTest('burn_exceed')} disabled={isPending || isConfirming} className="btn btn-warning btn-sm">
|
||||
<button onClick={() => runBoundaryTest('burn_exceed')} disabled={isProcessing} className="btn btn-warning btn-sm">
|
||||
{t('test.run')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="quick-actions" style={{ marginTop: '16px' }}>
|
||||
<button onClick={() => runBoundaryTest('mint_10000')} disabled={isPending || isConfirming} className="btn btn-primary">
|
||||
<button onClick={() => runBoundaryTest('mint_10000')} disabled={isProcessing} className="btn btn-primary">
|
||||
{t('test.mint10000')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<TransactionRecord, 'id' | 'timestamp'>) => {
|
||||
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<TransactionRecord>) => {
|
||||
// 确保 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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "未白名单"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user