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:
2025-12-16 18:57:26 +00:00
parent dbb287b814
commit 3da0bf24d0
15 changed files with 1826 additions and 179 deletions

View File

@@ -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;
}

View File

@@ -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 (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<ToastProvider>
<TransactionProvider>
<AppContent />
</TransactionProvider>
</ToastProvider>
</QueryClientProvider>
</WagmiProvider>
<ErrorBoundary>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<ToastProvider>
<TransactionProvider>
<AppContent />
</TransactionProvider>
</ToastProvider>
</QueryClientProvider>
</WagmiProvider>
</ErrorBoundary>
)
}

View File

@@ -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 (
<button onClick={() => open()} className="btn btn-primary">
{t('common.connectWallet')}
</button>
<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>
)
}

View 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
}
}

View File

@@ -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

View File

@@ -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,18 +74,26 @@ export function TransactionHistory({ transactions, onClear }: Props) {
<span className="tx-time">{formatTime(tx.timestamp)}</span>
</div>
<div className="tx-hash">
<a
href={`https://sepolia.arbiscan.io/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
>
{shortenHash(tx.hash)}
</a>
{tx.hash && typeof tx.hash === 'string' && tx.hash.startsWith('0x') ? (
<a
href={`https://sepolia.arbiscan.io/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
>
{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>

View File

@@ -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<string | null>(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 (
<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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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({

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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": "未白名单"
}
}