feat: 添加多链支持和 Lending 借贷系统

- 新增 ARB Sepolia + BNB Testnet 多链支持
- 添加 LendingPanel 借贷系统组件
- 添加 LendingAdminPanel 管理面板
- 添加 USDCPanel USDC 操作组件
- 添加 HoldersPanel 持有人信息组件
- 添加 AutoTestPanel 自动化测试组件
- 重构 LP 组件为模块化结构 (LP/)
- 添加多个调试和测试脚本
- 修复 USDC 精度动态配置
- 优化合约配置支持多链切换

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 23:32:29 +08:00
parent c8427caa01
commit 5cb64f881e
80 changed files with 18342 additions and 1117 deletions

View File

@@ -1,24 +1,58 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { WagmiProvider } from 'wagmi'
import { QueryClientProvider } from '@tanstack/react-query'
import { useWeb3Modal } from '@web3modal/wagmi/react'
import { useAccount, useChainId, useSwitchChain } from 'wagmi'
import { getSupportedChainIds, getChainName, isSupportedChain } from './config/contracts'
import { config, queryClient } from './config/wagmi'
import { ConnectButton } from './components/ConnectButton'
import { LanguageSwitch } from './components/LanguageSwitch'
import { WUSDPanel } from './components/WUSDPanel'
import { USDCPanel } from './components/USDCPanel'
import { VaultPanel } from './components/VaultPanel'
import { FactoryPanel } from './components/FactoryPanel'
import { LPPanel } from './components/LPPanel'
import { LPPanelNew as LPPanel } from './components/LP'
import { LendingPanel } from './components/LendingPanel'
import { HoldersPanel } from './components/HoldersPanel'
// import { AutoTestPanel } from './components/AutoTestPanel' // 隐藏自动测试
import { ToastProvider } from './components/Toast'
import { TransactionProvider } from './context/TransactionContext'
import { ErrorBoundary } from './components/ErrorBoundary'
import './App.css'
type Tab = 'wusd' | 'vault' | 'factory' | 'lp'
type Tab = 'usdc' | 'vault' | 'factory' | 'lp' | 'lending' | 'holders'
function AppContent() {
const { t } = useTranslation()
const [activeTab, setActiveTab] = useState<Tab>('vault')
const { open } = useWeb3Modal()
const { isConnected } = useAccount()
// ===== 多链支持:网络切换 =====
const chainId = useChainId()
const { switchChain } = useSwitchChain()
const supportedChains = getSupportedChainIds()
const currentChainName = getChainName(chainId)
const isSupported = isSupportedChain(chainId)
// 切换网络
const handleSwitchChain = (newChainId: number) => {
if (switchChain && newChainId !== chainId) {
switchChain({ chainId: newChainId })
}
}
// 检查是否需要自动打开连接弹窗(切换钱包后)
useEffect(() => {
const shouldAutoOpen = sessionStorage.getItem('autoOpenConnect')
if (shouldAutoOpen && !isConnected) {
sessionStorage.removeItem('autoOpenConnect')
// 延迟打开,等待页面完全加载
setTimeout(() => {
open()
}, 500)
}
}, [isConnected, open])
return (
<div className="app">
@@ -26,17 +60,51 @@ function AppContent() {
<h1>{t('header.title')}</h1>
<div className="header-info">
<LanguageSwitch />
<span className="network-badge">{t('common.network')}</span>
{/* 网络切换器 */}
<div className="network-switcher">
<select
value={chainId}
onChange={(e) => handleSwitchChain(Number(e.target.value))}
className="network-select"
style={{
padding: '6px 12px',
borderRadius: '6px',
border: isSupported ? '1px solid #4CAF50' : '1px solid #f44336',
backgroundColor: isSupported ? '#f1f8f4' : '#ffebee',
color: isSupported ? '#2e7d32' : '#c62828',
fontWeight: '500',
cursor: 'pointer',
fontSize: '14px',
}}
>
{supportedChains.map(id => {
const name = getChainName(id)
return (
<option key={id} value={id}>
{name} (ID: {id})
</option>
)
})}
</select>
{!isSupported && (
<span style={{ color: '#f44336', fontSize: '12px', marginLeft: '8px' }}>
</span>
)}
</div>
<span className="network-badge">{currentChainName}</span>
<ConnectButton />
</div>
</header>
<nav className="nav">
<button
className={`nav-btn ${activeTab === 'wusd' ? 'active' : ''}`}
onClick={() => setActiveTab('wusd')}
className={`nav-btn ${activeTab === 'usdc' ? 'active' : ''}`}
onClick={() => setActiveTab('usdc')}
>
{t('nav.wusd')}
{t('nav.usdc')}
</button>
<button
className={`nav-btn ${activeTab === 'vault' ? 'active' : ''}`}
@@ -56,13 +124,27 @@ function AppContent() {
>
{t('nav.lpPool')}
</button>
<button
className={`nav-btn ${activeTab === 'lending' ? 'active' : ''}`}
onClick={() => setActiveTab('lending')}
>
{t('nav.lending')}
</button>
<button
className={`nav-btn ${activeTab === 'holders' ? 'active' : ''}`}
onClick={() => setActiveTab('holders')}
>
{t('nav.holders')}
</button>
</nav>
<main className="main">
{activeTab === 'wusd' && <WUSDPanel />}
{activeTab === 'usdc' && <USDCPanel />}
{activeTab === 'vault' && <VaultPanel />}
{activeTab === 'factory' && <FactoryPanel />}
{activeTab === 'lp' && <LPPanel />}
{activeTab === 'lending' && <LendingPanel />}
{activeTab === 'holders' && <HoldersPanel />}
</main>
<footer className="footer">

View File

@@ -0,0 +1,999 @@
/**
* 自动化测试面板
*
* 模拟用户点击操作,自动执行交易流程
* 使用与前端一致的 Gas 配置和授权流程
*/
import { useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useAccount, usePublicClient, useWalletClient, useChainId, useChains } from 'wagmi'
import {
createWalletClient,
createPublicClient,
http,
parseUnits,
formatUnits,
type WalletClient,
type PublicClient,
type Account,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { arbitrumSepolia, bscTestnet } from 'viem/chains'
import { GAS_CONFIG, getContracts, getDecimals, getChainConfig, getChainName } from '../config/contracts'
// 测试配置
const TEST_AMOUNTS = {
SMALL: '10',
MEDIUM: '100',
LARGE: '1000',
}
// ABI 片段
const ERC20_ABI = [
{ inputs: [{ name: 'owner', type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'approve', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], name: 'allowance', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }], name: 'transfer', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
] as const
const USDC_LOCAL_ABI = [
{ inputs: [{ name: 'owner', type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: '_to', type: 'address' }, { name: '_amount', type: 'uint256' }], name: 'mint', outputs: [], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'approve', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], name: 'allowance', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
] as const
const VAULT_ABI = [
{ inputs: [{ name: 'usdcAmount', type: 'uint256' }], name: 'depositYT', outputs: [{ type: 'uint256' }], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [{ name: 'ytAmount', type: 'uint256' }], name: 'withdrawYT', outputs: [{ type: 'uint256' }], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [], name: 'hardCap', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [], name: 'totalSupply', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }], name: 'transfer', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
] as const
const ROUTER_ABI = [
{
inputs: [
{ name: '_token', type: 'address' },
{ name: '_amount', type: 'uint256' },
{ name: '_minUsdy', type: 'uint256' },
{ name: '_minYtLP', type: 'uint256' }
],
name: 'addLiquidity',
outputs: [{ type: 'uint256' }],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [
{ name: '_tokenOut', type: 'address' },
{ name: '_ytLPAmount', type: 'uint256' },
{ name: '_minOut', type: 'uint256' },
{ name: '_receiver', type: 'address' }
],
name: 'removeLiquidity',
outputs: [{ type: 'uint256' }],
stateMutability: 'nonpayable',
type: 'function'
},
{
inputs: [
{ name: '_tokenIn', type: 'address' },
{ name: '_tokenOut', type: 'address' },
{ name: '_amountIn', type: 'uint256' },
{ name: '_minOut', type: 'uint256' },
{ name: '_receiver', type: 'address' }
],
name: 'swapYT',
outputs: [{ type: 'uint256' }],
stateMutability: 'nonpayable',
type: 'function'
},
] as const
// 测试结果类型
interface TestResult {
name: string
status: 'pending' | 'running' | 'success' | 'failed' | 'skipped'
message?: string
txHash?: string
duration?: number
}
// 测试上下文
interface TestContext {
publicClient: PublicClient
walletClient: WalletClient
account: Account | `0x${string}`
address: `0x${string}`
log: (message: string) => void
}
// 测试项定义
interface TestItem {
id: string
name: string
description: string
run: (ctx: TestContext) => Promise<{ success: boolean; message: string; txHash?: string }>
}
// 辅助函数:执行授权(如果需要)
async function ensureApproval(
ctx: TestContext,
tokenAddress: `0x${string}`,
spenderAddress: `0x${string}`,
amount: bigint,
tokenName: string,
decimals: number = TOKEN_DECIMALS.DEFAULT
): Promise<boolean> {
const allowance = await ctx.publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'allowance',
args: [ctx.address, spenderAddress],
})
if (allowance >= amount) {
ctx.log(` ${tokenName} 已有足够授权,跳过`)
return true
}
ctx.log(`>>> 模拟操作: 点击 [授权 ${tokenName}] 按钮`)
ctx.log(` 当前授权: ${formatUnits(allowance, decimals)}, 需要: ${formatUnits(amount, decimals)}`)
const approveTx = await ctx.walletClient.writeContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'approve',
args: [spenderAddress, amount],
account: ctx.account,
chain: currentChain,
gas: GAS_CONFIG.SIMPLE,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
ctx.log(` 授权交易已发送: ${approveTx.slice(0, 18)}...`)
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: approveTx })
if (receipt.status === 'success') {
ctx.log(` 授权成功`)
return true
} else {
ctx.log(` [ERROR] 授权失败`)
return false
}
}
export function AutoTestPanel() {
const { t } = useTranslation()
const { address: connectedAddress, isConnected } = useAccount()
const publicClientHook = usePublicClient()
const { data: walletClientHook } = useWalletClient()
// ===== 多链支持 =====
const chainId = useChainId()
const CONTRACTS = getContracts(chainId)
const TOKEN_DECIMALS = getDecimals(chainId)
const currentChainName = getChainName(chainId)
// 根据 chainId 获取对应的 viem chain 配置
const currentChain = chainId === 97 ? bscTestnet : arbitrumSepolia
const isZh = t('nav.autoTest') === '自动测试'
// 状态
const [mode, setMode] = useState<'wallet' | 'privateKey'>('wallet')
const [privateKey, setPrivateKey] = useState('')
const [isRunning, setIsRunning] = useState(false)
const [results, setResults] = useState<TestResult[]>([])
const [logs, setLogs] = useState<string[]>([])
const [selectedTests, setSelectedTests] = useState<string[]>([
'check_balance',
'usdc_mint',
'vault_buy',
'vault_sell',
])
const abortRef = useRef(false)
const logsEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [logs])
const addLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString()
setLogs(prev => [...prev, `[${timestamp}] ${message}`])
}
// ===== 测试项定义 =====
const testItems: TestItem[] = [
{
id: 'check_balance',
name: isZh ? '检查余额' : 'Check Balance',
description: isZh ? '读取 USDC / YT / ytLP 余额' : 'Read USDC / YT / ytLP balance',
run: async (ctx) => {
ctx.log('>>> 模拟操作: 读取用户余额...')
const balances: string[] = []
const usdcBalance = await ctx.publicClient.readContract({
address: CONTRACTS.USDC,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [ctx.address],
})
balances.push(`USDC: ${formatUnits(usdcBalance, TOKEN_DECIMALS.USDC)}`)
const ytaBalance = await ctx.publicClient.readContract({
address: CONTRACTS.VAULTS.YT_A,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [ctx.address],
})
balances.push(`YT-A: ${formatUnits(ytaBalance, TOKEN_DECIMALS.YT)}`)
const ytlpBalance = await ctx.publicClient.readContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [ctx.address],
})
balances.push(`ytLP: ${formatUnits(ytlpBalance, TOKEN_DECIMALS.YT_LP)}`)
return { success: true, message: balances.join(' | ') }
}
},
{
id: 'usdc_mint',
name: isZh ? 'USDC 铸造' : 'USDC Mint',
description: isZh ? `铸造 ${TEST_AMOUNTS.LARGE} USDC` : `Mint ${TEST_AMOUNTS.LARGE} USDC`,
run: async (ctx) => {
const amount = parseUnits(TEST_AMOUNTS.LARGE, TOKEN_DECIMALS.USDC)
ctx.log('>>> 模拟操作: 进入 USDC 页面')
ctx.log(`>>> 模拟操作: 输入铸造金额 ${TEST_AMOUNTS.LARGE}`)
ctx.log('>>> 模拟操作: 点击 [铸造] 按钮')
const mintTx = await ctx.walletClient.writeContract({
address: CONTRACTS.USDC,
abi: USDC_LOCAL_ABI,
functionName: 'mint',
args: [ctx.address, amount],
account: ctx.account,
chain: currentChain,
gas: GAS_CONFIG.SIMPLE,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
ctx.log(` 交易已发送: ${mintTx.slice(0, 18)}...`)
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: mintTx })
if (receipt.status === 'success') {
return { success: true, message: `铸造 ${TEST_AMOUNTS.LARGE} USDC 成功`, txHash: mintTx }
}
return { success: false, message: '交易失败', txHash: mintTx }
}
},
{
id: 'vault_buy',
name: isZh ? 'Vault 买入 YT' : 'Vault Buy YT',
description: isZh ? `${TEST_AMOUNTS.SMALL} USDC -> 授权 -> 买入` : `${TEST_AMOUNTS.SMALL} USDC -> Approve -> Buy`,
run: async (ctx) => {
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.USDC)
const vault = CONTRACTS.VAULTS.YT_A as `0x${string}`
ctx.log('>>> 模拟操作: 进入金库交易页面')
ctx.log(`>>> 模拟操作: 选择 YT-A 金库`)
ctx.log(`>>> 模拟操作: 输入买入金额 ${TEST_AMOUNTS.SMALL} USDC`)
const balance = await ctx.publicClient.readContract({
address: CONTRACTS.USDC,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [ctx.address],
})
if (balance < amount) {
return { success: false, message: `USDC 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.USDC)}` }
}
ctx.log(` USDC 余额: ${formatUnits(balance, TOKEN_DECIMALS.USDC)}`)
const [totalSupply, hardCap] = await Promise.all([
ctx.publicClient.readContract({ address: vault, abi: VAULT_ABI, functionName: 'totalSupply' }),
ctx.publicClient.readContract({ address: vault, abi: VAULT_ABI, functionName: 'hardCap' }),
])
if (totalSupply >= hardCap) {
return { success: false, message: '已达硬顶,无法买入' }
}
ctx.log(` 当前供应: ${formatUnits(totalSupply, TOKEN_DECIMALS.YT)} / ${formatUnits(hardCap, TOKEN_DECIMALS.USDC)}`)
const approved = await ensureApproval(ctx, CONTRACTS.USDC as `0x${string}`, vault, amount, 'USDC', TOKEN_DECIMALS.USDC)
if (!approved) return { success: false, message: '授权失败' }
ctx.log('>>> 模拟操作: 点击 [买入] 按钮')
const buyTx = await ctx.walletClient.writeContract({
address: vault,
abi: VAULT_ABI,
functionName: 'depositYT',
args: [amount],
account: ctx.account,
chain: currentChain,
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
ctx.log(` 买入交易已发送: ${buyTx.slice(0, 18)}...`)
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: buyTx })
if (receipt.status === 'success') {
return { success: true, message: `买入成功`, txHash: buyTx }
}
return { success: false, message: '交易失败', txHash: buyTx }
}
},
{
id: 'vault_sell',
name: isZh ? 'Vault 卖出 YT' : 'Vault Sell YT',
description: isZh ? `${TEST_AMOUNTS.SMALL} YT-A -> 卖出` : `${TEST_AMOUNTS.SMALL} YT-A -> Sell`,
run: async (ctx) => {
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.YT)
const vault = CONTRACTS.VAULTS.YT_A as `0x${string}`
ctx.log('>>> 模拟操作: 切换到卖出 Tab')
ctx.log(`>>> 模拟操作: 输入卖出金额 ${TEST_AMOUNTS.SMALL} YT-A`)
const balance = await ctx.publicClient.readContract({
address: vault,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [ctx.address],
})
if (balance < amount) {
return { success: false, message: `YT-A 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.YT)}` }
}
ctx.log(` YT-A 余额: ${formatUnits(balance, TOKEN_DECIMALS.YT)}`)
ctx.log('>>> 模拟操作: 点击 [卖出] 按钮')
const sellTx = await ctx.walletClient.writeContract({
address: vault,
abi: VAULT_ABI,
functionName: 'withdrawYT',
args: [amount],
account: ctx.account,
chain: currentChain,
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
ctx.log(` 卖出交易已发送: ${sellTx.slice(0, 18)}...`)
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: sellTx })
if (receipt.status === 'success') {
return { success: true, message: `已加入队列`, txHash: sellTx }
}
return { success: false, message: '交易失败', txHash: sellTx }
}
},
{
id: 'yt_transfer',
name: isZh ? 'YT 转账' : 'YT Transfer',
description: isZh ? `转账 1 YT-A 给自己` : `Transfer 1 YT-A to self`,
run: async (ctx) => {
const amount = parseUnits('1', TOKEN_DECIMALS.YT)
const vault = CONTRACTS.VAULTS.YT_A as `0x${string}`
ctx.log('>>> 模拟操作: 进入 YT 转账')
const balance = await ctx.publicClient.readContract({
address: vault,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [ctx.address],
})
if (balance < amount) {
return { success: false, message: `YT-A 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.YT)}` }
}
ctx.log('>>> 模拟操作: 点击 [转账] 按钮')
const transferTx = await ctx.walletClient.writeContract({
address: vault,
abi: VAULT_ABI,
functionName: 'transfer',
args: [ctx.address, amount],
account: ctx.account,
chain: currentChain,
gas: GAS_CONFIG.SIMPLE,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
ctx.log(` 交易已发送: ${transferTx.slice(0, 18)}...`)
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: transferTx })
if (receipt.status === 'success') {
return { success: true, message: `转账成功`, txHash: transferTx }
}
return { success: false, message: '交易失败', txHash: transferTx }
}
},
{
id: 'lp_add',
name: isZh ? 'LP 添加流动性' : 'LP Add Liquidity',
description: isZh ? `USDC ${TEST_AMOUNTS.SMALL} -> 添加` : `USDC ${TEST_AMOUNTS.SMALL} -> Add`,
run: async (ctx) => {
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.USDC)
const router = CONTRACTS.YT_REWARD_ROUTER as `0x${string}`
ctx.log('>>> 模拟操作: 进入 LP 流动池页面')
ctx.log(`>>> 模拟操作: 输入金额 ${TEST_AMOUNTS.SMALL}`)
const balance = await ctx.publicClient.readContract({
address: CONTRACTS.USDC,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [ctx.address],
})
if (balance < amount) {
return { success: false, message: `USDC 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.USDC)}` }
}
const approved = await ensureApproval(ctx, CONTRACTS.USDC as `0x${string}`, router, amount, 'USDC', TOKEN_DECIMALS.USDC)
if (!approved) return { success: false, message: '授权失败' }
ctx.log('>>> 模拟操作: 点击 [添加流动性] 按钮')
const addTx = await ctx.walletClient.writeContract({
address: router,
abi: ROUTER_ABI,
functionName: 'addLiquidity',
args: [CONTRACTS.USDC as `0x${string}`, amount, 0n, 0n],
account: ctx.account,
chain: currentChain,
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
ctx.log(` 交易已发送: ${addTx.slice(0, 18)}...`)
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: addTx })
if (receipt.status === 'success') {
return { success: true, message: `添加成功`, txHash: addTx }
}
return { success: false, message: '交易失败', txHash: addTx }
}
},
{
id: 'lp_remove',
name: isZh ? 'LP 移除流动性' : 'LP Remove Liquidity',
description: isZh ? `ytLP ${TEST_AMOUNTS.SMALL} -> 移除` : `ytLP ${TEST_AMOUNTS.SMALL} -> Remove`,
run: async (ctx) => {
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.YT_LP)
const router = CONTRACTS.YT_REWARD_ROUTER as `0x${string}`
ctx.log('>>> 模拟操作: 切换到移除流动性 Tab')
const balance = await ctx.publicClient.readContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [ctx.address],
})
if (balance < amount) {
return { success: false, message: `ytLP 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.YT_LP)}` }
}
const approved = await ensureApproval(ctx, CONTRACTS.YT_LP_TOKEN as `0x${string}`, router, amount, 'ytLP', TOKEN_DECIMALS.YT_LP)
if (!approved) return { success: false, message: '授权失败' }
ctx.log('>>> 模拟操作: 点击 [移除流动性] 按钮')
const removeTx = await ctx.walletClient.writeContract({
address: router,
abi: ROUTER_ABI,
functionName: 'removeLiquidity',
args: [CONTRACTS.USDC as `0x${string}`, amount, 0n, ctx.address],
account: ctx.account,
chain: currentChain,
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
ctx.log(` 交易已发送: ${removeTx.slice(0, 18)}...`)
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: removeTx })
if (receipt.status === 'success') {
return { success: true, message: `移除成功`, txHash: removeTx }
}
return { success: false, message: '交易失败', txHash: removeTx }
}
},
{
id: 'lp_swap',
name: isZh ? 'LP Swap' : 'LP Swap',
description: isZh ? `USDC ${TEST_AMOUNTS.SMALL} -> YT-A` : `USDC ${TEST_AMOUNTS.SMALL} -> YT-A`,
run: async (ctx) => {
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.USDC)
const router = CONTRACTS.YT_REWARD_ROUTER as `0x${string}`
ctx.log('>>> 模拟操作: 切换到代币互换 Tab')
const balance = await ctx.publicClient.readContract({
address: CONTRACTS.USDC,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [ctx.address],
})
if (balance < amount) {
return { success: false, message: `USDC 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.USDC)}` }
}
const approved = await ensureApproval(ctx, CONTRACTS.USDC as `0x${string}`, router, amount, 'USDC', TOKEN_DECIMALS.USDC)
if (!approved) return { success: false, message: '授权失败' }
ctx.log('>>> 模拟操作: 点击 [交换] 按钮')
const swapTx = await ctx.walletClient.writeContract({
address: router,
abi: ROUTER_ABI,
functionName: 'swapYT',
args: [
CONTRACTS.USDC as `0x${string}`,
CONTRACTS.VAULTS.YT_A as `0x${string}`,
amount,
0n,
ctx.address
],
account: ctx.account,
chain: currentChain,
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
ctx.log(` 交易已发送: ${swapTx.slice(0, 18)}...`)
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: swapTx })
if (receipt.status === 'success') {
return { success: true, message: `Swap 成功`, txHash: swapTx }
}
return { success: false, message: '交易失败', txHash: swapTx }
}
},
]
// 运行测试
const runTests = async () => {
setIsRunning(true)
setLogs([])
abortRef.current = false
let publicClient: PublicClient
let walletClient: WalletClient
let account: Account | `0x${string}`
let address: `0x${string}`
try {
if (mode === 'privateKey') {
if (!privateKey || !privateKey.startsWith('0x')) {
addLog('[ERROR] 请输入有效的私钥 (0x开头)')
setIsRunning(false)
return
}
const acc = privateKeyToAccount(privateKey as `0x${string}`)
account = acc
address = acc.address
publicClient = createPublicClient({
chain: currentChain,
transport: http(),
})
walletClient = createWalletClient({
account: acc,
chain: currentChain,
transport: http(),
})
addLog(`[INFO] 私钥模式 - 全自动测试`)
addLog(`[INFO] 测试地址: ${address}`)
} else {
if (!isConnected || !connectedAddress || !walletClientHook || !publicClientHook) {
addLog('[ERROR] 请先连接钱包')
setIsRunning(false)
return
}
publicClient = publicClientHook as PublicClient
walletClient = walletClientHook as WalletClient
account = connectedAddress
address = connectedAddress
addLog(`[INFO] 钱包模式 - 需要手动确认交易`)
addLog(`[INFO] 测试地址: ${address}`)
}
const testsToRun = testItems.filter(t => selectedTests.includes(t.id))
setResults(testsToRun.map(t => ({
name: t.name,
status: 'pending',
})))
addLog(``)
addLog(`========================================`)
addLog(` 开始运行 ${testsToRun.length} 项测试`)
addLog(`========================================`)
for (let i = 0; i < testsToRun.length; i++) {
if (abortRef.current) {
addLog('[INFO] 测试已中止')
break
}
const test = testsToRun[i]
addLog(``)
addLog(`----------------------------------------`)
addLog(`[${i + 1}/${testsToRun.length}] ${test.name}`)
addLog(`----------------------------------------`)
setResults(prev => prev.map((r, idx) =>
idx === i ? { ...r, status: 'running' } : r
))
const startTime = Date.now()
try {
const ctx: TestContext = {
publicClient,
walletClient,
account,
address,
log: addLog,
}
const result = await test.run(ctx)
const duration = Date.now() - startTime
setResults(prev => prev.map((r, idx) =>
idx === i ? {
...r,
status: result.success ? 'success' : 'failed',
message: result.message,
txHash: result.txHash,
duration,
} : r
))
if (result.success) {
addLog(`[PASS] ${result.message} (${duration}ms)`)
} else {
addLog(`[FAIL] ${result.message}`)
}
} catch (error: any) {
const duration = Date.now() - startTime
const errorMsg = error?.shortMessage || error?.message || '未知错误'
setResults(prev => prev.map((r, idx) =>
idx === i ? {
...r,
status: 'failed',
message: errorMsg,
duration,
} : r
))
addLog(`[ERROR] ${errorMsg}`)
}
await new Promise(r => setTimeout(r, 500))
}
addLog(``)
addLog(`========================================`)
addLog(` 测试完成`)
addLog(`========================================`)
} catch (error: any) {
addLog(`[ERROR] 初始化失败: ${error.message}`)
}
setIsRunning(false)
}
const stopTests = () => {
abortRef.current = true
addLog('[INFO] 正在停止测试...')
}
const toggleTest = (id: string) => {
setSelectedTests(prev =>
prev.includes(id)
? prev.filter(t => t !== id)
: [...prev, id]
)
}
const toggleAll = () => {
if (selectedTests.length === testItems.length) {
setSelectedTests([])
} else {
setSelectedTests(testItems.map(t => t.id))
}
}
const stats = {
total: results.length,
success: results.filter(r => r.status === 'success').length,
failed: results.filter(r => r.status === 'failed').length,
pending: results.filter(r => r.status === 'pending' || r.status === 'running').length,
}
return (
<div className="panel">
<h2>{t('nav.autoTest')}</h2>
{/* 模式选择 */}
<div className="test-status">
<div className="form-group">
<label style={{ marginBottom: '12px', display: 'block' }}>
{isZh ? '测试模式' : 'Test Mode'}
</label>
<div style={{ display: 'flex', gap: '24px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="radio"
name="testMode"
checked={mode === 'wallet'}
onChange={() => setMode('wallet')}
disabled={isRunning}
style={{ width: '16px', height: '16px' }}
/>
<span>{isZh ? '钱包模式 (需手动确认)' : 'Wallet Mode (Manual)'}</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="radio"
name="testMode"
checked={mode === 'privateKey'}
onChange={() => setMode('privateKey')}
disabled={isRunning}
style={{ width: '16px', height: '16px' }}
/>
<span>{isZh ? '私钥模式 (全自动)' : 'Private Key Mode (Auto)'}</span>
</label>
</div>
</div>
{mode === 'privateKey' && (
<div className="form-group" style={{ marginTop: '16px' }}>
<label>
{isZh ? '测试私钥' : 'Test Private Key'}
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#ff9800' }}>
{isZh ? '仅用于测试网' : 'Testnet Only'}
</span>
</label>
<input
type="password"
className="input"
placeholder="0x..."
value={privateKey}
onChange={(e) => setPrivateKey(e.target.value)}
disabled={isRunning}
/>
</div>
)}
{mode === 'wallet' && (
<div
style={{
marginTop: '16px',
padding: '12px 16px',
borderRadius: '6px',
background: isConnected ? '#e8f5e9' : '#fff3e0',
color: isConnected ? '#2e7d32' : '#e65100',
fontSize: '14px'
}}
>
{isConnected
? `[OK] ${isZh ? '已连接' : 'Connected'}: ${connectedAddress?.slice(0, 6)}...${connectedAddress?.slice(-4)}`
: `[!] ${isZh ? '请先连接钱包' : 'Please connect wallet first'}`
}
</div>
)}
</div>
{/* 测试项选择 */}
<div className="section" style={{ marginTop: '20px', paddingTop: '16px' }}>
<div className="section-header">
<h3>{isZh ? '选择测试项' : 'Select Tests'}</h3>
<button
className="btn btn-secondary btn-sm"
onClick={toggleAll}
disabled={isRunning}
>
{selectedTests.length === testItems.length
? (isZh ? '取消全选' : 'Deselect All')
: (isZh ? '全选' : 'Select All')
}
</button>
</div>
<div className="test-grid" style={{ marginTop: '12px' }}>
{testItems.map(test => (
<div
key={test.id}
className="test-card"
style={{
opacity: isRunning ? 0.7 : 1,
cursor: isRunning ? 'not-allowed' : 'pointer',
}}
onClick={() => !isRunning && toggleTest(test.id)}
>
<div className="test-card-left">
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={selectedTests.includes(test.id)}
onChange={() => toggleTest(test.id)}
disabled={isRunning}
style={{ width: '14px', height: '14px' }}
onClick={(e) => e.stopPropagation()}
/>
<span className="test-name">{test.name}</span>
</div>
<p className="test-desc" style={{ marginLeft: '22px' }}>{test.description}</p>
</div>
</div>
))}
</div>
</div>
{/* 操作按钮 */}
<div style={{ display: 'flex', gap: '12px', marginTop: '20px' }}>
{!isRunning ? (
<button
className="btn btn-primary"
onClick={runTests}
disabled={selectedTests.length === 0 || (mode === 'wallet' && !isConnected)}
style={{ flex: 1 }}
>
{isZh ? `开始测试 (${selectedTests.length})` : `Start Test (${selectedTests.length})`}
</button>
) : (
<button
className="btn btn-danger"
onClick={stopTests}
style={{ flex: 1 }}
>
{isZh ? '停止测试' : 'Stop Test'}
</button>
)}
<button
className="btn btn-secondary"
onClick={() => { setLogs([]); setResults([]) }}
disabled={isRunning}
>
{isZh ? '清空' : 'Clear'}
</button>
</div>
{/* 测试结果 */}
{results.length > 0 && (
<div className="section" style={{ marginTop: '24px', paddingTop: '20px' }}>
<div className="section-header">
<h3>{isZh ? '测试结果' : 'Test Results'}</h3>
<div style={{ fontSize: '13px', color: '#666' }}>
{isZh ? '通过' : 'Pass'}: {stats.success} |
{isZh ? ' 失败' : ' Fail'}: {stats.failed} |
{isZh ? ' 等待' : ' Pending'}: {stats.pending}
</div>
</div>
<div style={{ marginTop: '12px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
{results.map((result, idx) => (
<div
key={idx}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 14px',
borderRadius: '6px',
background: result.status === 'success' ? '#e8f5e9' :
result.status === 'failed' ? '#ffebee' :
result.status === 'running' ? '#e3f2fd' :
'#f5f5f5',
borderLeft: `3px solid ${
result.status === 'success' ? '#4caf50' :
result.status === 'failed' ? '#f44336' :
result.status === 'running' ? '#2196f3' :
'#ddd'
}`,
}}
>
<span style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '13px',
fontWeight: 500,
color: result.status === 'success' ? '#2e7d32' :
result.status === 'failed' ? '#c62828' :
result.status === 'running' ? '#1565c0' :
'#666'
}}>
<span style={{
fontFamily: 'monospace',
fontSize: '11px',
padding: '2px 6px',
background: 'rgba(0,0,0,0.05)',
borderRadius: '3px'
}}>
{result.status === 'pending' && '...'}
{result.status === 'running' && 'RUN'}
{result.status === 'success' && 'OK'}
{result.status === 'failed' && 'FAIL'}
</span>
{result.name}
</span>
<span style={{ fontSize: '12px', color: '#666', maxWidth: '220px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{result.message}
{result.duration && ` (${result.duration}ms)`}
</span>
</div>
))}
</div>
</div>
)}
{/* 日志输出 */}
{logs.length > 0 && (
<div className="section" style={{ marginTop: '24px', paddingTop: '20px' }}>
<h3>{isZh ? '执行日志' : 'Execution Log'}</h3>
<div
style={{
marginTop: '12px',
background: '#1a1a1a',
color: '#e0e0e0',
borderRadius: '6px',
padding: '12px 16px',
height: '280px',
overflowY: 'auto',
fontFamily: 'Monaco, Consolas, monospace',
fontSize: '12px',
lineHeight: '1.6',
}}
>
{logs.map((log, idx) => (
<div
key={idx}
style={{
whiteSpace: 'pre-wrap',
color: log.includes('[ERROR]') ? '#f44336' :
log.includes('[FAIL]') ? '#ff9800' :
log.includes('[PASS]') ? '#4caf50' :
log.includes('[INFO]') ? '#2196f3' :
log.includes('>>>') ? '#9e9e9e' :
'#e0e0e0'
}}
>
{log}
</div>
))}
<div ref={logsEndRef} />
</div>
</div>
)}
</div>
)
}

View File

@@ -11,9 +11,10 @@ const clearAllCache = () => {
key.startsWith('wc@') ||
key.startsWith('wagmi') ||
key.startsWith('@w3m') ||
key.startsWith('@reown') ||
key.includes('walletconnect') ||
key.includes('WalletConnect') ||
key === 'yt_asset_tx_history' // 也清除可能损坏的交易历史
key.includes('appkit')
)) {
keysToRemove.push(key)
}
@@ -25,17 +26,55 @@ const clearAllCache = () => {
export function ConnectButton() {
const { t } = useTranslation()
const { open } = useWeb3Modal()
const { address, isConnected } = useAccount()
const { address, isConnected, connector } = useAccount()
const { disconnect } = useDisconnect()
const formatAddress = (addr: string) => {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
}
// 断开连接并清理缓存
const handleDisconnect = () => {
disconnect()
// 断开连接并清理缓存,然后刷新页面
const handleDisconnect = async () => {
try {
disconnect()
await new Promise(resolve => setTimeout(resolve, 100))
} catch {
// ignore
}
clearAllCache()
// 设置跳过自动重连标记
sessionStorage.setItem('skipReconnect', 'true')
window.location.reload()
}
// 切换账户:弹出钱包账户选择
const handleSwitchAccount = async () => {
try {
// 对于 injected 钱包MetaMask 等),使用 wallet_requestPermissions
if (connector?.id === 'injected' || connector?.id === 'metaMask' || connector?.id === 'io.metamask') {
const provider = await connector.getProvider() as { request?: (args: { method: string; params?: unknown[] }) => Promise<unknown> }
if (provider?.request) {
await provider.request({
method: 'wallet_requestPermissions',
params: [{ eth_accounts: {} }],
})
return
}
}
} catch (err) {
console.log('Switch account error:', err)
}
// 失败或其他钱包:断开后重新连接
try {
disconnect()
await new Promise(resolve => setTimeout(resolve, 100))
} catch {
// ignore
}
clearAllCache()
sessionStorage.setItem('skipReconnect', 'true')
sessionStorage.setItem('autoOpenConnect', 'true')
window.location.reload()
}
// 重置连接(清理缓存后刷新)
@@ -47,9 +86,14 @@ export function ConnectButton() {
if (isConnected && address) {
return (
<div className="connect-info">
<span className="address">{formatAddress(address)}</span>
<button onClick={handleDisconnect} className="btn btn-secondary">
{t('common.disconnect')}
<span className="address" title={address}>
{formatAddress(address)}
</span>
<button onClick={handleSwitchAccount} className="btn btn-outline btn-sm">
</button>
<button onClick={handleDisconnect} className="btn btn-secondary btn-sm">
</button>
</div>
)

View File

@@ -1,31 +1,35 @@
import { useState, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt, useChainId } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
import { CONTRACTS, GAS_CONFIG, FACTORY_ABI, VAULT_ABI } from '../config/contracts'
import { GAS_CONFIG, FACTORY_ABI, VAULT_ABI, getContracts, getDecimals, getChainName } from '../config/contracts'
export function FactoryPanel() {
const { t } = useTranslation()
const { address, isConnected } = useAccount()
// ===== 多链支持 =====
const chainId = useChainId()
const CONTRACTS = getContracts(chainId)
const TOKEN_DECIMALS = getDecimals(chainId)
const currentChainName = getChainName(chainId)
const [createForm, setCreateForm] = useState({
name: '',
symbol: '',
manager: '',
hardCap: '',
redemptionTime: '',
initialWusdPrice: '1',
initialYtPrice: '1',
})
const [priceForm, setPriceForm] = useState({
vault: '',
wusdPrice: '',
ytPrice: '',
})
const [showPermissionTest, setShowPermissionTest] = useState(false)
const [showOwnerConfig, setShowOwnerConfig] = useState(false)
const [newDefaultHardCap, setNewDefaultHardCap] = useState('')
const [showBatchOps, setShowBatchOps] = useState(false)
const [batchPriceForm, setBatchPriceForm] = useState({ wusdPrice: '1', ytPrice: '1' })
const [batchPriceForm, setBatchPriceForm] = useState({ ytPrice: '1' })
const [batchHardCapForm, setBatchHardCapForm] = useState('')
const [selectedVaultsForBatch, setSelectedVaultsForBatch] = useState<string[]>([])
const [showAdvanced, setShowAdvanced] = useState(false)
@@ -41,12 +45,18 @@ export function FactoryPanel() {
manager: string
hardCap: string
redemptionTime: string
wusdPrice: string
ytPrice: string
}[]>([
{ name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', wusdPrice: '1', ytPrice: '1' }
{ name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', ytPrice: '1' }
])
// 单金库管理配置 state
const [selectedVaultForManage, setSelectedVaultForManage] = useState('')
const [singleVaultPriceForm, setSingleVaultPriceForm] = useState({ ytPrice: '1' })
const [singleVaultHardCapForm, setSingleVaultHardCapForm] = useState('')
const [singleVaultRedemptionTime, setSingleVaultRedemptionTime] = useState('')
const [singleVaultManager, setSingleVaultManager] = useState('')
const { data: allVaults, refetch: refetchVaults } = useReadContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
@@ -138,7 +148,19 @@ export function FactoryPanel() {
}, [writeError])
const handleCreateVault = () => {
const redemptionTimestamp = Math.floor(new Date(createForm.redemptionTime).getTime() / 1000)
// 表单验证
if (!createForm.name || !createForm.symbol || !createForm.manager || !createForm.hardCap || !createForm.redemptionTime) {
console.error('请填写所有必填字段')
return
}
const redemptionDate = new Date(createForm.redemptionTime)
if (isNaN(redemptionDate.getTime())) {
console.error('无效的赎回时间')
return
}
const redemptionTimestamp = Math.floor(redemptionDate.getTime() / 1000)
writeContract({
address: CONTRACTS.FACTORY,
@@ -148,11 +170,11 @@ export function FactoryPanel() {
createForm.name,
createForm.symbol,
createForm.manager as `0x${string}`,
parseUnits(createForm.hardCap, 18),
CONTRACTS.WUSD,
parseUnits(createForm.hardCap, TOKEN_DECIMALS.YT), // hardCap 是 YT 代币数量上限,使用 18 位精度
CONTRACTS.USDC,
BigInt(redemptionTimestamp),
parseUnits(createForm.initialWusdPrice, 30), // wusdPrice 使用 30 位精度
parseUnits(createForm.initialYtPrice, 30), // ytPrice 使用 30 位精度
parseUnits(createForm.initialYtPrice, TOKEN_DECIMALS.INTERNAL_PRICE), // ytPrice 使用 30 位精度
CONTRACTS.USDC_PRICE_FEED, // USDC 价格来源 (Chainlink)
],
gas: GAS_CONFIG.VERY_COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
@@ -167,8 +189,7 @@ export function FactoryPanel() {
functionName: 'updateVaultPrices',
args: [
priceForm.vault as `0x${string}`,
parseUnits(priceForm.wusdPrice, 30), // wusdPrice 使用 30 位精度
parseUnits(priceForm.ytPrice, 30), // ytPrice 使用 30 位精度
parseUnits(priceForm.ytPrice, TOKEN_DECIMALS.INTERNAL_PRICE), // ytPrice 使用 30 位精度 (USDC价格来自Chainlink)
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
@@ -182,7 +203,7 @@ export function FactoryPanel() {
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setDefaultHardCap',
args: [parseUnits(newDefaultHardCap, 18)],
args: [parseUnits(newDefaultHardCap, TOKEN_DECIMALS.YT)], // hardCap 是 YT 代币数量上限,使用 18 位精度
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
@@ -192,13 +213,12 @@ export function FactoryPanel() {
// 批量更新价格
const handleBatchUpdatePrices = () => {
if (selectedVaultsForBatch.length === 0) return
const wusdPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.wusdPrice, 30)) // wusdPrice 使用 30 位精度
const ytPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.ytPrice, 30)) // ytPrice 使用 30 位精度
const ytPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.ytPrice, TOKEN_DECIMALS.INTERNAL_PRICE)) // ytPrice 使用 30 位精度 (USDC价格来自Chainlink)
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'updateVaultPricesBatch',
args: [selectedVaultsForBatch as `0x${string}`[], wusdPrices, ytPrices],
args: [selectedVaultsForBatch as `0x${string}`[], ytPrices],
gas: GAS_CONFIG.VERY_COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
@@ -208,7 +228,7 @@ export function FactoryPanel() {
// 批量设置硬顶
const handleBatchSetHardCap = () => {
if (selectedVaultsForBatch.length === 0 || !batchHardCapForm) return
const hardCaps = selectedVaultsForBatch.map(() => parseUnits(batchHardCapForm, 18))
const hardCaps = selectedVaultsForBatch.map(() => parseUnits(batchHardCapForm, TOKEN_DECIMALS.YT)) // hardCap 是 YT 代币数量上限
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
@@ -316,16 +336,15 @@ export function FactoryPanel() {
const names = validVaults.map(v => v.name)
const symbols = validVaults.map(v => v.symbol)
const managers = validVaults.map(v => v.manager as `0x${string}`)
const hardCaps = validVaults.map(v => parseUnits(v.hardCap, 18))
const hardCaps = validVaults.map(v => parseUnits(v.hardCap, TOKEN_DECIMALS.YT)) // hardCap 是 YT 代币数量上限
const redemptionTimes = validVaults.map(v => BigInt(Math.floor(new Date(v.redemptionTime).getTime() / 1000)))
const wusdPrices = validVaults.map(v => parseUnits(v.wusdPrice || '1', 30))
const ytPrices = validVaults.map(v => parseUnits(v.ytPrice || '1', 30))
const ytPrices = validVaults.map(v => parseUnits(v.ytPrice || '1', TOKEN_DECIMALS.INTERNAL_PRICE))
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'createVaultBatch',
args: [names, symbols, managers, hardCaps, CONTRACTS.WUSD, redemptionTimes, wusdPrices, ytPrices],
args: [names, symbols, managers, hardCaps, CONTRACTS.USDC, redemptionTimes, ytPrices, CONTRACTS.USDC_PRICE_FEED],
gas: GAS_CONFIG.VERY_COMPLEX * BigInt(validVaults.length),
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
@@ -334,7 +353,7 @@ export function FactoryPanel() {
// 添加新的金库配置行
const addBatchCreateRow = () => {
setBatchCreateVaults([...batchCreateVaults, { name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', wusdPrice: '1', ytPrice: '1' }])
setBatchCreateVaults([...batchCreateVaults, { name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', ytPrice: '1' }])
}
// 删除金库配置行
@@ -393,6 +412,69 @@ export function FactoryPanel() {
})
}
// ===== 单金库管理配置从VaultPanel移动过来=====
// 更新单个金库YT价格USDC价格来自Chainlink无需手动更新
const handleUpdateSingleVaultYTPrice = () => {
if (!selectedVaultForManage) return
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'updateVaultPrices',
args: [
selectedVaultForManage as `0x${string}`,
parseUnits(singleVaultPriceForm.ytPrice, TOKEN_DECIMALS.INTERNAL_PRICE),
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置单个金库硬顶
const handleSetSingleVaultHardCap = () => {
if (!selectedVaultForManage || !singleVaultHardCapForm) return
const hardCap = parseUnits(singleVaultHardCapForm, TOKEN_DECIMALS.YT)
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setHardCap',
args: [selectedVaultForManage as `0x${string}`, hardCap],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置单个金库赎回时间
const handleSetSingleVaultRedemptionTime = () => {
if (!selectedVaultForManage || !singleVaultRedemptionTime) return
const timestamp = BigInt(Math.floor(new Date(singleVaultRedemptionTime).getTime() / 1000))
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setVaultNextRedemptionTime',
args: [selectedVaultForManage as `0x${string}`, timestamp],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置单个金库管理员
const handleSetSingleVaultManager = () => {
if (!selectedVaultForManage || !singleVaultManager) return
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setVaultManager',
args: [selectedVaultForManage as `0x${string}`, singleVaultManager as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 权限测试函数
const runPermissionTest = (testType: string) => {
const testVault = allVaults && allVaults.length > 0 ? allVaults[0] : CONTRACTS.VAULTS.YT_A
@@ -403,7 +485,7 @@ export function FactoryPanel() {
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'updateVaultPrices',
args: [testVault as `0x${string}`, parseUnits('1', 30), parseUnits('1', 30)], // wusdPrice 和 ytPrice 都使用 30 位精度
args: [testVault as `0x${string}`, parseUnits('1', TOKEN_DECIMALS.INTERNAL_PRICE)], // 只有 ytPrice (USDC价格来自Chainlink)
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
@@ -458,8 +540,8 @@ export function FactoryPanel() {
<code style={{ fontSize: '10px' }}>{vaultImplementation || '-'}</code>
</div>
<div className="info-row">
<span title="默认硬顶 Default Hard Cap - 新创建金库的默认最大存款额度" style={{ cursor: 'help' }}>{t('factory.defaultHardCap')}:</span>
<strong>{defaultHardCap ? formatUnits(defaultHardCap, 18) : '0'}</strong>
<span title="默认硬顶 Default Hard Cap - 新创建金库的默认 YT 代币数量上限" style={{ cursor: 'help' }}>{t('factory.defaultHardCap')}:</span>
<strong>{defaultHardCap ? formatUnits(defaultHardCap, TOKEN_DECIMALS.YT) : '0'}</strong>
</div>
<div className="info-row">
<span title="金库总数 Total Vaults - 工厂已创建的金库数量" style={{ cursor: 'help' }}>{t('factory.totalVaults')}:</span>
@@ -486,9 +568,10 @@ export function FactoryPanel() {
padding: '2px 8px',
borderRadius: '4px',
fontSize: '11px',
background: isPaused ? '#ffebee' : '#e8f5e9',
color: isPaused ? '#c62828' : '#2e7d32',
fontWeight: 'bold'
background: isPaused ? '#fafafa' : '#f5f5f5',
color: isPaused ? '#666' : '#333',
fontWeight: 'bold',
border: '1px solid #e0e0e0'
}}>
{isPaused ? t('factory.paused') : t('factory.active')}
</span>
@@ -573,16 +656,6 @@ export function FactoryPanel() {
className="input"
/>
</div>
<div className="form-group">
<label>{t('factory.initialWusdPrice')}</label>
<input
type="number"
value={createForm.initialWusdPrice}
onChange={(e) => setCreateForm({ ...createForm, initialWusdPrice: e.target.value })}
placeholder="1"
className="input"
/>
</div>
<div className="form-group">
<label>{t('factory.initialYtPrice')}</label>
<input
@@ -673,7 +746,7 @@ export function FactoryPanel() {
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
<div>
<label style={{ fontSize: '10px', color: '#666' }}>{t('factory.redemptionTime')}</label>
<input
@@ -684,17 +757,6 @@ export function FactoryPanel() {
style={{ fontSize: '12px' }}
/>
</div>
<div>
<label style={{ fontSize: '10px', color: '#666' }}>{t('vault.wusdPrice')}</label>
<input
type="number"
value={vault.wusdPrice}
onChange={(e) => updateBatchCreateRow(index, 'wusdPrice', e.target.value)}
placeholder="1"
className="input"
style={{ fontSize: '12px' }}
/>
</div>
<div>
<label style={{ fontSize: '10px', color: '#666' }}>{t('vault.ytPrice')}</label>
<input
@@ -733,7 +795,7 @@ export function FactoryPanel() {
</div>
<div className="section">
<h3 title="更新价格 Update Prices - 修改指定金库的 WUSD 和 YT 价格" style={{ cursor: 'help' }}>{t('factory.updatePrices')}</h3>
<h3 title="更新价格 Update Prices - 修改指定金库的 YT 价格 (USDC 价格来自 Chainlink)" style={{ cursor: 'help' }}>{t('factory.updatePrices')}</h3>
<div className="form-grid">
<div className="form-group">
<label title="金库地址 Vault Address - 选择要更新价格的金库" style={{ cursor: 'help' }}>{t('factory.vaultAddress')}</label>
@@ -750,16 +812,6 @@ export function FactoryPanel() {
))}
</select>
</div>
<div className="form-group">
<label>{t('factory.newWusdPrice')}</label>
<input
type="number"
value={priceForm.wusdPrice}
onChange={(e) => setPriceForm({ ...priceForm, wusdPrice: e.target.value })}
placeholder="e.g. 1.05"
className="input"
/>
</div>
<div className="form-group">
<label>{t('factory.newYtPrice')}</label>
<input
@@ -794,6 +846,128 @@ export function FactoryPanel() {
{showOwnerConfig && (
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
{/* 单金库管理配置 */}
<div style={{ marginBottom: '20px', padding: '12px', background: '#f8f9fa', borderRadius: '8px', border: '1px solid #e0e0e0' }}>
<h5 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: 'bold', color: '#333' }}></h5>
{/* 选择金库 */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}></label>
<select
value={selectedVaultForManage}
onChange={(e) => setSelectedVaultForManage(e.target.value)}
className="input"
style={{ fontSize: '13px' }}
>
<option value="">-- --</option>
{allVaults?.map((vault, index) => (
<option key={index} value={vault}>
Vault {index + 1}: {vault.slice(0, 10)}...
</option>
))}
</select>
</div>
{selectedVaultForManage && (
<>
{/* 更新金库YT价格USDC价格来自Chainlink */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}> YT </label>
<p style={{ fontSize: '11px', color: '#666', marginBottom: '8px' }}>
USDC Chainlink
</p>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<div style={{ flex: 1 }}>
<label style={{ fontSize: '11px', color: '#666' }}>YT (30)</label>
<input
type="number"
value={singleVaultPriceForm.ytPrice}
onChange={(e) => setSingleVaultPriceForm({ ytPrice: e.target.value })}
placeholder="1"
className="input"
style={{ fontSize: '13px' }}
step="0.01"
/>
</div>
<button
onClick={handleUpdateSingleVaultYTPrice}
disabled={isProcessing}
className="btn btn-primary btn-sm"
style={{ marginTop: '18px' }}
>
{isProcessing ? '...' : '更新价格'}
</button>
</div>
</div>
{/* 设置硬顶 */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}></label>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="number"
value={singleVaultHardCapForm}
onChange={(e) => setSingleVaultHardCapForm(e.target.value)}
placeholder="100000"
className="input"
style={{ flex: 1, fontSize: '13px' }}
/>
<button
onClick={handleSetSingleVaultHardCap}
disabled={isProcessing || !singleVaultHardCapForm}
className="btn btn-secondary btn-sm"
>
{isProcessing ? '...' : '设置'}
</button>
</div>
</div>
{/* 设置赎回时间 */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}></label>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="datetime-local"
value={singleVaultRedemptionTime}
onChange={(e) => setSingleVaultRedemptionTime(e.target.value)}
className="input"
style={{ flex: 1, fontSize: '13px' }}
/>
<button
onClick={handleSetSingleVaultRedemptionTime}
disabled={isProcessing || !singleVaultRedemptionTime}
className="btn btn-secondary btn-sm"
>
{isProcessing ? '...' : '设置'}
</button>
</div>
</div>
{/* 设置管理员 */}
<div>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}></label>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={singleVaultManager}
onChange={(e) => setSingleVaultManager(e.target.value)}
placeholder="0x..."
className="input"
style={{ flex: 1, fontSize: '13px' }}
/>
<button
onClick={handleSetSingleVaultManager}
disabled={isProcessing || !singleVaultManager}
className="btn btn-secondary btn-sm"
>
{isProcessing ? '...' : '设置'}
</button>
</div>
</div>
</>
)}
</div>
{/* 设置默认硬顶 */}
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}>
@@ -804,7 +978,7 @@ export function FactoryPanel() {
type="number"
value={newDefaultHardCap}
onChange={(e) => setNewDefaultHardCap(e.target.value)}
placeholder={defaultHardCap ? formatUnits(defaultHardCap, 18) : '1000000'}
placeholder={defaultHardCap ? formatUnits(defaultHardCap, TOKEN_DECIMALS.YT) : '1000000'}
className="input"
style={{ flex: 1 }}
/>
@@ -848,11 +1022,11 @@ export function FactoryPanel() {
alignItems: 'center',
gap: '4px',
padding: '4px 8px',
background: selectedVaultsForBatch.includes(vault) ? '#e3f2fd' : '#f8f9fa',
background: selectedVaultsForBatch.includes(vault) ? '#f5f5f5' : '#fafafa',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
border: selectedVaultsForBatch.includes(vault) ? '1px solid #2196f3' : '1px solid #e0e0e0'
border: selectedVaultsForBatch.includes(vault) ? '1px solid #999' : '1px solid #e0e0e0'
}}>
<input
type="checkbox"
@@ -872,17 +1046,8 @@ export function FactoryPanel() {
{/* 批量更新价格 */}
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.batchUpdatePrices')}</h5>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: '8px', alignItems: 'end' }}>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('vault.wusdPrice')}</label>
<input
type="number"
value={batchPriceForm.wusdPrice}
onChange={(e) => setBatchPriceForm({ ...batchPriceForm, wusdPrice: e.target.value })}
className="input"
style={{ fontSize: '13px' }}
/>
</div>
<p style={{ fontSize: '11px', color: '#666', margin: '0 0 8px 0' }}>USDC Chainlink </p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '8px', alignItems: 'end' }}>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('vault.ytPrice')}</label>
<input
@@ -947,14 +1112,14 @@ export function FactoryPanel() {
</div>
{/* 批量暂停/恢复金库 */}
<div style={{ padding: '10px', background: '#fff3e0', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px', color: '#e65100' }}>{t('factory.batchPauseUnpause')}</h5>
<div style={{ padding: '10px', background: '#f5f5f5', borderRadius: '6px', border: '1px solid #e0e0e0' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px', color: '#666' }}>{t('factory.batchPauseUnpause')}</h5>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={handleBatchPauseVaults}
disabled={isProcessing || selectedVaultsForBatch.length === 0}
className="btn btn-sm"
style={{ flex: 1, background: '#ff9800', color: '#fff', border: 'none' }}
style={{ flex: 1, background: '#666', color: '#fff', border: 'none' }}
>
{isProcessing ? '...' : t('factory.pauseSelected')}
</button>
@@ -962,12 +1127,12 @@ export function FactoryPanel() {
onClick={handleBatchUnpauseVaults}
disabled={isProcessing || selectedVaultsForBatch.length === 0}
className="btn btn-sm"
style={{ flex: 1, background: '#4caf50', color: '#fff', border: 'none' }}
style={{ flex: 1, background: '#333', color: '#fff', border: 'none' }}
>
{isProcessing ? '...' : t('factory.unpauseSelected')}
</button>
</div>
<div style={{ fontSize: '11px', color: '#e65100', marginTop: '6px' }}>
<div style={{ fontSize: '11px', color: '#666', marginTop: '6px' }}>
{t('factory.pauseWarning')}
</div>
</div>
@@ -988,7 +1153,7 @@ export function FactoryPanel() {
{showAdvanced && (
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
{/* 警告提示 */}
<div style={{ marginBottom: '16px', padding: '8px', background: '#fff3e0', borderRadius: '4px', fontSize: '12px', color: '#e65100' }}>
<div style={{ marginBottom: '16px', padding: '8px', background: '#f5f5f5', borderRadius: '4px', fontSize: '12px', color: '#666', border: '1px solid #e0e0e0' }}>
{t('factory.advancedWarning')}
</div>

View File

@@ -0,0 +1,248 @@
.holders-panel {
width: 100%;
}
.holders-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.holders-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text);
}
.holders-actions {
display: flex;
align-items: center;
gap: 16px;
}
.update-btn {
padding: 10px 20px;
background: var(--color-primary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
}
.update-btn:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.update-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.last-update {
color: var(--color-text-secondary);
font-size: 14px;
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: var(--color-bg-card);
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.stat-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.stat-card.active {
border-color: var(--color-primary);
background: var(--color-primary-light);
}
.stat-type {
font-size: 14px;
font-weight: 500;
color: var(--color-text-secondary);
margin-bottom: 8px;
}
.stat-count {
font-size: 24px;
font-weight: 600;
color: var(--color-text);
}
/* 持有者列表 */
.holders-list {
background: var(--color-bg-card);
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--color-border);
}
.list-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text);
}
.holder-count {
color: var(--color-text-secondary);
font-size: 14px;
}
/* 表格 */
.table-container {
overflow-x: auto;
}
.holders-table {
width: 100%;
border-collapse: collapse;
}
.holders-table thead {
background: var(--color-bg-section);
}
.holders-table th {
padding: 12px 16px;
text-align: left;
font-weight: 500;
color: var(--color-text-secondary);
font-size: 14px;
border-bottom: 1px solid var(--color-border);
}
.holders-table td {
padding: 16px;
border-bottom: 1px solid var(--color-border-light);
font-size: 14px;
color: var(--color-text);
}
.holders-table tbody tr {
transition: background-color 0.2s;
}
.holders-table tbody tr:hover {
background-color: var(--color-bg-section);
}
.rank {
font-weight: 600;
color: var(--color-text);
}
.address a {
color: var(--color-primary);
text-decoration: none;
font-family: monospace;
transition: color 0.2s;
}
.address a:hover {
color: var(--color-primary-dark);
text-decoration: underline;
}
.balance {
font-weight: 500;
color: var(--color-text);
font-family: monospace;
}
.holding-time {
color: var(--color-text-secondary);
}
.last-updated {
color: var(--color-text-muted);
font-size: 13px;
}
/* 加载和错误状态 */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid var(--color-border);
border-top: 4px solid var(--color-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.no-data {
text-align: center;
padding: 60px 20px;
color: var(--color-text-muted);
}
.error-message {
background: var(--color-warning-bg);
border: 1px solid var(--color-warning);
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 20px;
color: var(--color-text);
}
/* 响应式 */
@media (max-width: 768px) {
.holders-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.table-container {
overflow-x: scroll;
}
.holders-table {
min-width: 600px;
}
}

View File

@@ -0,0 +1,268 @@
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { formatUnits } from 'viem'
import { useChainId } from 'wagmi'
import { getDecimals } from '../config/contracts'
import './HoldersPanel.css'
interface Holder {
id: number
holder_address: string
token_type: string
token_address: string
balance: string
first_seen: number
last_updated: number
}
interface Stats {
token_type: string
holder_count: number
total_balance: number
}
const API_BASE_URL = '/api'
export function HoldersPanel() {
const { t } = useTranslation()
// ===== 多链支持 =====
const chainId = useChainId()
const TOKEN_DECIMALS = getDecimals(chainId)
const [selectedType, setSelectedType] = useState<string>('YT-A')
const [holders, setHolders] = useState<Holder[]>([])
const [stats, setStats] = useState<Stats[]>([])
const [loading, setLoading] = useState(false)
const [lastUpdate, setLastUpdate] = useState<Date | null>(null)
const [error, setError] = useState<string | null>(null)
const tokenTypes = ['YT-A', 'YT-B', 'YT-C', 'ytLP', 'Lending']
// 获取统计数据
const fetchStats = async () => {
try {
const response = await fetch(`${API_BASE_URL}/stats`)
const data = await response.json()
if (data.success) {
setStats(data.data)
}
} catch (err) {
console.error('Failed to fetch stats:', err)
}
}
// 获取持有者数据
const fetchHolders = async (type: string) => {
setLoading(true)
setError(null)
try {
const response = await fetch(`${API_BASE_URL}/holders/${type}`)
const data = await response.json()
if (data.success) {
setHolders(data.data)
setLastUpdate(new Date())
} else {
setError(data.error || 'Failed to fetch holders')
}
} catch (err) {
setError('Network error: ' + (err as Error).message)
console.error('Failed to fetch holders:', err)
} finally {
setLoading(false)
}
}
// 手动触发更新
const triggerUpdate = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(`${API_BASE_URL}/update`, {
method: 'POST'
})
const data = await response.json()
if (data.success) {
// 等待后端更新完成
setTimeout(async () => {
try {
await Promise.all([
fetchHolders(selectedType),
fetchStats()
])
} catch (err) {
setError('Failed to refresh data: ' + (err as Error).message)
} finally {
setLoading(false)
}
}, 5000)
} else {
setError(data.error || 'Update failed')
setLoading(false)
}
} catch (err) {
setError('Failed to trigger update: ' + (err as Error).message)
setLoading(false)
}
}
// 初始加载和类型切换时获取数据
useEffect(() => {
fetchHolders(selectedType)
}, [selectedType])
// 初始加载统计数据
useEffect(() => {
fetchStats()
// 每30秒刷新一次统计
const interval = setInterval(fetchStats, 30000)
return () => clearInterval(interval)
}, [])
// 格式化地址显示前6位和后4位
const formatAddress = (address: string) => {
return `${address.slice(0, 6)}...${address.slice(-4)}`
}
// 格式化余额
const formatBalance = (balance: string) => {
try {
// ✅ 使用配置中的 YT 代币精度
const formatted = formatUnits(BigInt(balance), TOKEN_DECIMALS.YT)
return parseFloat(formatted).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 6
})
} catch {
return balance
}
}
// 计算持有时长
const formatHoldingTime = (firstSeen: number) => {
const now = Math.floor(Date.now() / 1000)
const duration = now - firstSeen
const days = Math.floor(duration / 86400)
const hours = Math.floor((duration % 86400) / 3600)
const minutes = Math.floor((duration % 3600) / 60)
if (days > 0) return `${days}${t('holders.days')} ${hours}${t('holders.hours')}`
if (hours > 0) return `${hours}${t('holders.hours')} ${minutes}${t('holders.minutes')}`
return `${minutes}${t('holders.minutes')}`
}
// 获取某类型的统计数据
const getStatForType = (type: string) => {
return stats.find(s => s.token_type === type)
}
return (
<div className="holders-panel">
<div className="holders-header">
<h2>{t('holders.title')}</h2>
<div className="holders-actions">
<button
className="update-btn"
onClick={triggerUpdate}
disabled={loading}
>
{t('holders.updateNow')}
</button>
{lastUpdate && (
<span className="last-update">
{t('holders.lastUpdate')}: {lastUpdate.toLocaleTimeString()}
</span>
)}
</div>
</div>
{/* 统计卡片 */}
<div className="stats-grid">
{tokenTypes.map(type => {
const stat = getStatForType(type)
return (
<div
key={type}
className={`stat-card ${selectedType === type ? 'active' : ''}`}
onClick={() => setSelectedType(type)}
>
<div className="stat-type">{type}</div>
<div className="stat-count">
{stat?.holder_count || 0} {t('holders.holders')}
</div>
</div>
)
})}
</div>
{/* 错误提示 */}
{error && (
<div className="error-message">
{error}
</div>
)}
{/* 持有者列表 */}
<div className="holders-list">
<div className="list-header">
<h3>{selectedType} {t('holders.holdersList')}</h3>
<span className="holder-count">
{t('holders.total')}: {holders.length}
</span>
</div>
{loading ? (
<div className="loading">
<div className="spinner"></div>
<p>{t('common.loading')}</p>
</div>
) : holders.length === 0 ? (
<div className="no-data">
<p>{t('holders.noHolders')}</p>
</div>
) : (
<div className="table-container">
<table className="holders-table">
<thead>
<tr>
<th>{t('holders.rank')}</th>
<th>{t('holders.address')}</th>
<th>{t('holders.balance')}</th>
<th>{t('holders.holdingTime')}</th>
<th>{t('holders.lastUpdated')}</th>
</tr>
</thead>
<tbody>
{holders.map((holder, index) => (
<tr key={holder.id}>
<td className="rank">#{index + 1}</td>
<td className="address">
<a
href={`https://sepolia.arbiscan.io/address/${holder.holder_address}`}
target="_blank"
rel="noopener noreferrer"
>
{formatAddress(holder.holder_address)}
</a>
</td>
<td className="balance">
{formatBalance(holder.balance)}
</td>
<td className="holding-time">
{formatHoldingTime(holder.first_seen)}
</td>
<td className="last-updated">
{new Date(holder.last_updated * 1000).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,247 @@
/**
* LP 边界测试组件
* 用于测试各种边界条件
*/
import { useTranslation } from 'react-i18next'
import { parseUnits } from 'viem'
import {
CONTRACTS,
GAS_CONFIG,
TOKEN_DECIMALS,
YT_REWARD_ROUTER_ABI,
} from '../../config/contracts'
import type { TransactionType } from '../../context/TransactionContext'
interface LPBoundaryTestProps {
address: `0x${string}` | undefined
ytLPBalance?: bigint
isProcessing: boolean
writeContract: (config: any) => void
recordTx: (type: TransactionType, amount?: string, token?: string) => void
show: boolean
onToggle: () => void
}
export function LPBoundaryTest({
address,
ytLPBalance,
isProcessing,
writeContract,
recordTx,
show,
onToggle,
}: LPBoundaryTestProps) {
const { t } = useTranslation()
const runBoundaryTest = (testType: string) => {
if (!address) return
recordTx('test', undefined, 'LP')
switch (testType) {
case 'add_zero':
// 添加流动性金额为0
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'addLiquidity',
args: [CONTRACTS.VAULTS.YT_A, BigInt(0), BigInt(0), BigInt(0)],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
case 'add_exceed_balance':
// 添加超过余额
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'addLiquidity',
args: [CONTRACTS.VAULTS.YT_A, parseUnits('999999999', TOKEN_DECIMALS.YT), BigInt(0), BigInt(0)],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
case 'remove_zero':
// 移除流动性金额为0
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'removeLiquidity',
args: [CONTRACTS.VAULTS.YT_A, BigInt(0), BigInt(0), address],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
case 'remove_exceed_balance':
// 移除超过ytLP余额
const exceedAmount = ytLPBalance
? ytLPBalance + parseUnits('999999', TOKEN_DECIMALS.YT_LP)
: parseUnits('999999999', TOKEN_DECIMALS.YT_LP)
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'removeLiquidity',
args: [CONTRACTS.VAULTS.YT_A, exceedAmount, BigInt(0), address],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
case 'remove_high_minout':
// 移除时minOut过高
const testAmount = ytLPBalance && ytLPBalance > BigInt(0)
? (ytLPBalance > parseUnits('0.1', TOKEN_DECIMALS.YT_LP) ? parseUnits('0.1', TOKEN_DECIMALS.YT_LP) : ytLPBalance)
: parseUnits('0.001', TOKEN_DECIMALS.YT_LP)
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'removeLiquidity',
args: [CONTRACTS.VAULTS.YT_A, testAmount, parseUnits('999999999', TOKEN_DECIMALS.YT), address],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
case 'swap_zero':
// 互换金额为0
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'swapYT',
args: [CONTRACTS.VAULTS.YT_A, CONTRACTS.VAULTS.YT_B, BigInt(0), BigInt(0), address],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
case 'swap_same_token':
// 相同代币互换
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'swapYT',
args: [CONTRACTS.VAULTS.YT_A, CONTRACTS.VAULTS.YT_A, parseUnits('1', TOKEN_DECIMALS.YT), BigInt(0), address],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
case 'swap_exceed_balance':
// 互换超过余额
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'swapYT',
args: [CONTRACTS.VAULTS.YT_A, CONTRACTS.VAULTS.YT_B, parseUnits('999999999', TOKEN_DECIMALS.YT), BigInt(0), address],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
}
}
return (
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px', border: '1px solid #e0e0e0' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={onToggle}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('lp.boundaryTest')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{show ? '▼' : '▶'}</span>
</div>
{show && (
<div style={{ marginTop: '12px' }}>
<p style={{ fontSize: '11px', color: '#666', marginBottom: '8px' }}>
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '8px' }}>
{/* Add Liquidity Tests */}
<div style={{ padding: '8px', background: '#fff', borderRadius: '4px', border: '1px solid #e0e0e0' }}>
<div style={{ fontSize: '11px', fontWeight: 'bold', color: '#333', marginBottom: '6px' }}></div>
<button
onClick={() => runBoundaryTest('add_zero')}
disabled={isProcessing}
className="btn btn-sm btn-outline"
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
>
=0
</button>
<button
onClick={() => runBoundaryTest('add_exceed_balance')}
disabled={isProcessing}
className="btn btn-sm btn-outline"
style={{ fontSize: '10px' }}
>
</button>
</div>
{/* Remove Liquidity Tests */}
<div style={{ padding: '8px', background: '#fff', borderRadius: '4px', border: '1px solid #e0e0e0' }}>
<div style={{ fontSize: '11px', fontWeight: 'bold', color: '#333', marginBottom: '6px' }}></div>
<button
onClick={() => runBoundaryTest('remove_zero')}
disabled={isProcessing}
className="btn btn-sm btn-outline"
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
>
=0
</button>
<button
onClick={() => runBoundaryTest('remove_exceed_balance')}
disabled={isProcessing}
className="btn btn-sm btn-outline"
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
>
</button>
<button
onClick={() => runBoundaryTest('remove_high_minout')}
disabled={isProcessing}
className="btn btn-sm btn-outline"
style={{ fontSize: '10px' }}
>
minOut
</button>
</div>
{/* Swap Tests */}
<div style={{ padding: '8px', background: '#fff', borderRadius: '4px', border: '1px solid #e0e0e0' }}>
<div style={{ fontSize: '11px', fontWeight: 'bold', color: '#333', marginBottom: '6px' }}></div>
<button
onClick={() => runBoundaryTest('swap_zero')}
disabled={isProcessing}
className="btn btn-sm btn-outline"
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
>
=0
</button>
<button
onClick={() => runBoundaryTest('swap_same_token')}
disabled={isProcessing}
className="btn btn-sm btn-outline"
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
>
</button>
<button
onClick={() => runBoundaryTest('swap_exceed_balance')}
disabled={isProcessing}
className="btn btn-sm btn-outline"
style={{ fontSize: '10px' }}
>
</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,153 @@
/**
* LP 调试信息组件
* 显示合约地址和管理员信息
*/
import { useTranslation } from 'react-i18next'
import { useChainId } from 'wagmi'
import { getContracts, getChainName } from '../../config/contracts'
interface LPDebugInfoProps {
vaultGov?: string
poolManagerGov?: string
show: boolean
onToggle: () => void
}
export function LPDebugInfo({
vaultGov,
poolManagerGov,
show,
onToggle,
}: LPDebugInfoProps) {
const { t } = useTranslation()
// ===== 多链支持 =====
const chainId = useChainId()
const CONTRACTS = getContracts(chainId)
const currentChainName = getChainName(chainId)
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
}
const styles = {
container: {
marginTop: '16px',
padding: '12px 16px',
background: '#f9f9f9',
borderRadius: '8px',
border: '1px solid #e0e0e0',
},
header: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
},
title: {
margin: 0,
color: '#333',
fontSize: '14px',
fontWeight: 600,
},
content: {
marginTop: '16px',
},
section: {
marginBottom: '16px',
background: '#fff',
borderRadius: '6px',
border: '1px solid #e8e8e8',
overflow: 'hidden',
},
sectionTitle: {
padding: '10px 12px',
background: '#fafafa',
borderBottom: '1px solid #e8e8e8',
fontSize: '12px',
fontWeight: 600,
color: '#333',
},
sectionBody: {
padding: '8px 0',
},
row: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '6px 12px',
fontSize: '12px',
},
label: {
color: '#666',
fontWeight: 500,
minWidth: '120px',
},
address: {
fontFamily: 'monospace',
color: '#1890ff',
cursor: 'pointer',
fontSize: '11px',
wordBreak: 'break-all' as const,
textAlign: 'right' as const,
flex: 1,
marginLeft: '12px',
},
hint: {
fontSize: '11px',
color: '#999',
textAlign: 'center' as const,
padding: '8px',
},
}
const AddressRow = ({ label, address }: { label: string; address?: string }) => (
<div style={styles.row}>
<span style={styles.label}>{label}</span>
<span
style={styles.address}
onClick={() => address && copyToClipboard(address)}
title={address ? '点击复制完整地址' : ''}
>
{address || 'Loading...'}
</span>
</div>
)
return (
<div style={styles.container}>
<div style={styles.header} onClick={onToggle}>
<h4 style={styles.title}>{t('lp.debugInfo')}</h4>
<span style={{ color: '#999', fontSize: '12px' }}>{show ? '收起' : '展开'}</span>
</div>
{show && (
<div style={styles.content}>
{/* 合约地址 */}
<div style={styles.section}>
<div style={styles.sectionTitle}></div>
<div style={styles.sectionBody}>
<AddressRow label="YTRewardRouter" address={CONTRACTS.YT_REWARD_ROUTER} />
<AddressRow label="YTLPToken" address={CONTRACTS.YT_LP_TOKEN} />
<AddressRow label="YTPoolManager" address={CONTRACTS.YT_POOL_MANAGER} />
<AddressRow label="YTVault" address={CONTRACTS.YT_VAULT} />
<AddressRow label="USDY" address={CONTRACTS.USDY} />
<AddressRow label="USDC" address={CONTRACTS.USDC} />
</div>
</div>
{/* 管理员地址 */}
<div style={{ ...styles.section, marginBottom: 0 }}>
<div style={styles.sectionTitle}></div>
<div style={styles.sectionBody}>
<AddressRow label="PoolManager Gov" address={poolManagerGov} />
<AddressRow label="Vault Gov" address={vaultGov} />
</div>
</div>
<div style={styles.hint}></div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,882 @@
/**
* LP 操作组件
* 包含添加流动性、移除流动性、代币交换功能
*/
import { useState, useMemo, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { parseUnits, formatUnits } from 'viem'
import { useReadContract, useChainId } from 'wagmi'
import {
CONTRACTS,
GAS_CONFIG,
TOKEN_DECIMALS,
getTokenDecimals,
getDecimals,
YT_REWARD_ROUTER_ABI,
YT_VAULT_ABI,
USDC_ABI,
YT_POOL_MANAGER_ABI,
} from '../../config/contracts'
import type { TransactionType } from '../../context/TransactionContext'
import type { ExtendedPoolToken } from './useLPPoolData'
import type { AddLiquidityForm, RemoveLiquidityForm, SwapForm } from './types'
interface LPOperationsProps {
address: `0x${string}` | undefined
poolTokens: ExtendedPoolToken[]
ytLPBalance?: bigint
ytLPPrice?: bigint
isProcessing: boolean
writeContract: (config: any) => void
recordTx: (type: TransactionType, amount?: string, token?: string) => void
getTokenSymbol: (address: string) => string
// 手续费信息
swapFee?: bigint
taxBasisPoints?: bigint
dynamicFees?: boolean
}
export function LPOperations({
address,
poolTokens,
ytLPBalance,
ytLPPrice,
isProcessing,
writeContract,
recordTx,
getTokenSymbol,
swapFee,
taxBasisPoints,
dynamicFees,
}: LPOperationsProps) {
const { t } = useTranslation()
// ===== 多链支持 =====
const chainId = useChainId()
const TOKEN_DECIMALS_DYNAMIC = getDecimals(chainId)
// Tab 状态
const [activeTab, setActiveTab] = useState<'add' | 'remove' | 'swap'>('add')
// 表单状态
const [addLiquidityForm, setAddLiquidityForm] = useState<AddLiquidityForm>({
token: poolTokens.find(t => t.isWhitelisted)?.address || '',
amount: '',
slippage: '0.5',
})
const [removeLiquidityForm, setRemoveLiquidityForm] = useState<RemoveLiquidityForm>({
token: poolTokens.find(t => t.isWhitelisted)?.address || '',
amount: '',
slippage: '1',
})
const [swapForm, setSwapForm] = useState<SwapForm>({
tokenIn: poolTokens.find(t => t.isWhitelisted)?.address || '',
tokenOut: poolTokens.filter(t => t.isWhitelisted)[1]?.address || '',
amount: '',
slippage: '0.5',
})
// 白名单代币
const whitelistedTokens = useMemo(() =>
poolTokens.filter(t => t.isWhitelisted),
[poolTokens]
)
// 当 poolTokens 加载完成后,自动设置默认代币
useEffect(() => {
if (whitelistedTokens.length > 0) {
// 添加流动性:如果当前没有选中有效代币,设置第一个白名单代币
if (!addLiquidityForm.token || !whitelistedTokens.find(t => t.address === addLiquidityForm.token)) {
setAddLiquidityForm(prev => ({ ...prev, token: whitelistedTokens[0].address }))
}
// 移除流动性
if (!removeLiquidityForm.token || !whitelistedTokens.find(t => t.address === removeLiquidityForm.token)) {
setRemoveLiquidityForm(prev => ({ ...prev, token: whitelistedTokens[0].address }))
}
// Swap
if (!swapForm.tokenIn || !whitelistedTokens.find(t => t.address === swapForm.tokenIn)) {
setSwapForm(prev => ({ ...prev, tokenIn: whitelistedTokens[0].address }))
}
if (!swapForm.tokenOut || !whitelistedTokens.find(t => t.address === swapForm.tokenOut)) {
const secondToken = whitelistedTokens[1]?.address || whitelistedTokens[0]?.address || ''
setSwapForm(prev => ({ ...prev, tokenOut: secondToken }))
}
}
}, [whitelistedTokens, addLiquidityForm.token, removeLiquidityForm.token, swapForm.tokenIn, swapForm.tokenOut])
// 验证代币地址是否有效
const isValidAddToken = !!(addLiquidityForm.token && addLiquidityForm.token.length === 42 && addLiquidityForm.token.startsWith('0x'))
const isValidRemoveToken = !!(removeLiquidityForm.token && removeLiquidityForm.token.length === 42 && removeLiquidityForm.token.startsWith('0x'))
const isValidSwapTokenIn = !!(swapForm.tokenIn && swapForm.tokenIn.length === 42 && swapForm.tokenIn.startsWith('0x'))
const isValidSwapTokenOut = !!(swapForm.tokenOut && swapForm.tokenOut.length === 42 && swapForm.tokenOut.startsWith('0x'))
// 追踪 isProcessing 变化,用于在交易完成后刷新授权
const prevIsProcessingRef = useRef(isProcessing)
// 读取授权额度
const { data: tokenAllowance, refetch: refetchTokenAllowance } = useReadContract({
address: isValidAddToken ? (addLiquidityForm.token as `0x${string}`) : undefined,
abi: USDC_ABI,
functionName: 'allowance',
args: address && isValidAddToken ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined,
query: {
enabled: !!address && isValidAddToken,
},
})
const { data: ytLPAllowance, refetch: refetchYtLPAllowance } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: USDC_ABI,
functionName: 'allowance',
args: address ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined,
query: {
enabled: !!address,
},
})
const { data: swapAllowance, refetch: refetchSwapAllowance } = useReadContract({
address: isValidSwapTokenIn ? (swapForm.tokenIn as `0x${string}`) : undefined,
abi: USDC_ABI,
functionName: 'allowance',
args: address && isValidSwapTokenIn ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined,
query: {
enabled: !!address && isValidSwapTokenIn,
},
})
// 交易完成后刷新授权额度 (isProcessing 从 true 变为 false)
useEffect(() => {
if (prevIsProcessingRef.current && !isProcessing) {
// 交易完成,刷新所有授权额度
setTimeout(() => {
refetchTokenAllowance()
refetchYtLPAllowance()
refetchSwapAllowance()
}, 1000) // 延迟1秒确保链上状态已更新
}
prevIsProcessingRef.current = isProcessing
}, [isProcessing, refetchTokenAllowance, refetchYtLPAllowance, refetchSwapAllowance])
// 读取添加流动性代币价格
const { data: addLiquidityTokenPrice } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'getMinPrice',
args: isValidAddToken ? [addLiquidityForm.token as `0x${string}`] : undefined,
query: {
enabled: isValidAddToken,
},
})
// 读取移除流动性代币价格
const { data: removeLiquidityTokenMaxPrice } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'getMaxPrice',
args: isValidRemoveToken ? [removeLiquidityForm.token as `0x${string}`] : undefined,
query: {
enabled: isValidRemoveToken,
},
})
// 读取 Swap tokenIn 最大价格 (用于卖出)
const { data: swapTokenInMaxPrice } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'getMaxPrice',
args: isValidSwapTokenIn ? [swapForm.tokenIn as `0x${string}`] : undefined,
query: {
enabled: isValidSwapTokenIn,
},
})
// 读取 Swap tokenOut 最小价格 (用于买入)
const { data: swapTokenOutMinPrice } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'getMinPrice',
args: isValidSwapTokenOut ? [swapForm.tokenOut as `0x${string}`] : undefined,
query: {
enabled: isValidSwapTokenOut,
},
})
// 检查 RewardRouter 是否被设置为 Swapper
const { data: isRewardRouterSwapper } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'isSwapper',
args: [CONTRACTS.YT_REWARD_ROUTER],
})
// 检查 RewardRouter 是否被设置为 PoolManager 的 Handler
const { data: isRewardRouterHandler } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'isHandler',
args: [CONTRACTS.YT_REWARD_ROUTER],
})
// 读取 maxSwapAmount 限制
const { data: maxSwapAmountIn } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'maxSwapAmounts',
args: isValidSwapTokenIn ? [swapForm.tokenIn as `0x${string}`] : undefined,
query: {
enabled: isValidSwapTokenIn,
},
})
// 读取 isSwapEnabled 状态
const { data: isSwapEnabled } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'isSwapEnabled',
})
// 读取池子中 tokenOut 的余额
const { data: poolAmountOut } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'poolAmounts',
args: isValidSwapTokenOut ? [swapForm.tokenOut as `0x${string}`] : undefined,
query: {
enabled: isValidSwapTokenOut,
},
})
// 读取池子中 tokenIn 的 usdyAmount
const { data: usdyAmountIn } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'usdyAmounts',
args: isValidSwapTokenIn ? [swapForm.tokenIn as `0x${string}`] : undefined,
query: {
enabled: isValidSwapTokenIn,
},
})
// 读取池子中 tokenOut 的 usdyAmount
const { data: usdyAmountOut } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'usdyAmounts',
args: isValidSwapTokenOut ? [swapForm.tokenOut as `0x${string}`] : undefined,
query: {
enabled: isValidSwapTokenOut,
},
})
// 计算添加流动性预估输出
const addLiquidityPreviewAmount = useMemo(() => {
if (!addLiquidityForm.amount || !addLiquidityTokenPrice || !ytLPPrice) return null
if (ytLPPrice === 0n) return null
try {
const tokenDecimals = getTokenDecimals(addLiquidityForm.token)
const amountIn = parseUnits(addLiquidityForm.amount, tokenDecimals)
const PRECISION_DIFF = 10n ** 12n
const ytLPAmount = (amountIn * addLiquidityTokenPrice) / ytLPPrice / PRECISION_DIFF
return ytLPAmount
} catch {
return null
}
}, [addLiquidityForm.amount, addLiquidityForm.token, addLiquidityTokenPrice, ytLPPrice])
// 计算移除流动性预估输出
const removeLiquidityPreviewAmount = useMemo(() => {
if (!removeLiquidityForm.amount || !ytLPPrice || !removeLiquidityTokenMaxPrice) return null
if (removeLiquidityTokenMaxPrice === 0n) return null
try {
const ytLPAmount = parseUnits(removeLiquidityForm.amount, TOKEN_DECIMALS.YT_LP)
const PRECISION_DIFF = 10n ** 12n
const tokenAmount = (ytLPAmount * ytLPPrice * PRECISION_DIFF) / removeLiquidityTokenMaxPrice
return tokenAmount
} catch {
return null
}
}, [removeLiquidityForm.amount, ytLPPrice, removeLiquidityTokenMaxPrice])
// 检查是否需要授权
const needsApproval = () => {
if (!addLiquidityForm.amount) return false
try {
const amount = parseUnits(addLiquidityForm.amount, getTokenDecimals(addLiquidityForm.token))
if (tokenAllowance === undefined || tokenAllowance === null) return true
return tokenAllowance < amount
} catch {
return false
}
}
const needsYtLPApproval = () => {
if (!removeLiquidityForm.amount) return false
try {
const amount = parseUnits(removeLiquidityForm.amount, TOKEN_DECIMALS.YT_LP)
if (ytLPAllowance === undefined || ytLPAllowance === null) return true
return ytLPAllowance < amount
} catch {
return false
}
}
const needsSwapApproval = () => {
if (!swapForm.amount) return false
try {
const amount = parseUnits(swapForm.amount, getTokenDecimals(swapForm.tokenIn))
if (swapAllowance === undefined || swapAllowance === null) return true
return swapAllowance < amount
} catch {
return false
}
}
// 处理函数
const handleApproveToken = () => {
if (!address || !addLiquidityForm.amount || !isValidAddToken) return
const tokenDecimals = getTokenDecimals(addLiquidityForm.token)
const amount = parseUnits(addLiquidityForm.amount, tokenDecimals)
recordTx('approve', addLiquidityForm.amount, getTokenSymbol(addLiquidityForm.token))
writeContract({
address: addLiquidityForm.token as `0x${string}`,
abi: USDC_ABI,
functionName: 'approve',
args: [CONTRACTS.YT_REWARD_ROUTER, amount],
gas: GAS_CONFIG.SIMPLE,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleApproveYtLP = () => {
if (!address || !removeLiquidityForm.amount) return
const amount = parseUnits(removeLiquidityForm.amount, TOKEN_DECIMALS.YT_LP)
recordTx('approve', removeLiquidityForm.amount, 'ytLP')
writeContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: USDC_ABI,
functionName: 'approve',
args: [CONTRACTS.YT_REWARD_ROUTER, amount],
gas: GAS_CONFIG.SIMPLE,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleApproveSwapToken = () => {
if (!address || !swapForm.amount || !isValidSwapTokenIn) return
const tokenDecimals = getTokenDecimals(swapForm.tokenIn)
const amount = parseUnits(swapForm.amount, tokenDecimals)
recordTx('approve', swapForm.amount, getTokenSymbol(swapForm.tokenIn))
writeContract({
address: swapForm.tokenIn as `0x${string}`,
abi: USDC_ABI,
functionName: 'approve',
args: [CONTRACTS.YT_REWARD_ROUTER, amount],
gas: GAS_CONFIG.SIMPLE,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleAddLiquidity = () => {
if (!address || !addLiquidityForm.amount || !isValidAddToken) return
const tokenDecimals = getTokenDecimals(addLiquidityForm.token)
const amount = parseUnits(addLiquidityForm.amount, tokenDecimals)
const slippageBps = Math.floor(Number(addLiquidityForm.slippage) * 100)
// 基于预期的 ytLP 输出计算 minOut而不是输入金额
let minOut = BigInt(0)
if (addLiquidityPreviewAmount && addLiquidityPreviewAmount > 0n) {
minOut = (addLiquidityPreviewAmount * BigInt(10000 - slippageBps)) / BigInt(10000)
}
recordTx('addLiquidity', addLiquidityForm.amount, getTokenSymbol(addLiquidityForm.token))
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'addLiquidity',
args: [addLiquidityForm.token as `0x${string}`, amount, minOut, BigInt(0)],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleRemoveLiquidity = () => {
if (!address || !removeLiquidityForm.amount || !isValidRemoveToken) return
const amount = parseUnits(removeLiquidityForm.amount, TOKEN_DECIMALS.YT_LP)
const slippageBps = Math.floor(Number(removeLiquidityForm.slippage) * 100)
// 基于预期的代币输出计算 minOut
let minOut = BigInt(0)
if (removeLiquidityPreviewAmount && removeLiquidityPreviewAmount > 0n) {
minOut = (removeLiquidityPreviewAmount * BigInt(10000 - slippageBps)) / BigInt(10000)
}
recordTx('removeLiquidity', removeLiquidityForm.amount, 'ytLP')
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'removeLiquidity',
args: [removeLiquidityForm.token as `0x${string}`, amount, minOut, address],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleSwap = () => {
if (!address || !swapForm.amount || !isValidSwapTokenIn || !isValidSwapTokenOut) return
const tokenInDecimals = getTokenDecimals(swapForm.tokenIn)
const tokenOutDecimals = getTokenDecimals(swapForm.tokenOut)
const amount = parseUnits(swapForm.amount, tokenInDecimals)
const slippageBps = Math.floor(Number(swapForm.slippage) * 100)
// 基于预期的输出计算 minOut
let minOut = BigInt(0)
if (swapTokenInMaxPrice && swapTokenOutMinPrice && swapTokenOutMinPrice > 0n) {
// 计算预期输出: amountOut = (amountIn * tokenInMaxPrice) / tokenOutMinPrice
const expectedOut = (amount * swapTokenInMaxPrice) / swapTokenOutMinPrice
// 调整精度差异
const precisionDiff = tokenInDecimals - tokenOutDecimals
const adjustedOut = precisionDiff > 0
? expectedOut / (10n ** BigInt(precisionDiff))
: expectedOut * (10n ** BigInt(-precisionDiff))
minOut = (adjustedOut * BigInt(10000 - slippageBps)) / BigInt(10000)
}
recordTx('swap', swapForm.amount, `${getTokenSymbol(swapForm.tokenIn)}${getTokenSymbol(swapForm.tokenOut)}`)
writeContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'swapYT',
args: [
swapForm.tokenIn as `0x${string}`,
swapForm.tokenOut as `0x${string}`,
amount,
minOut,
address,
],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
return (
<div>
{/* Tab Navigation */}
<div className="lp-tabs">
<button
className={`tab-btn ${activeTab === 'add' ? 'active' : ''}`}
onClick={() => setActiveTab('add')}
>
{t('lp.addLiquidity')}
</button>
<button
className={`tab-btn ${activeTab === 'remove' ? 'active' : ''}`}
onClick={() => setActiveTab('remove')}
>
{t('lp.removeLiquidity')}
</button>
<button
className={`tab-btn ${activeTab === 'swap' ? 'active' : ''}`}
onClick={() => setActiveTab('swap')}
>
{t('lp.swapTokens')}
</button>
</div>
{/* Add Liquidity Form */}
{activeTab === 'add' && (
<div style={{ marginTop: '12px' }}>
<p className="text-muted" style={{ marginBottom: '8px', fontSize: '12px' }}>{t('lp.addLiquidityDesc')}</p>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 2fr 1fr', gap: '8px', marginBottom: '8px' }}>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.selectToken')}</label>
<select
value={addLiquidityForm.token}
onChange={(e) => setAddLiquidityForm({ ...addLiquidityForm, token: e.target.value })}
className="input"
style={{ fontSize: '13px' }}
>
{whitelistedTokens.map((token) => (
<option key={token.address} value={token.address}>
{token.symbol} - {token.name}
</option>
))}
</select>
</div>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.amount')}</label>
<input
type="number"
value={addLiquidityForm.amount}
onChange={(e) => setAddLiquidityForm({ ...addLiquidityForm, amount: e.target.value })}
placeholder="0.0"
className="input"
style={{ fontSize: '13px' }}
/>
</div>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.slippage')} (%)</label>
<input
type="number"
value={addLiquidityForm.slippage}
onChange={(e) => setAddLiquidityForm({ ...addLiquidityForm, slippage: e.target.value })}
placeholder="0.5"
className="input"
style={{ fontSize: '13px' }}
step="0.1"
/>
</div>
</div>
{/* 预估输出 */}
{addLiquidityForm.amount && addLiquidityPreviewAmount && (
<div style={{ marginBottom: '12px', padding: '10px', background: '#f8f9fa', borderRadius: '6px', fontSize: '13px' }}>
<span style={{ color: '#666' }}>{t('vault.youWillReceive')}: </span>
<strong style={{ color: '#333', fontSize: '15px' }}>
{formatUnits(addLiquidityPreviewAmount, TOKEN_DECIMALS.YT_LP)} ytLP
</strong>
</div>
)}
{/* 调试信息 */}
{addLiquidityForm.amount && (
<div style={{ marginBottom: '12px', padding: '10px', background: '#e8f4e8', borderRadius: '6px', fontSize: '11px', fontFamily: 'monospace' }}>
<div style={{ marginBottom: '4px', color: '#666' }}>
<strong>:</strong>{' '}
{getTokenSymbol(addLiquidityForm.token)} ({addLiquidityForm.token ? `${addLiquidityForm.token.slice(0, 6)}...${addLiquidityForm.token.slice(-4)}` : '未选择'}) |{' '}
: {isValidAddToken ? <span style={{ color: '#4caf50' }}></span> : <span style={{ color: '#f44336' }}></span>}
</div>
<div style={{ marginBottom: '4px', color: '#666' }}>
<strong>:</strong>{' '}
: {tokenAllowance !== undefined ? formatUnits(tokenAllowance, getTokenDecimals(addLiquidityForm.token)) : '加载中'} {getTokenSymbol(addLiquidityForm.token)} |{' '}
: {addLiquidityForm.amount} |{' '}
: {needsApproval() ? <span style={{ color: '#f44336' }}></span> : <span style={{ color: '#4caf50' }}></span>} |{' '}
: {isProcessing ? <span style={{ color: '#f44336' }}></span> : <span style={{ color: '#4caf50' }}></span>}
</div>
<div style={{ marginBottom: '4px', color: '#555' }}>
<strong>:</strong>{' '}
tokenMinPrice: {addLiquidityTokenPrice?.toString() || 'N/A'} (: {addLiquidityTokenPrice ? (Number(addLiquidityTokenPrice) / 1e30).toFixed(8) : 'N/A'})
</div>
<div style={{ marginBottom: '4px', color: '#555' }}>
ytLPPrice: {ytLPPrice?.toString() || 'N/A'} (: {ytLPPrice ? (Number(ytLPPrice) / 1e30).toFixed(8) : 'N/A'})
</div>
{addLiquidityPreviewAmount && (
<div style={{ color: '#555' }}>
<strong>:</strong>{' '}
: {formatUnits(addLiquidityPreviewAmount, TOKEN_DECIMALS.YT_LP)} ytLP |{' '}
minOut (): {formatUnits((addLiquidityPreviewAmount * BigInt(10000 - Math.floor(Number(addLiquidityForm.slippage) * 100))) / BigInt(10000), TOKEN_DECIMALS.YT_LP)} ytLP
</div>
)}
</div>
)}
{/* 按钮 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
{needsApproval() ? (
<button onClick={handleApproveToken} disabled={isProcessing} className="btn btn-secondary btn-sm">
{isProcessing ? '...' : t('lp.approveToken')}
</button>
) : (
<button onClick={handleAddLiquidity} disabled={isProcessing || !addLiquidityForm.amount} className="btn btn-primary btn-sm">
{isProcessing ? '...' : t('lp.addLiquidity')}
</button>
)}
</div>
</div>
)}
{/* Remove Liquidity Form */}
{activeTab === 'remove' && (
<div style={{ marginTop: '12px' }}>
<p className="text-muted" style={{ marginBottom: '8px', fontSize: '12px' }}>{t('lp.removeLiquidityDesc')}</p>
<div style={{ display: 'grid', gridTemplateColumns: '2fr 2fr 1fr', gap: '8px', marginBottom: '8px' }}>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.outputToken')}</label>
<select
value={removeLiquidityForm.token}
onChange={(e) => setRemoveLiquidityForm({ ...removeLiquidityForm, token: e.target.value })}
className="input"
style={{ fontSize: '13px' }}
>
{whitelistedTokens.map((token) => (
<option key={token.address} value={token.address}>
{token.symbol} - {token.name}
</option>
))}
</select>
</div>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.ytLPAmount')}</label>
<div style={{ display: 'flex', gap: '4px' }}>
<input
type="number"
value={removeLiquidityForm.amount}
onChange={(e) => setRemoveLiquidityForm({ ...removeLiquidityForm, amount: e.target.value })}
placeholder="0.0"
className="input"
style={{ fontSize: '13px', flex: 1 }}
/>
<button
className="btn btn-sm btn-link"
onClick={() => setRemoveLiquidityForm({
...removeLiquidityForm,
amount: ytLPBalance ? formatUnits(ytLPBalance, TOKEN_DECIMALS.YT_LP) : '0'
})}
style={{ fontSize: '10px', padding: '2px 6px' }}
>
MAX
</button>
</div>
</div>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.slippage')} (%)</label>
<input
type="number"
value={removeLiquidityForm.slippage}
onChange={(e) => setRemoveLiquidityForm({ ...removeLiquidityForm, slippage: e.target.value })}
placeholder="1"
className="input"
style={{ fontSize: '13px' }}
step="0.1"
/>
</div>
</div>
{/* 预估输出 */}
{removeLiquidityForm.amount && removeLiquidityPreviewAmount && (
<div style={{ marginBottom: '12px', padding: '10px', background: '#f8f9fa', borderRadius: '6px', fontSize: '13px' }}>
<span style={{ color: '#666' }}>{t('vault.youWillReceive')}: </span>
<strong style={{ color: '#333', fontSize: '15px' }}>
{formatUnits(removeLiquidityPreviewAmount, getTokenDecimals(removeLiquidityForm.token))} {getTokenSymbol(removeLiquidityForm.token)}
</strong>
</div>
)}
{/* 调试信息 */}
{removeLiquidityForm.amount && (
<div style={{ marginBottom: '12px', padding: '10px', background: '#fff3e0', borderRadius: '6px', fontSize: '11px', fontFamily: 'monospace' }}>
<div style={{ marginBottom: '4px', color: '#666' }}>
<strong>:</strong>{' '}
ytLP授权额度: {ytLPAllowance !== undefined ? formatUnits(ytLPAllowance, TOKEN_DECIMALS.YT_LP) : '加载中'} ytLP |{' '}
: {removeLiquidityForm.amount} |{' '}
: {needsYtLPApproval() ? <span style={{ color: '#f44336' }}></span> : <span style={{ color: '#4caf50' }}></span>} |{' '}
: {isProcessing ? <span style={{ color: '#f44336' }}></span> : <span style={{ color: '#4caf50' }}></span>}
</div>
<div style={{ marginBottom: '4px', color: '#555' }}>
<strong>:</strong>{' '}
ytLP余额: {ytLPBalance !== undefined ? formatUnits(ytLPBalance, TOKEN_DECIMALS.YT_LP) : 'N/A'} ytLP
</div>
<div style={{ marginBottom: '4px', color: '#555' }}>
<strong>:</strong>{' '}
tokenMaxPrice: {removeLiquidityTokenMaxPrice?.toString() || 'N/A'} (: {removeLiquidityTokenMaxPrice ? (Number(removeLiquidityTokenMaxPrice) / 1e30).toFixed(8) : 'N/A'})
</div>
<div style={{ marginBottom: '4px', color: '#555' }}>
ytLPPrice: {ytLPPrice?.toString() || 'N/A'} (: {ytLPPrice ? (Number(ytLPPrice) / 1e30).toFixed(8) : 'N/A'})
</div>
{removeLiquidityPreviewAmount && (
<div style={{ color: '#555' }}>
<strong>:</strong>{' '}
: {removeLiquidityPreviewAmount.toString()} |{' '}
: (ytLPAmount × ytLPPrice × 1e12) / tokenMaxPrice
</div>
)}
</div>
)}
{/* 按钮 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
{needsYtLPApproval() ? (
<button onClick={handleApproveYtLP} disabled={isProcessing} className="btn btn-secondary btn-sm">
{isProcessing ? '...' : t('lp.approveYtLP')}
</button>
) : (
<button onClick={handleRemoveLiquidity} disabled={isProcessing || !removeLiquidityForm.amount} className="btn btn-primary btn-sm">
{isProcessing ? '...' : t('lp.removeLiquidity')}
</button>
)}
</div>
</div>
)}
{/* Swap Form */}
{activeTab === 'swap' && (
<div style={{ marginTop: '12px' }}>
<p className="text-muted" style={{ marginBottom: '8px', fontSize: '12px' }}>{t('lp.swapDesc')}</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '8px', marginBottom: '8px' }}>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.fromToken')}</label>
<select
value={swapForm.tokenIn}
onChange={(e) => setSwapForm({ ...swapForm, tokenIn: e.target.value })}
className="input"
style={{ fontSize: '13px' }}
>
{whitelistedTokens.map((token) => (
<option key={token.address} value={token.address}>
{token.symbol}
</option>
))}
</select>
</div>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.toToken')}</label>
<select
value={swapForm.tokenOut}
onChange={(e) => setSwapForm({ ...swapForm, tokenOut: e.target.value })}
className="input"
style={{ fontSize: '13px' }}
>
{whitelistedTokens.map((token) => (
<option key={token.address} value={token.address}>
{token.symbol}
</option>
))}
</select>
</div>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.amount')}</label>
<input
type="number"
value={swapForm.amount}
onChange={(e) => setSwapForm({ ...swapForm, amount: e.target.value })}
placeholder="0.0"
className="input"
style={{ fontSize: '13px' }}
/>
</div>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.slippage')} (%)</label>
<input
type="number"
value={swapForm.slippage}
onChange={(e) => setSwapForm({ ...swapForm, slippage: e.target.value })}
placeholder="0.5"
className="input"
style={{ fontSize: '13px' }}
step="0.1"
/>
</div>
</div>
{/* 警告 */}
{swapForm.tokenIn === swapForm.tokenOut && (
<div style={{ marginBottom: '8px', padding: '8px', background: '#f5f5f5', borderRadius: '6px', fontSize: '11px', color: '#666', border: '1px solid #e0e0e0' }}>
{t('lp.sameTokenWarning')}
</div>
)}
{/* 调试信息 */}
{swapForm.amount && (
<div style={{ marginBottom: '12px', padding: '10px', background: '#e3f2fd', borderRadius: '6px', fontSize: '11px', fontFamily: 'monospace' }}>
{/* 权限检查 - 最重要 */}
<div style={{ marginBottom: '4px', padding: '4px', background: isRewardRouterSwapper && isRewardRouterHandler ? '#e8f5e9' : '#ffebee', borderRadius: '4px' }}>
<strong> :</strong>{' '}
RewardRouter是Swapper: {isRewardRouterSwapper === undefined ? '加载中...' : isRewardRouterSwapper ? <span style={{ color: '#4caf50' }}> </span> : <span style={{ color: '#f44336' }}> ()</span>} |{' '}
RewardRouter是Handler: {isRewardRouterHandler === undefined ? '加载中...' : isRewardRouterHandler ? <span style={{ color: '#4caf50' }}> </span> : <span style={{ color: '#f44336' }}> ()</span>}
</div>
{(!isRewardRouterSwapper || !isRewardRouterHandler) && (
<div style={{ marginBottom: '4px', padding: '4px', background: '#fff3e0', borderRadius: '4px', color: '#e65100' }}>
<strong>🔧 :</strong> "管理员配置": {!isRewardRouterSwapper && 'setSwapper(RewardRouter, true)'} {!isRewardRouterHandler && 'setHandler(RewardRouter, true)'}
</div>
)}
{/* Swap 开关状态 */}
<div style={{ marginBottom: '4px', color: '#666' }}>
<strong>Swap状态:</strong>{' '}
isSwapEnabled: {isSwapEnabled === undefined ? '加载中...' : isSwapEnabled ? <span style={{ color: '#4caf50' }}> </span> : <span style={{ color: '#f44336' }}> </span>}
</div>
{/* 池子余额信息 */}
<div style={{ marginBottom: '4px', color: '#666' }}>
<strong>:</strong>{' '}
{getTokenSymbol(swapForm.tokenOut)}: {poolAmountOut !== undefined ? formatUnits(poolAmountOut as bigint, getTokenDecimals(swapForm.tokenOut)) : '加载中'} |{' '}
{/* ✅ 使用配置中的 USDY 精度 */}
{getTokenSymbol(swapForm.tokenIn)} USDY债务: {usdyAmountIn !== undefined ? formatUnits(usdyAmountIn as bigint, TOKEN_DECIMALS_DYNAMIC.USDY) : '加载中'} |{' '}
{getTokenSymbol(swapForm.tokenOut)} USDY债务: {usdyAmountOut !== undefined ? formatUnits(usdyAmountOut as bigint, TOKEN_DECIMALS_DYNAMIC.USDY) : '加载中'}
</div>
<div style={{ marginBottom: '4px', color: '#666' }}>
<strong>:</strong>{' '}
{getTokenSymbol(swapForm.tokenIn)}: {swapAllowance !== undefined ? formatUnits(swapAllowance, getTokenDecimals(swapForm.tokenIn)) : '加载中'} |{' '}
: {swapForm.amount} |{' '}
: {needsSwapApproval() ? <span style={{ color: '#f44336' }}></span> : <span style={{ color: '#4caf50' }}></span>} |{' '}
: {isProcessing ? <span style={{ color: '#f44336' }}></span> : <span style={{ color: '#4caf50' }}></span>}
</div>
<div style={{ marginBottom: '4px', color: '#666' }}>
<strong>:</strong>{' '}
maxSwapAmount({getTokenSymbol(swapForm.tokenIn)}): {maxSwapAmountIn !== undefined ? (maxSwapAmountIn === 0n ? '无限制' : formatUnits(maxSwapAmountIn, getTokenDecimals(swapForm.tokenIn))) : '加载中'}
</div>
<div style={{ marginBottom: '4px', color: '#ff9800' }}>
<strong>:</strong>{' '}
swapFee: {swapFee !== undefined ? `${Number(swapFee) / 100}%` : 'N/A'} |{' '}
taxBps: {taxBasisPoints !== undefined ? `${Number(taxBasisPoints) / 100}%` : 'N/A'} |{' '}
: {dynamicFees ? '是' : '否'} |{' '}
<span style={{ color: '#f44336' }}>: {swapFee && taxBasisPoints ? `${(Number(swapFee) + Number(taxBasisPoints)) / 100 + 0.5}%` : '2%'}</span>
</div>
<div style={{ marginBottom: '4px', color: '#555' }}>
<strong>TokenIn价格 ({getTokenSymbol(swapForm.tokenIn)}):</strong>{' '}
maxPrice: {swapTokenInMaxPrice?.toString() || 'N/A'} (: {swapTokenInMaxPrice ? (Number(swapTokenInMaxPrice) / 1e30).toFixed(8) : 'N/A'})
</div>
<div style={{ marginBottom: '4px', color: '#555' }}>
<strong>TokenOut价格 ({getTokenSymbol(swapForm.tokenOut)}):</strong>{' '}
minPrice: {swapTokenOutMinPrice?.toString() || 'N/A'} (: {swapTokenOutMinPrice ? (Number(swapTokenOutMinPrice) / 1e30).toFixed(8) : 'N/A'})
</div>
{swapTokenInMaxPrice && swapTokenOutMinPrice && swapTokenOutMinPrice > 0n && (() => {
try {
const tokenInDecimals = getTokenDecimals(swapForm.tokenIn)
const tokenOutDecimals = getTokenDecimals(swapForm.tokenOut)
const amount = parseUnits(swapForm.amount, tokenInDecimals)
const expectedOut = (amount * swapTokenInMaxPrice) / swapTokenOutMinPrice
const precisionDiff = tokenInDecimals - tokenOutDecimals
const adjustedOut = precisionDiff > 0
? expectedOut / (10n ** BigInt(precisionDiff))
: expectedOut * (10n ** BigInt(-precisionDiff))
const slippageBps = Math.floor(Number(swapForm.slippage) * 100)
const minOut = (adjustedOut * BigInt(10000 - slippageBps)) / BigInt(10000)
return (
<div style={{ marginBottom: '4px', color: '#555' }}>
<strong>:</strong>{' '}
: {formatUnits(adjustedOut, tokenOutDecimals)} {getTokenSymbol(swapForm.tokenOut)} |{' '}
minOut: {formatUnits(minOut, tokenOutDecimals)} {getTokenSymbol(swapForm.tokenOut)}
</div>
)
} catch {
return null
}
})()}
{(!swapTokenInMaxPrice || !swapTokenOutMinPrice) && (
<div style={{ color: '#f44336' }}>
<strong>:</strong> minOut将为0
</div>
)}
</div>
)}
{/* 按钮 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
{needsSwapApproval() ? (
<button onClick={handleApproveSwapToken} disabled={isProcessing} className="btn btn-secondary btn-sm">
{isProcessing ? '...' : t('lp.approveToken')}
</button>
) : (
<button
onClick={handleSwap}
disabled={isProcessing || !swapForm.amount || swapForm.tokenIn === swapForm.tokenOut}
className="btn btn-primary btn-sm"
>
{isProcessing ? '...' : t('lp.swap')}
</button>
)}
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,203 @@
/**
* LP 流动性池面板 (重构版)
* 使用拆分的组件和 hooks
*/
import { useState, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useTransactions } from '../../context/TransactionContext'
import { TransactionHistory } from '../TransactionHistory'
import { useLPPoolData } from './useLPPoolData'
import { useLPTransactions } from './useLPTransactions'
import { useLPAdminActions } from './useLPAdminActions'
import { LPPoolInfo } from './LPPoolInfo'
import { LPOperations } from './LPOperations'
import { LPTokenBalances } from './LPTokenBalances'
import { LPAdminConfig } from './LPAdminConfig'
import { LPDebugInfo } from './LPDebugInfo'
import { LPUsdyPanel } from './LPUsdyPanel'
export function LPPanelNew() {
const { t } = useTranslation()
const { transactions, clearHistory } = useTransactions()
// 使用拆分的 hooks
const poolData = useLPPoolData()
// 解构 refetch 函数,这些函数引用是稳定的
const {
refetchBalance,
refetchTokenBalances,
refetchYtLPTotalSupply,
refetchYtLPPrice,
refetchAumInUsdy,
refetchAccountValue,
refetchUsdyAmounts,
refetchTokenWeights,
refetchEmergencyMode,
refetchSwapEnabled,
refetchUsdySupply,
refetchPoolValue,
refetchVaultWhitelist,
} = poolData
// 使用 useCallback 稳定回调,依赖解构后的稳定函数引用
const handleSuccess = useCallback(() => {
// 刷新数据
refetchBalance()
refetchTokenBalances()
refetchYtLPTotalSupply()
refetchYtLPPrice()
refetchAumInUsdy()
refetchAccountValue()
refetchUsdyAmounts()
refetchTokenWeights()
refetchEmergencyMode()
refetchSwapEnabled()
refetchUsdySupply()
refetchPoolValue()
refetchVaultWhitelist()
}, [
refetchBalance, refetchTokenBalances, refetchYtLPTotalSupply, refetchYtLPPrice,
refetchAumInUsdy, refetchAccountValue, refetchUsdyAmounts, refetchTokenWeights,
refetchEmergencyMode, refetchSwapEnabled, refetchUsdySupply, refetchPoolValue,
refetchVaultWhitelist
])
// 使用 useMemo 稳定 callbacks 对象
const txCallbacks = useMemo(() => ({
onSuccess: handleSuccess,
}), [handleSuccess])
const {
writeContract,
recordTx,
isProcessing,
} = useLPTransactions(txCallbacks)
// 管理员操作 - recordTx 需要 cast 为 string 类型
const adminActions = useLPAdminActions({
address: poolData.address,
writeContract,
recordTx: recordTx as (type: string, amount?: string, token?: string) => void,
})
// UI 状态
const [showAdminConfig, setShowAdminConfig] = useState(false)
const [showDebugInfo, setShowDebugInfo] = useState(false)
const [showUsdyPanel, setShowUsdyPanel] = useState(false)
// 未连接钱包
if (!poolData.isConnected) {
return (
<div className="panel">
<h2>{t('lp.title')}</h2>
<p className="text-muted">{t('common.connectFirst')}</p>
</div>
)
}
return (
<div className="panel">
<h2>{t('lp.title')}</h2>
{/* 池子信息 */}
<LPPoolInfo
ytLPName={poolData.ytLPName as string}
ytLPSymbol={poolData.ytLPSymbol as string}
ytLPDecimals={poolData.ytLPDecimals as number}
ytLPBalance={poolData.ytLPBalance as bigint}
ytLPTotalSupply={poolData.ytLPTotalSupply as bigint}
ytLPPrice={poolData.ytLPPrice as bigint}
aumInUsdy={poolData.aumInUsdy as bigint}
accountValue={poolData.accountValue as bigint}
cooldownDuration={poolData.cooldownDuration as bigint}
lastAddedAt={poolData.lastAddedAt as bigint}
emergencyMode={poolData.emergencyMode as boolean}
swapEnabled={poolData.swapEnabled as boolean}
poolValue={poolData.poolValue as bigint}
/>
{/* 代币余额表格 */}
<LPTokenBalances
poolTokens={poolData.poolTokens}
allAvailableTokens={poolData.allAvailableTokens}
/>
{/* LP 操作 */}
<LPOperations
address={poolData.address}
poolTokens={poolData.poolTokens}
ytLPBalance={poolData.ytLPBalance as bigint}
ytLPPrice={poolData.ytLPPrice as bigint}
isProcessing={isProcessing}
writeContract={writeContract}
recordTx={recordTx}
getTokenSymbol={poolData.getTokenSymbol}
swapFee={poolData.swapFee as bigint}
taxBasisPoints={poolData.taxBasisPoints as bigint}
dynamicFees={poolData.dynamicFees as boolean}
/>
{/* 管理员配置 */}
<LPAdminConfig
address={poolData.address}
isProcessing={isProcessing}
poolPaused={poolData.poolPaused as boolean}
dynamicFees={poolData.dynamicFees as boolean}
emergencyMode={poolData.emergencyMode as boolean}
swapEnabled={poolData.swapEnabled as boolean}
cooldownDuration={poolData.cooldownDuration as bigint}
usdcPriceSource={poolData.usdcPriceSource as string}
availableTokens={poolData.allAvailableTokens}
handleSetCooldownDuration={adminActions.handleSetCooldownDuration}
handleSetSwapFees={adminActions.handleSetSwapFees}
handleWithdrawToken={adminActions.handleWithdrawToken}
handleSetDynamicFees={adminActions.handleSetDynamicFees}
handlePausePool={adminActions.handlePausePool}
handleUnpausePool={adminActions.handleUnpausePool}
handleSetMaxSwapAmount={adminActions.handleSetMaxSwapAmount}
handleSetAumAdjustment={adminActions.handleSetAumAdjustment}
handleSetMaxSwapSlippageBps={adminActions.handleSetMaxSwapSlippageBps}
handleSetMaxPriceChangeBps={adminActions.handleSetMaxPriceChangeBps}
handleSetPoolManagerGov={adminActions.handleSetPoolManagerGov}
handleSetVaultGov={adminActions.handleSetVaultGov}
handleSetHandler={adminActions.handleSetHandler}
handleSetKeeper={adminActions.handleSetKeeper}
handleSetSwapper={adminActions.handleSetSwapper}
handleSetPoolManager={adminActions.handleSetPoolManager}
handleSetMinter={adminActions.handleSetMinter}
handleSetWhitelistedToken={adminActions.handleSetWhitelistedToken}
handleClearWhitelistedToken={adminActions.handleClearWhitelistedToken}
handleSetEmergencyMode={adminActions.handleSetEmergencyMode}
handleSetSwapEnabled={adminActions.handleSetSwapEnabled}
handleSetPrice={adminActions.handleSetPrice}
handleSetSpread={adminActions.handleSetSpread}
handleSetUsdcPriceSource={adminActions.handleSetUsdcPriceSource}
handleSetStableToken={adminActions.handleSetStableToken}
handleAddUsdyVault={adminActions.handleAddUsdyVault}
handleRemoveUsdyVault={adminActions.handleRemoveUsdyVault}
show={showAdminConfig}
onToggle={() => setShowAdminConfig(!showAdminConfig)}
/>
{/* USDY 面板 */}
<LPUsdyPanel
show={showUsdyPanel}
onToggle={() => setShowUsdyPanel(!showUsdyPanel)}
/>
{/* 调试信息 */}
<LPDebugInfo
vaultGov={poolData.vaultGov as string}
poolManagerGov={poolData.poolManagerGov as string}
show={showDebugInfo}
onToggle={() => setShowDebugInfo(!showDebugInfo)}
/>
{/* 交易历史 */}
<TransactionHistory transactions={transactions} onClear={clearHistory} />
</div>
)
}

View File

@@ -0,0 +1,110 @@
/**
* LP 池子信息显示组件
* 显示 ytLP 代币信息、池子数据、用户数据
*/
import { useTranslation } from 'react-i18next'
import { formatUnits } from 'viem'
import { useChainId } from 'wagmi'
import { getContracts, getDecimals } from '../../config/contracts'
interface LPPoolInfoProps {
// ytLP 代币信息
ytLPName?: string
ytLPSymbol?: string
ytLPDecimals?: number
ytLPBalance?: bigint
ytLPTotalSupply?: bigint
ytLPPrice?: bigint
// 池子数据
aumInUsdy?: bigint
accountValue?: bigint
cooldownDuration?: bigint
lastAddedAt?: bigint
// 状态
emergencyMode?: boolean
swapEnabled?: boolean
poolValue?: bigint
}
export function LPPoolInfo({
ytLPName,
ytLPSymbol,
ytLPDecimals,
ytLPBalance,
ytLPTotalSupply,
ytLPPrice,
aumInUsdy,
accountValue,
cooldownDuration,
lastAddedAt,
emergencyMode,
swapEnabled,
poolValue,
}: LPPoolInfoProps) {
const { t } = useTranslation()
// ===== 多链支持 =====
const chainId = useChainId()
const CONTRACTS = getContracts(chainId)
const TOKEN_DECIMALS = getDecimals(chainId)
// 计算冷却剩余时间
const getCooldownRemaining = () => {
if (!lastAddedAt || !cooldownDuration) return 0
const cooldownEnd = Number(lastAddedAt) + Number(cooldownDuration)
const now = Math.floor(Date.now() / 1000)
return Math.max(0, cooldownEnd - now)
}
const formatCooldown = (seconds: number) => {
if (seconds <= 0) return t('lp.noCooldown')
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}m ${secs}s`
}
return (
<div className="pool-info" style={{ fontSize: '12px' }}>
{/* ytLP 代币信息 */}
<div style={{ display: 'flex', gap: '16px', marginBottom: '8px', flexWrap: 'wrap', padding: '8px', background: '#f8f9fa', borderRadius: '4px' }}>
<span style={{ color: '#666' }}>: <strong>{ytLPName || 'ytLP'}</strong></span>
<span style={{ color: '#666' }}>: <strong>{ytLPSymbol || 'ytLP'}</strong></span>
<span style={{ color: '#666' }}>: <strong>{ytLPDecimals?.toString() || '18'}</strong></span>
<span style={{ color: '#666' }}>: <code style={{ fontSize: '11px' }}>{CONTRACTS.YT_LP_TOKEN.slice(0, 8)}...{CONTRACTS.YT_LP_TOKEN.slice(-6)}</code></span>
</div>
{/* 池子数据 + 用户数据 - 网格布局 */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '8px 16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<div title="管理资产总额,池中所有代币的总价值 / Assets Under Management, total value of all tokens in pool" style={{ cursor: 'help' }}>
<span style={{ color: '#666' }}>AUM:</span> <strong>{aumInUsdy ? Number(formatUnits(aumInUsdy, TOKEN_DECIMALS.USDY)).toFixed(2) : '0'} USDY</strong>
</div>
<div title="ytLP代币价格 = AUM / ytLP总供应量 / ytLP token price = AUM / total supply" style={{ cursor: 'help' }}>
<span style={{ color: '#666' }}>ytLP价格:</span> <strong>{ytLPPrice ? Number(formatUnits(ytLPPrice, TOKEN_DECIMALS.INTERNAL_PRICE)).toFixed(6) : '1'}</strong>
</div>
<div title="ytLP代币总发行量 / Total ytLP tokens minted" style={{ cursor: 'help' }}>
<span style={{ color: '#666' }}>:</span> <strong>{ytLPTotalSupply ? Number(formatUnits(ytLPTotalSupply, TOKEN_DECIMALS.YT_LP)).toFixed(2) : '0'}</strong>
</div>
<div title="你持有的ytLP代币数量 / Your ytLP token balance" style={{ cursor: 'help' }}>
<span style={{ color: '#666' }}>ytLP:</span> <strong>{ytLPBalance ? Number(formatUnits(ytLPBalance, TOKEN_DECIMALS.YT_LP)).toFixed(2) : '0'}</strong>
</div>
<div title="添加流动性后的冷却时间,期间无法移除流动性 / Cooldown after adding liquidity, cannot remove during this period" style={{ cursor: 'help' }}>
<span style={{ color: '#666' }}>:</span> <strong>{formatCooldown(getCooldownRemaining())}</strong>
</div>
<div title="你的账户在池中的总价值USD / Your account value in the pool (USD)" style={{ cursor: 'help' }}>
<span style={{ color: '#666' }}>:</span> <strong>${accountValue ? Number(formatUnits(accountValue, TOKEN_DECIMALS.INTERNAL_PRICE)).toFixed(6) : '0'}</strong>
</div>
<div title="紧急模式开启时,部分功能将被禁用 / When emergency mode is ON, some functions are disabled" style={{ cursor: 'help' }}>
<span style={{ color: '#666' }}>:</span>{' '}
<strong style={{ color: emergencyMode ? '#f44336' : '#4caf50' }}>{emergencyMode ? 'ON' : '正常'}</strong>
</div>
<div title="Swap 功能开关 / Swap function toggle" style={{ cursor: 'help' }}>
<span style={{ color: '#666' }}>Swap:</span>{' '}
<strong style={{ color: swapEnabled ? '#4caf50' : '#f44336' }}>{swapEnabled ? '开启' : '关闭'}</strong>
</div>
<div title="池子价值 (USD) / Pool value (USD)" style={{ cursor: 'help' }}>
<span style={{ color: '#666' }}>:</span> <strong>${poolValue ? Number(formatUnits(poolValue, TOKEN_DECIMALS.INTERNAL_PRICE)).toFixed(2) : '0'}</strong>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,231 @@
/**
* LP 代币余额表格组件
* 显示池子中所有代币的余额、权重、比例等信息
*/
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { formatUnits } from 'viem'
import { TOKEN_DECIMALS, getTokenDecimals } from '../../config/contracts'
import type { ExtendedPoolToken, AvailableToken } from './useLPPoolData'
interface TokenWithStats extends ExtendedPoolToken {
targetRatio: number
currentRatio: number
deviation: number
inPool: boolean
}
interface PoolStats {
tokens: TokenWithStats[]
totalUsdy: bigint
totalWeight: bigint
}
interface LPTokenBalancesProps {
poolTokens: ExtendedPoolToken[]
allAvailableTokens?: AvailableToken[]
totalTokenWeights?: bigint
}
export function LPTokenBalances({ poolTokens, allAvailableTokens, totalTokenWeights }: LPTokenBalancesProps) {
const { t } = useTranslation()
// 计算池子统计数据,合并显示所有代币(池子代币 + Factory代币
const poolStats = useMemo((): PoolStats | null => {
if (!poolTokens || poolTokens.length === 0) {
// 如果池子没有代币,但有 Factory 代币,也显示
if (allAvailableTokens && allAvailableTokens.length > 0) {
const factoryOnlyTokens: TokenWithStats[] = allAvailableTokens
.filter(t => t.source === 'factory')
.map(t => ({
address: t.address as `0x${string}`,
symbol: t.symbol,
name: t.name,
decimals: 18,
balance: 0n,
isWhitelisted: t.isWhitelisted,
usdyAmount: 0n,
isStable: t.isStable,
weight: 0n,
targetRatio: 0,
currentRatio: 0,
deviation: 0,
inPool: false,
}))
if (factoryOnlyTokens.length > 0) {
return { tokens: factoryOnlyTokens, totalUsdy: 0n, totalWeight: 0n }
}
}
return null
}
const totalUsdy = poolTokens.reduce((sum, t) => sum + (t.usdyAmount || 0n), 0n)
const totalWeight = totalTokenWeights || poolTokens.reduce((sum, t) => sum + (t.weight || 0n), 0n)
// 池子代币
const poolTokensWithStats: TokenWithStats[] = poolTokens.map(token => {
const targetRatio = totalWeight > 0n
? (Number(token.weight || 0n) / Number(totalWeight)) * 100
: 0
const currentRatio = totalUsdy > 0n
? (Number(token.usdyAmount || 0n) / Number(totalUsdy)) * 100
: 0
const deviation = currentRatio - targetRatio
return {
...token,
targetRatio,
currentRatio,
deviation,
inPool: true,
}
})
// 添加 Factory 里有但池子里没有的代币
const poolAddresses = new Set(poolTokens.map(t => t.address.toLowerCase()))
const factoryOnlyTokens: TokenWithStats[] = (allAvailableTokens || [])
.filter(t => t.source === 'factory' && !poolAddresses.has(t.address.toLowerCase()))
.map(t => ({
address: t.address as `0x${string}`,
symbol: t.symbol,
name: t.name,
decimals: 18,
balance: 0n,
isWhitelisted: t.isWhitelisted,
usdyAmount: 0n,
isStable: t.isStable,
weight: 0n,
targetRatio: 0,
currentRatio: 0,
deviation: 0,
inPool: false,
}))
return { tokens: [...poolTokensWithStats, ...factoryOnlyTokens], totalUsdy, totalWeight }
}, [poolTokens, allAvailableTokens, totalTokenWeights])
if (!poolStats) {
return (
<div className="balance-info" style={{ marginTop: '12px' }}>
<h4 style={{ marginBottom: '8px', fontSize: '14px' }}>{t('lp.yourTokenBalances')}</h4>
<p style={{ color: '#666', fontSize: '12px' }}>Loading...</p>
</div>
)
}
return (
<div className="balance-info" style={{ marginTop: '12px' }}>
<h4 style={{ marginBottom: '8px', fontSize: '14px' }}>{t('lp.yourTokenBalances')}</h4>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', fontSize: '12px', borderCollapse: 'collapse', minWidth: '600px' }}>
<thead>
<tr style={{ borderBottom: '2px solid #ddd', background: '#f8f9fa' }}>
<th style={{ textAlign: 'left', padding: '6px 4px', cursor: 'help' }} title="代币符号 / Token Symbol">
{t('lp.selectToken')}
</th>
<th style={{ textAlign: 'center', padding: '6px 4px', cursor: 'help' }} title="是否在白名单中 / Is whitelisted">
WL
</th>
<th style={{ textAlign: 'center', padding: '6px 4px', cursor: 'help' }} title="是否在池子中 / Is in pool">
Pool
</th>
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="你的钱包余额 / Your wallet balance">
{t('common.balance')}
</th>
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="池中代币价值 / Token value in pool">
USDY
</th>
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="代币权重,用于计算目标比例 / Token weight for target ratio">
Weight
</th>
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="目标比例 = 权重/总权重 / Target ratio = weight/totalWeights">
Target
</th>
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="当前比例 = USDY数量/总USDY / Current ratio = usdyAmount/totalUsdy">
Current
</th>
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="偏差 = 当前 - 目标。正值:代币过多(卖出费率高)。负值:代币不足(买入费率低)">
Deviation
</th>
<th style={{ textAlign: 'center', padding: '6px 4px', cursor: 'help' }} title="是否稳定币(更低的交换费率 0.04%">
Stable
</th>
</tr>
</thead>
<tbody>
{poolStats.tokens.map((token) => (
<tr key={token.address} style={{ borderBottom: '1px solid #eee', opacity: token.inPool ? 1 : 0.7 }}>
<td style={{ padding: '4px' }}>
<strong>{token.symbol}</strong>
</td>
<td style={{ textAlign: 'center', padding: '4px' }}>
{token.isWhitelisted ? (
<span style={{ color: '#4caf50', fontWeight: 'bold' }}></span>
) : (
<span style={{ color: '#f44336', fontWeight: 'bold' }}></span>
)}
</td>
<td style={{ textAlign: 'center', padding: '4px' }}>
{token.inPool ? (
<span style={{ color: '#4caf50', fontWeight: 'bold' }}></span>
) : (
<span style={{ color: '#ff9800', fontSize: '10px' }}></span>
)}
</td>
<td style={{ textAlign: 'right', padding: '4px' }}>
{token.balance ? Number(formatUnits(token.balance, getTokenDecimals(token.address))).toFixed(2) : '0'}
</td>
<td style={{ textAlign: 'right', padding: '4px' }}>
{token.usdyAmount ? Number(formatUnits(token.usdyAmount, TOKEN_DECIMALS.USDY)).toFixed(2) : '0'}
</td>
<td style={{ textAlign: 'right', padding: '4px' }}>
{token.weight ? token.weight.toString() : '0'}
</td>
<td style={{ textAlign: 'right', padding: '4px' }}>
{token.targetRatio.toFixed(1)}%
</td>
<td style={{ textAlign: 'right', padding: '4px' }}>
{token.currentRatio.toFixed(1)}%
</td>
<td style={{ textAlign: 'right', padding: '4px' }}>
<span style={{
color: Math.abs(token.deviation) < 1 ? '#4caf50' :
Math.abs(token.deviation) < 5 ? '#ff9800' : '#f44336',
fontWeight: Math.abs(token.deviation) >= 5 ? 'bold' : 'normal'
}}>
{token.deviation >= 0 ? '+' : ''}{token.deviation.toFixed(1)}%
</span>
</td>
<td style={{ textAlign: 'center', padding: '4px' }}>
{token.isStable ? <span style={{ color: '#2196f3' }}>Yes</span> : <span style={{ color: '#999' }}>-</span>}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr style={{ borderTop: '2px solid #ddd', background: '#f8f9fa', fontWeight: 'bold' }}>
<td style={{ padding: '6px 4px' }}>Total</td>
<td style={{ textAlign: 'center', padding: '6px 4px' }}>-</td>
<td style={{ textAlign: 'center', padding: '6px 4px' }}>-</td>
<td style={{ textAlign: 'right', padding: '6px 4px' }}>-</td>
<td style={{ textAlign: 'right', padding: '6px 4px' }}>
{Number(formatUnits(poolStats.totalUsdy, TOKEN_DECIMALS.USDY)).toFixed(2)}
</td>
<td style={{ textAlign: 'right', padding: '6px 4px' }}>
{poolStats.totalWeight.toString()}
</td>
<td style={{ textAlign: 'right', padding: '6px 4px' }}>100%</td>
<td style={{ textAlign: 'right', padding: '6px 4px' }}>100%</td>
<td style={{ textAlign: 'right', padding: '6px 4px' }}>-</td>
<td style={{ textAlign: 'center', padding: '6px 4px' }}>-</td>
</tr>
</tfoot>
</table>
</div>
<p style={{ fontSize: '10px', color: '#999', marginTop: '6px' }}>
Deviation: 偏差值
</p>
</div>
)
}

View File

@@ -0,0 +1,202 @@
/**
* LP USDY 面板组件
* 显示 USDY 相关操作
*/
import { useState } from 'react'
import { formatUnits, parseUnits } from 'viem'
import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, useChainId } from 'wagmi'
import { GAS_CONFIG, getContracts, getDecimals } from '../../config/contracts'
import { ERC20_BASE_ABI } from './types'
import { useToast } from '../Toast'
interface LPUsdyPanelProps {
show: boolean
onToggle: () => void
}
export function LPUsdyPanel({
show,
onToggle,
}: LPUsdyPanelProps) {
const { address } = useAccount()
const { showToast } = useToast()
// ===== 多链支持 =====
const chainId = useChainId()
const CONTRACTS = getContracts(chainId)
const TOKEN_DECIMALS = getDecimals(chainId)
const [transferForm, setTransferForm] = useState({
recipient: '',
amount: '',
})
// 读取 USDY 余额
const { data: usdyBalance } = useReadContract({
address: CONTRACTS.USDY as `0x${string}`,
abi: ERC20_BASE_ABI,
functionName: 'balanceOf',
args: address ? [address] : undefined,
})
// 读取 USDY 授权额度
const { data: usdyAllowance } = useReadContract({
address: CONTRACTS.USDY as `0x${string}`,
abi: ERC20_BASE_ABI,
functionName: 'allowance',
args: address ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined,
})
const { writeContract, data: hash, isPending } = useWriteContract()
const { isLoading: isConfirming } = useWaitForTransactionReceipt({ hash })
const isProcessing = isPending || isConfirming
// 授权 USDY 给 Router
const handleApproveUsdy = () => {
if (!address) return
writeContract({
address: CONTRACTS.USDY as `0x${string}`,
abi: ERC20_BASE_ABI,
functionName: 'approve',
args: [CONTRACTS.YT_REWARD_ROUTER, parseUnits('999999999', TOKEN_DECIMALS.USDY)],
gas: GAS_CONFIG.SIMPLE,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
showToast('info', '正在授权 USDY...')
}
// 转账 USDY
const handleTransferUsdy = () => {
if (!address || !transferForm.recipient || !transferForm.amount) return
writeContract({
address: CONTRACTS.USDY as `0x${string}`,
abi: ERC20_BASE_ABI,
functionName: 'transfer',
args: [
transferForm.recipient as `0x${string}`,
parseUnits(transferForm.amount, TOKEN_DECIMALS.USDY),
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
showToast('info', '正在转账 USDY...')
}
const inputStyle = {
padding: '6px 8px',
fontSize: '12px',
border: '1px solid #ddd',
borderRadius: '4px',
width: '100%',
marginBottom: '6px',
}
return (
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f9f9f9', borderRadius: '8px', border: '1px solid #e0e0e0' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={onToggle}
>
<h4 style={{ margin: 0, color: '#333', fontSize: '13px' }}>USDY </h4>
<span style={{ color: '#999', fontSize: '16px' }}>{show ? '▼' : '▶'}</span>
</div>
{show && (
<div style={{ marginTop: '12px' }}>
{/* USDY 余额信息 */}
<div style={{
padding: '10px',
background: '#fff',
borderRadius: '6px',
marginBottom: '12px',
border: '1px solid #e0e0e0'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '11px', color: '#666' }}>USDY :</span>
<span style={{ fontSize: '13px', fontWeight: 'bold', color: '#333' }}>
{usdyBalance
? Number(formatUnits(usdyBalance, TOKEN_DECIMALS.USDY)).toFixed(4)
: '0'} USDY
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: '11px', color: '#666' }}>Router :</span>
<span style={{ fontSize: '11px', color: '#666' }}>
{usdyAllowance !== undefined
? (Number(formatUnits(usdyAllowance as bigint, TOKEN_DECIMALS.USDY)) > 1000000
? 'Unlimited'
: Number(formatUnits(usdyAllowance as bigint, TOKEN_DECIMALS.USDY)).toFixed(4))
: '0'}
</span>
</div>
</div>
{/* 授权按钮 */}
<div style={{ marginBottom: '12px' }}>
<button
onClick={handleApproveUsdy}
disabled={isProcessing || !address}
className="btn btn-sm btn-success"
style={{ fontSize: '11px', width: '100%' }}
>
{isProcessing ? '处理中...' : '授权 USDY 给 Router (无限额度)'}
</button>
</div>
{/* 转账 USDY */}
<div style={{
padding: '10px',
background: '#fff',
borderRadius: '6px',
border: '1px solid #e0e0e0'
}}>
<div style={{ fontSize: '11px', fontWeight: 'bold', color: '#333', marginBottom: '8px' }}>
USDY
</div>
<input
type="text"
value={transferForm.recipient}
onChange={(e) => setTransferForm({ ...transferForm, recipient: e.target.value })}
style={inputStyle}
placeholder="接收地址 (0x...)"
/>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={transferForm.amount}
onChange={(e) => setTransferForm({ ...transferForm, amount: e.target.value })}
style={{ ...inputStyle, flex: 1 }}
placeholder="数量"
/>
<button
onClick={() => {
if (usdyBalance) {
setTransferForm({
...transferForm,
amount: formatUnits(usdyBalance, TOKEN_DECIMALS.USDY),
})
}
}}
className="btn btn-sm btn-outline"
style={{ fontSize: '10px', whiteSpace: 'nowrap' }}
>
MAX
</button>
</div>
<button
onClick={handleTransferUsdy}
disabled={isProcessing || !address || !transferForm.recipient || !transferForm.amount}
className="btn btn-sm btn-primary"
style={{ fontSize: '11px', width: '100%', marginTop: '8px' }}
>
{isProcessing ? '处理中...' : '转账'}
</button>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,15 @@
/**
* LP 模块导出
*/
export * from './types'
export * from './useLPPoolData'
export * from './useLPTransactions'
export * from './useLPAdminActions'
export * from './LPPoolInfo'
export * from './LPOperations'
export * from './LPTokenBalances'
export * from './LPAdminConfig'
export * from './LPDebugInfo'
export * from './LPUsdyPanel'
export * from './LPPanelNew'

View File

@@ -0,0 +1,134 @@
/**
* LP 流动性池相关类型定义
*/
// 池子代币信息
export interface PoolToken {
address: `0x${string}`
symbol: string
name: string
decimals: number
}
// 添加流动性表单
export interface AddLiquidityForm {
token: string
amount: string
slippage: string
}
// 移除流动性表单
export interface RemoveLiquidityForm {
token: string
amount: string
slippage: string
}
// 交换表单
export interface SwapForm {
tokenIn: string
tokenOut: string
amount: string
slippage: string
}
// 价格配置表单
export interface PriceConfigForm {
token: string
price: string
spreadBps: string
}
// Vault 配置表单
export interface VaultConfigForm {
token: string
tokenDecimals: string
tokenWeight: string
maxUsdyAmount: string
}
// YT 价格表单
export interface YtPriceForm {
ytPrice: string
}
// 稳定代币配置
export interface StableTokenConfig {
token: string
isStable: boolean
}
// P1: 冷却时间表单
export interface CooldownForm {
duration: string
}
// P1: 交换手续费表单
export interface SwapFeesForm {
swapFee: string
stableSwapFee: string
taxBasisPoints: string
stableTaxBasisPoints: string
}
// P1: 提取代币表单
export interface WithdrawForm {
token: string
receiver: string
amount: string
}
// P2: AUM 调整表单
export interface AumAdjustForm {
addition: string
deduction: string
}
// P2: 限制表单
export interface LimitsForm {
maxSwapSlippageBps: string
maxPriceChangeBps: string
}
// P2: 最大交换金额表单
export interface MaxSwapForm {
token: string
amount: string
}
// P3: 权限管理表单
export interface PermissionForm {
govAddress: string
handlerAddress: string
handlerActive: boolean
keeperAddress: string
keeperActive: boolean
swapperAddress: string
swapperActive: boolean
minterAddress: string
minterActive: boolean
poolManagerAddress: string
}
// LP Panel Context - 共享状态
export interface LPPanelContext {
address: `0x${string}` | undefined
isConnected: boolean
isProcessing: boolean
poolTokens: PoolToken[]
allAvailableTokens: PoolToken[]
showToast: (type: 'success' | 'error' | 'info' | 'warning', message: string) => void
recordTx: (type: string, amount?: string, token?: string) => void
writeContract: (config: any) => void
refetchAll: () => void
}
// ERC20 基础 ABI
export const ERC20_BASE_ABI = [
{ inputs: [], name: 'symbol', outputs: [{ type: 'string' }], stateMutability: 'view', type: 'function' },
{ inputs: [], name: 'name', outputs: [{ type: 'string' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ type: 'address' }, { type: 'address' }], name: 'allowance', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ type: 'address' }, { type: 'uint256' }], name: 'approve', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [{ type: 'address' }, { type: 'uint256' }], name: 'transfer', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
] as const

View File

@@ -0,0 +1,525 @@
/**
* LP 管理员操作 Hook
* 包含 P1-P3 级别的管理功能
*/
import { parseUnits } from 'viem'
import {
CONTRACTS,
GAS_CONFIG,
TOKEN_DECIMALS,
getTokenDecimals,
YT_POOL_MANAGER_ABI,
YT_VAULT_ABI,
YT_LP_TOKEN_ABI,
YT_PRICE_FEED_ABI,
FACTORY_ABI,
USDY_ABI,
} from '../../config/contracts'
import type {
CooldownForm,
SwapFeesForm,
WithdrawForm,
AumAdjustForm,
LimitsForm,
MaxSwapForm,
PermissionForm,
PriceConfigForm,
VaultConfigForm,
StableTokenConfig,
} from './types'
interface UseLPAdminActionsProps {
address: `0x${string}` | undefined
writeContract: (config: any) => void
recordTx: (type: string, amount?: string, token?: string) => void
}
export function useLPAdminActions({ address, writeContract, recordTx }: UseLPAdminActionsProps) {
// ===== P1: 核心配置 =====
// 设置冷却时间
const handleSetCooldownDuration = (form: CooldownForm) => {
if (!address || !form.duration) return
recordTx('test', form.duration, 'SetCooldownDuration')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'setCooldownDuration',
args: [BigInt(form.duration)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置交换手续费
const handleSetSwapFees = (form: SwapFeesForm) => {
if (!address) return
recordTx('test', 'SwapFees', 'SetSwapFees')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setSwapFees',
args: [
BigInt(form.swapFee),
BigInt(form.stableSwapFee),
BigInt(form.taxBasisPoints),
BigInt(form.stableTaxBasisPoints)
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 紧急提取代币
const handleWithdrawToken = (form: WithdrawForm) => {
if (!address || !form.token || !form.receiver || !form.amount) return
recordTx('test', form.amount, 'WithdrawToken')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'withdrawToken',
args: [
form.token as `0x${string}`,
form.receiver as `0x${string}`,
parseUnits(form.amount, getTokenDecimals(form.token))
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// ===== P2: 交易限制 =====
// 设置动态手续费开关
const handleSetDynamicFees = (enabled: boolean) => {
if (!address) return
recordTx('test', enabled ? 'ON' : 'OFF', 'SetDynamicFees')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setDynamicFees',
args: [enabled],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 暂停 LP Pool
const handlePausePool = () => {
if (!address) return
recordTx('test', 'Pause', 'PausePool')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'pause',
args: [],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 恢复 LP Pool
const handleUnpausePool = () => {
if (!address) return
recordTx('test', 'Unpause', 'UnpausePool')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'unpause',
args: [],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 AUM 调整
const handleSetAumAdjustment = (form: AumAdjustForm) => {
if (!address) return
recordTx('test', `+${form.addition}/-${form.deduction}`, 'SetAumAdjustment')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'setAumAdjustment',
args: [
parseUnits(form.addition || '0', TOKEN_DECIMALS.USDY),
parseUnits(form.deduction || '0', TOKEN_DECIMALS.USDY)
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置最大交换金额
const handleSetMaxSwapAmount = (form: MaxSwapForm) => {
if (!address || !form.token || !form.amount) return
recordTx('test', form.amount, 'SetMaxSwapAmount')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setMaxSwapAmount',
args: [
form.token as `0x${string}`,
parseUnits(form.amount, getTokenDecimals(form.token))
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置最大滑点
const handleSetMaxSwapSlippageBps = (form: LimitsForm) => {
if (!address || !form.maxSwapSlippageBps) return
recordTx('test', form.maxSwapSlippageBps, 'SetMaxSwapSlippageBps')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setMaxSwapSlippageBps',
args: [BigInt(form.maxSwapSlippageBps)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置最大价格变化
const handleSetMaxPriceChangeBps = (form: LimitsForm) => {
if (!address || !form.maxPriceChangeBps) return
recordTx('test', form.maxPriceChangeBps, 'SetMaxPriceChangeBps')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setMaxPriceChangeBps',
args: [BigInt(form.maxPriceChangeBps)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// ===== P3: 权限管理 =====
// 设置 PoolManager Gov
const handleSetPoolManagerGov = (form: PermissionForm) => {
if (!address || !form.govAddress) return
recordTx('test', form.govAddress, 'SetPoolManagerGov')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'setGov',
args: [form.govAddress as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 YTVault Gov
const handleSetVaultGov = (form: PermissionForm) => {
if (!address || !form.govAddress) return
recordTx('test', form.govAddress, 'SetVaultGov')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setGov',
args: [form.govAddress as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 Handler
const handleSetHandler = (form: PermissionForm) => {
if (!address || !form.handlerAddress) return
recordTx('test', form.handlerAddress, 'SetHandler')
writeContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'setHandler',
args: [form.handlerAddress as `0x${string}`, form.handlerActive],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 Keeper
const handleSetKeeper = (form: PermissionForm) => {
if (!address || !form.keeperAddress) return
recordTx('test', form.keeperAddress, 'SetKeeper')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setKeeper',
args: [form.keeperAddress as `0x${string}`, form.keeperActive],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 Swapper
const handleSetSwapper = (form: PermissionForm) => {
if (!address || !form.swapperAddress) return
recordTx('test', form.swapperAddress, 'SetSwapper')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setSwapper',
args: [form.swapperAddress as `0x${string}`, form.swapperActive],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 PoolManager
const handleSetPoolManager = (form: PermissionForm) => {
if (!address || !form.poolManagerAddress) return
recordTx('test', form.poolManagerAddress, 'SetPoolManager')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setPoolManager',
args: [form.poolManagerAddress as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 ytLP Minter
const handleSetMinter = (form: PermissionForm) => {
if (!address || !form.minterAddress) return
recordTx('test', form.minterAddress, 'SetMinter')
writeContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'setMinter',
args: [form.minterAddress as `0x${string}`, form.minterActive],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// ===== 价格配置 =====
// 设置价格
const handleSetPrice = (form: PriceConfigForm) => {
if (!address || !form.price) return
recordTx('test', form.price, 'SetPrice')
const priceIn30Decimals = parseUnits(form.price, TOKEN_DECIMALS.INTERNAL_PRICE)
writeContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'setPrice',
args: [form.token as `0x${string}`, priceIn30Decimals],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置价差
const handleSetSpread = (form: PriceConfigForm) => {
if (!address || !form.spreadBps) return
recordTx('test', form.spreadBps, 'SetSpread')
writeContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'setSpreadBasisPoints',
args: [form.token as `0x${string}`, BigInt(form.spreadBps)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置白名单代币
const handleSetWhitelistedToken = (form: VaultConfigForm) => {
if (!address || !form.token) return
recordTx('test', form.token, 'SetWhitelistedToken')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setWhitelistedToken',
args: [
form.token as `0x${string}`,
BigInt(form.tokenDecimals),
BigInt(form.tokenWeight),
parseUnits(form.maxUsdyAmount, TOKEN_DECIMALS.USDY),
true
],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 移除白名单代币
const handleClearWhitelistedToken = (token: string) => {
if (!address || !token) return
recordTx('test', token, 'ClearWhitelistedToken')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'clearWhitelistedToken',
args: [token as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 USDC 价格源
const handleSetUsdcPriceSource = (source: string) => {
if (!address || !source) return
recordTx('test', source, 'SetUsdcPriceSource')
writeContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'setUsdcPriceSource',
args: [source as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置稳定币
const handleSetStableToken = (config: StableTokenConfig) => {
if (!address) return
recordTx('test', config.token, 'SetStableToken')
writeContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'setStableTokens',
args: [config.token as `0x${string}`, config.isStable],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置紧急模式
const handleSetEmergencyMode = (enabled: boolean) => {
if (!address) return
recordTx('test', enabled ? 'ON' : 'OFF', 'SetEmergencyMode')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setEmergencyMode',
args: [enabled],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置 Swap 开关
const handleSetSwapEnabled = (enabled: boolean) => {
if (!address) return
recordTx('test', enabled ? 'ON' : 'OFF', 'SetSwapEnabled')
writeContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'setSwapEnabled',
args: [enabled],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 更新 ytPrice
const handleUpdateYtPrice = (token: string, ytPrice: string, usdcPrice: bigint) => {
if (!address || !ytPrice || !usdcPrice) return
recordTx('test', ytPrice, 'UpdateYtPrice')
const ytPriceIn30Decimals = parseUnits(ytPrice, TOKEN_DECIMALS.INTERNAL_PRICE)
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'updateVaultPrices',
args: [token as `0x${string}`, usdcPrice, ytPriceIn30Decimals],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// USDY 白名单管理
const handleAddUsdyVault = (vault: string) => {
if (!address || !vault) return
recordTx('test', vault, 'AddUsdyVault')
writeContract({
address: CONTRACTS.USDY,
abi: USDY_ABI,
functionName: 'addVault',
args: [vault as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleRemoveUsdyVault = (vault: string) => {
if (!address || !vault) return
recordTx('test', vault, 'RemoveUsdyVault')
writeContract({
address: CONTRACTS.USDY,
abi: USDY_ABI,
functionName: 'removeVault',
args: [vault as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
return {
// P1
handleSetCooldownDuration,
handleSetSwapFees,
handleWithdrawToken,
// P2
handleSetDynamicFees,
handlePausePool,
handleUnpausePool,
handleSetAumAdjustment,
handleSetMaxSwapAmount,
handleSetMaxSwapSlippageBps,
handleSetMaxPriceChangeBps,
// P3
handleSetPoolManagerGov,
handleSetVaultGov,
handleSetHandler,
handleSetKeeper,
handleSetSwapper,
handleSetPoolManager,
handleSetMinter,
// 价格配置
handleSetPrice,
handleSetSpread,
handleSetWhitelistedToken,
handleClearWhitelistedToken,
handleSetUsdcPriceSource,
handleSetStableToken,
handleSetEmergencyMode,
handleSetSwapEnabled,
handleUpdateYtPrice,
// USDY 白名单
handleAddUsdyVault,
handleRemoveUsdyVault,
}
}

View File

@@ -0,0 +1,449 @@
/**
* LP 池数据读取 Hook
* 集中管理所有池子状态的读取逻辑
*/
import { useMemo } from 'react'
import { useAccount, useReadContract, useReadContracts } from 'wagmi'
import {
CONTRACTS,
TOKEN_DECIMALS,
YT_REWARD_ROUTER_ABI,
YT_LP_TOKEN_ABI,
YT_POOL_MANAGER_ABI,
YT_VAULT_ABI,
YT_PRICE_FEED_ABI,
FACTORY_ABI,
USDY_ABI,
} from '../../config/contracts'
import { ERC20_BASE_ABI, type PoolToken } from './types'
// 扩展的池子代币类型
export interface ExtendedPoolToken extends PoolToken {
balance?: bigint
isWhitelisted?: boolean
usdyAmount?: bigint
isStable?: boolean
weight?: bigint
}
// 管理员配置用的代币类型
export interface AvailableToken {
address: string
symbol: string
name: string
source: 'pool' | 'factory'
isStable?: boolean
isWhitelisted?: boolean
}
export function useLPPoolData() {
const { address, isConnected } = useAccount()
// ===== 动态获取 LP 池代币列表 =====
const { data: rawPoolTokenAddresses } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'getAllPoolTokens',
})
// ===== 从 Factory 获取所有创建的金库 =====
const { data: factoryVaults } = useReadContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'getAllVaults',
})
// 去重处理
const poolTokenAddresses = useMemo(() => {
if (!rawPoolTokenAddresses || rawPoolTokenAddresses.length === 0) return []
const seen = new Set<string>()
return (rawPoolTokenAddresses as string[]).filter((addr: string) => {
const lower = addr.toLowerCase()
if (seen.has(lower)) return false
seen.add(lower)
return true
})
}, [rawPoolTokenAddresses])
// 批量读取代币信息
const tokenInfoContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.flatMap((addr: string) => [
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'symbol' as const },
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'name' as const },
])
}, [poolTokenAddresses])
const { data: tokenInfoResults } = useReadContracts({ contracts: tokenInfoContracts })
// 批量读取 Factory 金库信息
const factoryVaultInfoContracts = useMemo(() => {
if (!factoryVaults || factoryVaults.length === 0) return []
return (factoryVaults as string[]).flatMap((addr: string) => [
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'symbol' as const },
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'name' as const },
])
}, [factoryVaults])
const { data: factoryVaultInfoResults } = useReadContracts({ contracts: factoryVaultInfoContracts })
// 批量读取用户余额
const balanceContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0 || !address) return []
return poolTokenAddresses.map((addr: string) => ({
address: addr as `0x${string}`,
abi: ERC20_BASE_ABI,
functionName: 'balanceOf' as const,
args: [address] as const,
}))
}, [poolTokenAddresses, address])
const { data: balanceResults, refetch: refetchTokenBalances } = useReadContracts({ contracts: balanceContracts })
// 批量读取白名单状态
const whitelistContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string) => ({
address: CONTRACTS.YT_VAULT as `0x${string}`,
abi: YT_VAULT_ABI,
functionName: 'whitelistedTokens' as const,
args: [addr] as const,
}))
}, [poolTokenAddresses])
const { data: whitelistResults, refetch: refetchVaultWhitelist } = useReadContracts({ contracts: whitelistContracts })
// 批量读取 usdyAmounts
const usdyAmountsContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string) => ({
address: CONTRACTS.YT_VAULT as `0x${string}`,
abi: YT_VAULT_ABI,
functionName: 'usdyAmounts' as const,
args: [addr] as const,
}))
}, [poolTokenAddresses])
const { data: usdyAmountsResults, refetch: refetchUsdyAmounts } = useReadContracts({ contracts: usdyAmountsContracts })
// 批量读取稳定币状态
const stableTokensContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string) => ({
address: CONTRACTS.YT_PRICE_FEED as `0x${string}`,
abi: YT_PRICE_FEED_ABI,
functionName: 'stableTokens' as const,
args: [addr] as const,
}))
}, [poolTokenAddresses])
const { data: stableTokensResults } = useReadContracts({ contracts: stableTokensContracts })
// 批量读取代币权重
const tokenWeightsContracts = useMemo(() => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string) => ({
address: CONTRACTS.YT_VAULT as `0x${string}`,
abi: YT_VAULT_ABI,
functionName: 'tokenWeights' as const,
args: [addr] as const,
}))
}, [poolTokenAddresses])
const { data: tokenWeightsResults, refetch: refetchTokenWeights } = useReadContracts({ contracts: tokenWeightsContracts })
// 构建代币列表
const poolTokens = useMemo((): ExtendedPoolToken[] => {
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
return poolTokenAddresses.map((addr: string, index: number) => {
const symbol = tokenInfoResults?.[index * 2]?.result as string | undefined
const name = tokenInfoResults?.[index * 2 + 1]?.result as string | undefined
const balance = balanceResults?.[index]?.result as bigint | undefined
const isWhitelisted = whitelistResults?.[index]?.result as boolean | undefined
const usdyAmount = usdyAmountsResults?.[index]?.result as bigint | undefined
const isStable = stableTokensResults?.[index]?.result as boolean | undefined
const weight = tokenWeightsResults?.[index]?.result as bigint | undefined
return {
address: addr as `0x${string}`,
symbol: symbol || `Token ${index}`,
name: name || `Unknown Token ${index}`,
decimals: TOKEN_DECIMALS.YT, // YT 代币默认 18 位
balance,
isWhitelisted,
usdyAmount,
isStable,
weight,
}
})
}, [poolTokenAddresses, tokenInfoResults, balanceResults, whitelistResults, usdyAmountsResults, stableTokensResults, tokenWeightsResults])
// 合并所有可用代币(用于管理员配置)
const allAvailableTokens = useMemo((): AvailableToken[] => {
const poolAddressSet = new Set(poolTokenAddresses?.map((a: string) => a.toLowerCase()) || [])
const factoryOnlyTokens: AvailableToken[] = []
if (factoryVaults && factoryVaults.length > 0) {
(factoryVaults as string[]).forEach((addr: string, index: number) => {
if (!poolAddressSet.has(addr.toLowerCase())) {
const symbol = factoryVaultInfoResults?.[index * 2]?.result as string | undefined
const name = factoryVaultInfoResults?.[index * 2 + 1]?.result as string | undefined
factoryOnlyTokens.push({
address: addr,
symbol: symbol || `Vault ${index}`,
name: name || `Factory Vault ${index}`,
source: 'factory',
})
}
})
}
const poolWithSource: AvailableToken[] = poolTokens.map(t => ({
address: t.address,
symbol: t.symbol,
name: t.name,
source: 'pool' as const,
isStable: t.isStable,
isWhitelisted: t.isWhitelisted,
}))
const usdcInList = [...poolWithSource, ...factoryOnlyTokens].some(
t => t.address.toLowerCase() === CONTRACTS.USDC.toLowerCase()
)
const result: AvailableToken[] = [...poolWithSource, ...factoryOnlyTokens]
if (!usdcInList) {
result.push({
address: CONTRACTS.USDC,
symbol: 'USDC',
name: 'USD Coin',
source: 'pool',
})
}
return result
}, [poolTokens, poolTokenAddresses, factoryVaults, factoryVaultInfoResults])
// ===== ytLP 代币信息 =====
const { data: ytLPBalance, refetch: refetchBalance } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'balanceOf',
args: address ? [address] : undefined,
})
const { data: ytLPTotalSupply, refetch: refetchYtLPTotalSupply } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'totalSupply',
})
const { data: ytLPDecimals } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'decimals',
})
const { data: ytLPName } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'name',
})
const { data: ytLPSymbol } = useReadContract({
address: CONTRACTS.YT_LP_TOKEN,
abi: YT_LP_TOKEN_ABI,
functionName: 'symbol',
})
const { data: ytLPPrice, refetch: refetchYtLPPrice } = useReadContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'getYtLPPrice',
})
// ===== 池子数据 =====
const { data: aumInUsdy, refetch: refetchAumInUsdy } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'getAumInUsdy',
args: [true],
})
const { data: cooldownDuration } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'cooldownDuration',
})
const { data: lastAddedAt } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'lastAddedAt',
args: address ? [address] : undefined,
})
const { data: accountValue, refetch: refetchAccountValue } = useReadContract({
address: CONTRACTS.YT_REWARD_ROUTER,
abi: YT_REWARD_ROUTER_ABI,
functionName: 'getAccountValue',
args: address ? [address] : undefined,
})
// ===== 管理状态 =====
const { data: emergencyMode, refetch: refetchEmergencyMode } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'emergencyMode',
})
const { data: swapEnabled, refetch: refetchSwapEnabled } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'isSwapEnabled',
})
const { data: poolPaused } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'paused',
})
// ===== 手续费配置 =====
const { data: swapFee } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'swapFee',
})
const { data: stableSwapFee } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'stableSwapFee',
})
const { data: taxBasisPoints } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'taxBasisPoints',
})
const { data: stableTaxBasisPoints } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'stableTaxBasisPoints',
})
const { data: dynamicFees } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'dynamicFees',
})
// ===== 管理员地址 =====
const { data: vaultGov } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'gov',
})
const { data: poolManagerGov } = useReadContract({
address: CONTRACTS.YT_POOL_MANAGER,
abi: YT_POOL_MANAGER_ABI,
functionName: 'gov',
})
const { data: priceFeedGov } = useReadContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'gov',
})
const { data: usdcPriceSource } = useReadContract({
address: CONTRACTS.YT_PRICE_FEED,
abi: YT_PRICE_FEED_ABI,
functionName: 'usdcPriceSource',
})
// ===== USDY 信息 =====
const { data: usdyTotalSupply, refetch: refetchUsdySupply } = useReadContract({
address: CONTRACTS.USDY,
abi: USDY_ABI,
functionName: 'totalSupply',
})
const { data: poolValue, refetch: refetchPoolValue } = useReadContract({
address: CONTRACTS.YT_VAULT,
abi: YT_VAULT_ABI,
functionName: 'getPoolValue',
})
// 辅助函数:根据地址获取代币符号
const getTokenSymbol = (tokenAddress: string): string => {
const token = poolTokens.find(t => t.address.toLowerCase() === tokenAddress.toLowerCase())
return token?.symbol || 'Token'
}
return {
// 连接状态
address,
isConnected,
// 代币列表
poolTokens,
allAvailableTokens,
poolTokenAddresses,
// ytLP 数据
ytLPBalance,
ytLPTotalSupply,
ytLPDecimals,
ytLPName,
ytLPSymbol,
ytLPPrice,
// 池子数据
aumInUsdy,
cooldownDuration,
lastAddedAt,
accountValue,
poolValue,
usdyTotalSupply,
// 管理状态
emergencyMode,
swapEnabled,
poolPaused,
// 手续费
swapFee,
stableSwapFee,
taxBasisPoints,
stableTaxBasisPoints,
dynamicFees,
// 管理员地址
vaultGov,
poolManagerGov,
priceFeedGov,
usdcPriceSource,
// 辅助函数
getTokenSymbol,
// Refetch 函数
refetchBalance,
refetchTokenBalances,
refetchYtLPTotalSupply,
refetchYtLPPrice,
refetchAumInUsdy,
refetchAccountValue,
refetchUsdyAmounts,
refetchTokenWeights,
refetchEmergencyMode,
refetchSwapEnabled,
refetchUsdySupply,
refetchPoolValue,
refetchVaultWhitelist,
}
}

View File

@@ -0,0 +1,208 @@
/**
* LP 交易处理 Hook
* 集中管理所有交易逻辑
*/
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { useTransactions } from '../../context/TransactionContext'
import type { TransactionType } from '../../context/TransactionContext'
import { useToast } from '../Toast'
interface TransactionCallbacks {
onSuccess?: () => void
onError?: (error: string) => void
}
export function useLPTransactions(callbacks?: TransactionCallbacks) {
const { t } = useTranslation()
const { addTransaction, updateTransaction } = useTransactions()
const { showToast } = useToast()
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
// 使用 ref 存储 callbacks避免作为依赖项
const callbacksRef = useRef(callbacks)
callbacksRef.current = callbacks
const { writeContract, data: hash, isPending, error: writeError, reset, status: writeStatus } = useWriteContract()
const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({
hash,
})
// 使用 ref 追踪已处理的状态,防止重复触发
const processedHashRef = useRef<string | null>(null)
const successProcessedRef = useRef(false)
const errorProcessedRef = useRef(false)
const writeErrorProcessedRef = useRef<Error | null>(null)
// 超时自动重置机制
useEffect(() => {
let timeoutId: NodeJS.Timeout | null = null
if (isPending && !hash) {
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, t, showToast, reset, updateTransaction])
// 处理交易提交
useEffect(() => {
if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) {
// 防止重复处理
if (processedHashRef.current === hash) return
processedHashRef.current = hash
updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' })
showToast('info', t('toast.txSubmitted'))
}
}, [hash, t, showToast, updateTransaction])
// 处理交易成功
useEffect(() => {
if (isSuccess && !successProcessedRef.current) {
successProcessedRef.current = true
if (pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { status: 'success' })
showToast('success', t('toast.txSuccess'))
pendingTxRef.current = null
}
callbacksRef.current?.onSuccess?.()
reset()
// 重置标记
processedHashRef.current = null
successProcessedRef.current = false
}
}, [isSuccess, t, showToast, updateTransaction, reset])
// 处理交易失败
useEffect(() => {
if (isError && pendingTxRef.current && !errorProcessedRef.current) {
errorProcessedRef.current = true
const errorMsg = typeof txError?.message === 'string' ? txError.message : 'Transaction failed'
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
showToast('error', t('toast.txFailed'))
pendingTxRef.current = null
callbacksRef.current?.onError?.(errorMsg)
reset()
processedHashRef.current = null
// 延迟重置
setTimeout(() => { errorProcessedRef.current = false }, 100)
}
}, [isError, txError, t, showToast, updateTransaction, reset])
// 处理写入错误
useEffect(() => {
if (writeError && writeErrorProcessedRef.current !== writeError) {
writeErrorProcessedRef.current = writeError
const errorMsg = parseError(writeError, t)
showToast('error', errorMsg)
if (pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
pendingTxRef.current = null
}
callbacksRef.current?.onError?.(errorMsg)
reset()
processedHashRef.current = null
}
}, [writeError, t, showToast, updateTransaction, reset])
// 错误解析
const parseError = (error: any, t: any): string => {
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') || msg.includes('less than block base fee')) {
return t('toast.insufficientBalance') + ' (Gas)'
}
if (msg.includes('CooldownNotPassed')) {
return t('lp.cooldownNotPassed')
}
if (msg.includes('InsufficientOutput')) {
return t('lp.insufficientOutput')
}
const match = msg.match(/error[:\s]+(\w+)/i)
if (match) return match[1]
return msg.slice(0, 100)
}
// 记录交易
const recordTx = (type: TransactionType, amount?: string, token?: string) => {
const id = addTransaction({
type,
hash: '',
status: 'pending',
amount,
token,
})
pendingTxRef.current = { id, type, amount }
}
// 计算按钮是否应该禁用
const isProcessing = (isPending || isConfirming) && writeStatus !== 'error'
return {
writeContract,
recordTx,
isProcessing,
isPending,
isConfirming,
hash,
reset,
}
}
export { parseError }
function parseError(error: any, t: any): string {
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') || msg.includes('less than block base fee')) {
return t('toast.insufficientBalance') + ' (Gas)'
}
if (msg.includes('CooldownNotPassed')) {
return t('lp.cooldownNotPassed')
}
if (msg.includes('InsufficientOutput')) {
return t('lp.insufficientOutput')
}
const match = msg.match(/error[:\s]+(\w+)/i)
if (match) return match[1]
return msg.slice(0, 100)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,313 @@
.lending-panel {
width: 100%;
}
.lending-header {
margin-bottom: 24px;
}
.lending-header h2 {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
color: var(--color-text);
}
.contract-info {
font-size: 14px;
color: var(--color-text-secondary);
}
.contract-info .label {
font-weight: 500;
margin-right: 8px;
}
.contract-info .value {
font-family: monospace;
color: var(--color-primary);
}
/* 账户数据 */
.account-data {
background: var(--color-bg-card);
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.account-data h3 {
margin: 0 0 20px 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text);
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.data-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
background: var(--color-bg-section);
border-radius: 6px;
border: 1px solid var(--color-border);
}
.data-item .label {
font-size: 14px;
color: var(--color-text-secondary);
font-weight: 500;
}
.data-item .value {
font-size: 24px;
font-weight: 600;
color: var(--color-text);
}
.data-item .value.warning {
color: var(--color-error);
}
/* 系统信息 */
.system-info {
background: var(--color-bg-card);
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.system-info h3 {
margin: 0 0 20px 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text);
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.info-item {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px;
background: var(--color-bg-section);
border-radius: 6px;
border: 1px solid var(--color-border);
}
.info-item .label {
font-size: 13px;
color: var(--color-text-secondary);
}
.info-item .value {
font-size: 18px;
font-weight: 600;
color: var(--color-text);
}
/* 操作区域 */
.operations {
background: var(--color-bg-card);
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.tabs {
display: flex;
gap: 4px;
margin-bottom: 24px;
border-bottom: 1px solid var(--color-border);
}
.tab {
padding: 12px 24px;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--color-text-secondary);
transition: all 0.2s;
}
.tab:hover {
color: var(--color-text);
}
.tab.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
}
.tab-content {
min-height: 300px;
}
.operation-form {
max-width: 500px;
margin: 0 auto;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--color-text);
font-size: 14px;
}
.select-input,
.amount-input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 14px;
transition: border-color 0.2s;
}
.select-input:focus,
.amount-input:focus {
outline: none;
border-color: var(--color-primary);
}
.info-box {
background: var(--color-primary-light);
border: 1px solid var(--color-primary);
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 20px;
}
.info-box p {
margin: 0;
padding: 4px 0;
color: var(--color-text);
font-size: 14px;
}
.action-button {
width: 100%;
padding: 12px;
background: var(--color-primary);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.action-button:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 抵押品明细 */
.collateral-details {
background: var(--color-bg-card);
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.collateral-details h3 {
margin: 0 0 20px 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text);
}
.details-table {
overflow-x: auto;
}
.details-table table {
width: 100%;
border-collapse: collapse;
}
.details-table th {
text-align: left;
padding: 12px 16px;
background: var(--color-bg-section);
font-weight: 500;
color: var(--color-text-secondary);
font-size: 14px;
border-bottom: 1px solid var(--color-border);
}
.details-table td {
padding: 16px;
border-bottom: 1px solid var(--color-border-light);
color: var(--color-text);
font-size: 14px;
}
.details-table tbody tr:hover {
background: var(--color-bg-section);
}
/* 连接提示 */
.connect-prompt {
text-align: center;
padding: 80px 20px;
background: var(--color-bg-card);
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.connect-prompt p {
font-size: 16px;
color: var(--color-text-secondary);
margin: 0;
}
/* 响应式 */
@media (max-width: 768px) {
.lending-panel {
padding: 16px;
}
.data-grid,
.info-grid {
grid-template-columns: 1fr;
}
.tabs {
flex-wrap: wrap;
}
.tab {
flex: 1 1 auto;
min-width: 120px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, createContext, useContext } from 'react'
import { useState, useEffect, createContext, useContext, useCallback } from 'react'
import type { ReactNode } from 'react'
export type ToastType = 'success' | 'error' | 'info' | 'warning'
@@ -19,14 +19,14 @@ const ToastContext = createContext<ToastContextType | null>(null)
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([])
const showToast = (type: ToastType, message: string, duration = 4000) => {
const showToast = useCallback((type: ToastType, message: string, duration = 4000) => {
const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
setToasts(prev => [...prev, { id, type, message, duration }])
}
}, [])
const removeToast = (id: string) => {
const removeToast = useCallback((id: string) => {
setToasts(prev => prev.filter(t => t.id !== id))
}
}, [])
return (
<ToastContext.Provider value={{ showToast }}>

View File

@@ -23,8 +23,8 @@ export function TransactionHistory({ transactions, onClear }: Props) {
const getTypeLabel = (type: string) => {
const labels: Record<string, string> = {
mint: 'Mint WUSD',
burn: 'Burn WUSD',
mint: 'Mint USDC',
burn: 'Burn USDC',
buy: 'Buy YT',
sell: 'Sell YT',
approve: 'Approve',

View File

@@ -0,0 +1,297 @@
import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, useChainId } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
import { GAS_CONFIG, USDC_ABI, getContracts, getDecimals, getChainName } from '../config/contracts'
import { useTransactions } from '../context/TransactionContext'
import type { TransactionType } from '../context/TransactionContext'
import { useToast } from './Toast'
import { TransactionHistory } from './TransactionHistory'
// USDC 精度备用值 - 使用统一常量
export function USDCPanel() {
const { t } = useTranslation()
const { address, isConnected } = useAccount()
// ===== 多链支持 =====
const chainId = useChainId()
const CONTRACTS = getContracts(chainId)
const TOKEN_DECIMALS = getDecimals(chainId)
const currentChainName = getChainName(chainId)
const { transactions, addTransaction, updateTransaction, clearHistory } = useTransactions()
const { showToast } = useToast()
const [transferTo, setTransferTo] = useState('')
const [transferAmount, setTransferAmount] = useState('')
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
const { data: balance, refetch: refetchBalance } = useReadContract({
address: CONTRACTS.USDC,
abi: USDC_ABI,
functionName: 'balanceOf',
args: address ? [address] : undefined,
})
const { data: symbol } = useReadContract({
address: CONTRACTS.USDC,
abi: USDC_ABI,
functionName: 'symbol',
})
const { data: decimals } = useReadContract({
address: CONTRACTS.USDC,
abi: USDC_ABI,
functionName: 'decimals',
})
const { data: totalSupply, refetch: refetchTotalSupply } = useReadContract({
address: CONTRACTS.USDC,
abi: USDC_ABI,
functionName: 'totalSupply',
})
const { data: name } = useReadContract({
address: CONTRACTS.USDC,
abi: USDC_ABI,
functionName: 'name',
})
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(() => {
// 验证 hash 是有效的交易哈希字符串
if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' })
showToast('info', t('toast.txSubmitted'))
}
}, [hash])
// 处理交易成功
useEffect(() => {
if (isSuccess) {
if (pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { status: 'success' })
showToast('success', t('toast.txSuccess'))
pendingTxRef.current = null
}
refetchBalance()
refetchTotalSupply()
setTransferTo('')
setTransferAmount('')
}
}, [isSuccess])
// 处理交易失败
useEffect(() => {
if (isError && pendingTxRef.current) {
const errMsg = typeof txError?.message === 'string' ? txError.message : 'Transaction failed'
updateTransaction(pendingTxRef.current.id, {
status: 'failed',
error: errMsg
})
showToast('error', t('toast.txFailed'))
pendingTxRef.current = null
// 重置状态,允许用户重新操作
reset()
}
}, [isError])
// 处理写入错误
useEffect(() => {
if (writeError) {
const errorMsg = parseError(writeError)
showToast('error', errorMsg)
if (pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
pendingTxRef.current = null
}
// 重置状态,允许用户重新操作
reset()
}
}, [writeError])
const parseError = (error: any): string => {
// 确保获取字符串形式的错误信息
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') || 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]
return msg.slice(0, 100)
}
const recordTx = (type: TransactionType, amount?: string, token?: string) => {
const id = addTransaction({
type,
hash: '',
status: 'pending',
amount,
token,
})
pendingTxRef.current = { id, type, amount }
}
// 计算按钮是否应该禁用 - 排除错误状态
const isProcessing = (isPending || isConfirming) && writeStatus !== 'error'
const handleTransfer = async () => {
const tokenDecimals = decimals ?? TOKEN_DECIMALS.USDC
if (!address || !transferTo || !transferAmount) return
// 验证地址格式
if (!transferTo.startsWith('0x') || transferTo.length !== 42) {
showToast('error', t('usdc.invalidAddress'))
return
}
recordTx('transfer', transferAmount, 'USDC')
writeContract({
address: CONTRACTS.USDC,
abi: USDC_ABI,
functionName: 'transfer',
args: [transferTo as `0x${string}`, parseUnits(transferAmount, tokenDecimals)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
if (!isConnected) {
return (
<div className="panel">
<h2>USDC</h2>
<p className="text-muted">{t('common.connectFirst')}</p>
</div>
)
}
const tokenDecimals = decimals ?? TOKEN_DECIMALS.USDC
return (
<div className="panel">
<h2>{t('usdc.title')} <span style={{ fontSize: '12px', color: '#666', fontWeight: 'normal' }}>({currentChainName})</span></h2>
{/* 代币信息卡片 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '12px', marginBottom: '16px' }}>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="代币名称 Token Name - USDC 稳定币的完整名称"></div>
<strong style={{ fontSize: '14px' }}>{name || 'USD Coin'}</strong>
</div>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="代币符号 Token Symbol - USDC 稳定币的交易符号"></div>
<strong style={{ fontSize: '14px' }}>{symbol || 'USDC'}</strong>
</div>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title={`精度 Decimals - 当前链 ${currentChainName} 的 USDC 使用 ${tokenDecimals} 位小数精度`}></div>
<strong style={{ fontSize: '14px' }}>{tokenDecimals.toString()}</strong>
</div>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px', gridColumn: 'span 2' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="合约地址 Contract Address - USDC 稳定币的 ERC20 合约地址">{t('common.contract')}</div>
<code style={{ fontSize: '11px', wordBreak: 'break-all' }}>{CONTRACTS.USDC}</code>
</div>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="总供应量 Total Supply - USDC 代币的全网发行总量">{t('usdc.totalSupply')}</div>
<strong style={{ fontSize: '14px' }}>
{totalSupply !== undefined
? Number(formatUnits(totalSupply, tokenDecimals)).toLocaleString()
: '0'}
</strong>
</div>
</div>
{/* 用户余额 */}
<div style={{ padding: '16px', background: 'linear-gradient(135deg, #2775ca 0%, #1e5aa8 100%)', borderRadius: '12px', marginBottom: '16px', color: 'white' }}>
<div style={{ fontSize: '12px', opacity: 0.9, marginBottom: '4px' }}>{t('common.balance')}</div>
<strong style={{ fontSize: '24px' }}>
{balance !== undefined
? Number(formatUnits(balance, tokenDecimals)).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })
: '0.00'} {symbol || 'USDC'}
</strong>
</div>
{/* 提示信息 */}
<div style={{ padding: '12px', background: '#f5f5f5', borderRadius: '8px', marginBottom: '16px', fontSize: '13px', color: '#666' }}>
<strong>:</strong> USDC Circle
</div>
{/* 转账功能 */}
<div style={{ padding: '16px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', marginBottom: '12px' }}>
<h4 style={{ margin: '0 0 12px 0', fontSize: '14px', color: '#333', cursor: 'help' }} title="转账 Transfer - 将 USDC 转移到其他钱包地址">{t('usdc.transfer')}</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 120px', gap: '12px', alignItems: 'end' }}>
<div>
<div className="form-group" style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '12px', color: '#666' }}>{t('usdc.toAddress')}</label>
<input
type="text"
value={transferTo}
onChange={(e) => setTransferTo(e.target.value)}
placeholder="0x..."
className="input"
style={{ fontSize: '12px' }}
/>
</div>
<div className="form-group" style={{ marginBottom: '0' }}>
<label style={{ fontSize: '12px', color: '#666' }}>{t('usdc.transferAmount')}</label>
<input
type="number"
value={transferAmount}
onChange={(e) => setTransferAmount(e.target.value)}
placeholder={t('usdc.enterAmount')}
className="input"
/>
</div>
</div>
<button
onClick={handleTransfer}
disabled={isProcessing || !transferTo || !transferAmount}
className="btn btn-primary"
style={{ height: '40px' }}
>
{isProcessing ? '...' : t('usdc.transfer')}
</button>
</div>
</div>
{/* 交易历史 */}
<TransactionHistory transactions={transactions} onClear={clearHistory} />
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,465 +0,0 @@
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, GAS_CONFIG, WUSD_ABI } from '../config/contracts'
import { useTransactions } from '../context/TransactionContext'
import type { TransactionType } from '../context/TransactionContext'
import { useToast } from './Toast'
import { TransactionHistory } from './TransactionHistory'
export function WUSDPanel() {
const { t } = useTranslation()
const { address, isConnected } = useAccount()
const { transactions, addTransaction, updateTransaction, clearHistory } = useTransactions()
const { showToast } = useToast()
const [mintAmount, setMintAmount] = useState('')
const [burnAmount, setBurnAmount] = useState('')
const [transferTo, setTransferTo] = useState('')
const [transferAmount, setTransferAmount] = useState('')
const [showBoundaryTest, setShowBoundaryTest] = useState(false)
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
const { data: balance, refetch: refetchBalance } = useReadContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'balanceOf',
args: address ? [address] : undefined,
})
const { data: symbol } = useReadContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'symbol',
})
const { data: decimals } = useReadContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'decimals',
})
const { data: totalSupply, refetch: refetchTotalSupply } = useReadContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'totalSupply',
})
const { data: owner } = useReadContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'owner',
})
const { data: name } = useReadContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'name',
})
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(() => {
// 验证 hash 是有效的交易哈希字符串
if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' })
showToast('info', t('toast.txSubmitted'))
}
}, [hash])
// 处理交易成功
useEffect(() => {
if (isSuccess) {
if (pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { status: 'success' })
showToast('success', t('toast.txSuccess'))
pendingTxRef.current = null
}
refetchBalance()
refetchTotalSupply()
setMintAmount('')
setBurnAmount('')
setTransferTo('')
setTransferAmount('')
}
}, [isSuccess])
// 处理交易失败
useEffect(() => {
if (isError && pendingTxRef.current) {
const errMsg = typeof txError?.message === 'string' ? txError.message : 'Transaction failed'
updateTransaction(pendingTxRef.current.id, {
status: 'failed',
error: errMsg
})
showToast('error', t('toast.txFailed'))
pendingTxRef.current = null
// 重置状态,允许用户重新操作
reset()
}
}, [isError])
// 处理写入错误
useEffect(() => {
if (writeError) {
const errorMsg = parseError(writeError)
showToast('error', errorMsg)
if (pendingTxRef.current) {
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
pendingTxRef.current = null
}
// 重置状态,允许用户重新操作
reset()
}
}, [writeError])
const parseError = (error: any): string => {
// 确保获取字符串形式的错误信息
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') || 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]
return msg.slice(0, 100)
}
const recordTx = (type: TransactionType, amount?: string, token?: string) => {
const id = addTransaction({
type,
hash: '',
status: 'pending',
amount,
token,
})
pendingTxRef.current = { id, type, amount }
}
// 计算按钮是否应该禁用 - 排除错误状态
const isProcessing = (isPending || isConfirming) && writeStatus !== 'error'
const handleMint = async () => {
if (!address || !mintAmount || !decimals) return
recordTx('mint', mintAmount, 'WUSD')
writeContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'mint',
args: [address, parseUnits(mintAmount, decimals)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleBurn = async () => {
if (!address || !burnAmount || !decimals) return
recordTx('burn', burnAmount, 'WUSD')
writeContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'burn',
args: [address, parseUnits(burnAmount, decimals)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleTransfer = async () => {
if (!address || !transferTo || !transferAmount || !decimals) return
// 验证地址格式
if (!transferTo.startsWith('0x') || transferTo.length !== 42) {
showToast('error', t('wusd.invalidAddress'))
return
}
recordTx('transfer', transferAmount, 'WUSD')
writeContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'transfer',
args: [transferTo as `0x${string}`, parseUnits(transferAmount, decimals)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 边界测试函数
const runBoundaryTest = (testType: string) => {
if (!address || !decimals) return
recordTx('test', undefined, 'WUSD')
switch (testType) {
case 'mint_zero':
writeContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'mint',
args: [address, BigInt(0)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
case 'burn_exceed':
const exceedAmount = balance ? balance + parseUnits('999999', decimals) : parseUnits('999999999', decimals)
writeContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'burn',
args: [address, exceedAmount],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
case 'mint_10000':
pendingTxRef.current!.amount = '10000'
writeContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'mint',
args: [address, parseUnits('10000', decimals)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
break
}
}
if (!isConnected) {
return (
<div className="panel">
<h2>WUSD</h2>
<p className="text-muted">{t('common.connectFirst')}</p>
</div>
)
}
return (
<div className="panel">
<h2>{t('wusd.title')}</h2>
{/* 代币信息卡片 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '12px', marginBottom: '16px' }}>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="代币名称 Token Name - WUSD 稳定币的完整名称"></div>
<strong style={{ fontSize: '14px' }}>{name || 'WUSD'}</strong>
</div>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="代币符号 Token Symbol - WUSD 稳定币的交易符号"></div>
<strong style={{ fontSize: '14px' }}>{symbol || 'WUSD'}</strong>
</div>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="精度 Decimals - 代币小数位数"></div>
<strong style={{ fontSize: '14px' }}>{decimals?.toString() || '18'}</strong>
</div>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="合约地址 Contract Address - WUSD 稳定币的 ERC20 合约地址">{t('common.contract')}</div>
<code style={{ fontSize: '11px', wordBreak: 'break-all' }}>{CONTRACTS.WUSD}</code>
</div>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="所有者 Owner - WUSD 合约的所有者地址,拥有铸造和销毁权限">{t('wusd.owner')}</div>
<code style={{ fontSize: '11px', wordBreak: 'break-all' }}>{owner ? `${(owner as string).slice(0, 10)}...${(owner as string).slice(-8)}` : '-'}</code>
</div>
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="总供应量 Total Supply - WUSD 代币的全网发行总量">{t('wusd.totalSupply')}</div>
<strong style={{ fontSize: '14px' }}>
{totalSupply !== undefined && decimals !== undefined
? Number(formatUnits(totalSupply, decimals)).toLocaleString()
: '0'}
</strong>
</div>
</div>
{/* 用户余额 */}
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px', marginBottom: '16px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="余额 Balance - 你钱包中持有的 WUSD 稳定币数量">{t('common.balance')}</div>
<strong style={{ fontSize: '18px' }}>
{balance !== undefined && decimals !== undefined
? Number(formatUnits(balance, decimals)).toLocaleString()
: '0'} {symbol || 'WUSD'}
</strong>
</div>
{/* 铸造和销毁 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div style={{ padding: '12px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
<div className="form-group" style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '13px', fontWeight: 500, cursor: 'help' }} title="铸造数量 Mint Amount - 输入要铸造的 WUSD 数量(仅 Owner 可用)">{t('wusd.mintAmount')}</label>
<input
type="number"
value={mintAmount}
onChange={(e) => setMintAmount(e.target.value)}
placeholder={t('wusd.enterAmount')}
className="input"
/>
</div>
<button
onClick={handleMint}
disabled={isProcessing || !mintAmount}
className="btn btn-primary"
style={{ width: '100%' }}
>
{isProcessing ? t('common.processing') : t('wusd.mint')}
</button>
</div>
<div style={{ padding: '12px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
<div className="form-group" style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '13px', fontWeight: 500, cursor: 'help' }} title="销毁数量 Burn Amount - 输入要销毁的 WUSD 数量(仅 Owner 可用)">{t('wusd.burnAmount')}</label>
<input
type="number"
value={burnAmount}
onChange={(e) => setBurnAmount(e.target.value)}
placeholder={t('wusd.enterAmount')}
className="input"
/>
</div>
<button
onClick={handleBurn}
disabled={isProcessing || !burnAmount}
className="btn btn-secondary"
style={{ width: '100%' }}
>
{isProcessing ? t('common.processing') : t('wusd.burn')}
</button>
</div>
</div>
{/* 转账功能 */}
<div style={{ padding: '12px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', marginBottom: '12px' }}>
<h4 style={{ margin: '0 0 12px 0', fontSize: '14px', color: '#333', cursor: 'help' }} title="转账 Transfer - 将 WUSD 转移到其他钱包地址">{t('wusd.transfer')}</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 120px', gap: '12px', alignItems: 'end' }}>
<div>
<div className="form-group" style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '12px', color: '#666' }}>{t('wusd.toAddress')}</label>
<input
type="text"
value={transferTo}
onChange={(e) => setTransferTo(e.target.value)}
placeholder="0x..."
className="input"
style={{ fontSize: '12px' }}
/>
</div>
<div className="form-group" style={{ marginBottom: '0' }}>
<label style={{ fontSize: '12px', color: '#666' }}>{t('wusd.transferAmount')}</label>
<input
type="number"
value={transferAmount}
onChange={(e) => setTransferAmount(e.target.value)}
placeholder={t('wusd.enterAmount')}
className="input"
/>
</div>
</div>
<button
onClick={handleTransfer}
disabled={isProcessing || !transferTo || !transferAmount}
className="btn btn-primary"
style={{ height: '40px' }}
>
{isProcessing ? '...' : t('wusd.transfer')}
</button>
</div>
</div>
{/* 边界测试区域 */}
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowBoundaryTest(!showBoundaryTest)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }} title="边界测试 Boundary Tests - 测试 WUSD 合约的边界条件和错误处理">{t('test.boundaryTests')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showBoundaryTest ? '▼' : '▶'}</span>
</div>
{showBoundaryTest && (
<div style={{ marginTop: '10px' }}>
<div className="test-hint">{t('test.boundaryHint')}</div>
<div className="test-grid">
<div className="test-card">
<div className="test-card-left">
<span className="test-name">{t('test.mintZero')}</span>
<span className="test-error">MaySucceed</span>
<p className="test-desc">{t('test.mintZeroDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('mint_zero')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
</div>
<div className="test-card">
<div className="test-card-left">
<span className="test-name">{t('test.burnExceed')}</span>
<span className="test-error">InsufficientBalance</span>
<p className="test-desc">{t('test.burnExceedDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('burn_exceed')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
</div>
<div className="test-card">
<div className="test-card-left">
<span className="test-name">{t('test.mint10000')}</span>
<span className="test-error">Mint</span>
<p className="test-desc">{t('test.mint10000Desc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('mint_10000')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
</div>
</div>
</div>
)}
</div>
{/* 交易历史 */}
<TransactionHistory transactions={transactions} onClear={clearHistory} />
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
import { createWeb3Modal } from '@web3modal/wagmi/react'
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'
import { arbitrumSepolia } from 'wagmi/chains'
import { http, createConfig, createStorage } from 'wagmi'
import { arbitrumSepolia, bscTestnet } from 'wagmi/chains'
import { QueryClient } from '@tanstack/react-query'
import { injected, coinbaseWallet } from 'wagmi/connectors'
export const queryClient = new QueryClient()
@@ -15,27 +16,44 @@ const metadata = {
icons: ['https://avatars.githubusercontent.com/u/37784886']
}
const chains = [arbitrumSepolia] as const
// 检查是否应该跳过自动重连
const shouldSkipReconnect = () => {
if (typeof window === 'undefined') return false
const skip = sessionStorage.getItem('skipReconnect') === 'true'
if (skip) {
sessionStorage.removeItem('skipReconnect')
}
return skip
}
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,
// 自定义 storage在需要时禁用重连
const customStorage = createStorage({
storage: {
getItem: (key: string) => {
// 如果设置了跳过重连,返回 null 来阻止重连
if (shouldSkipReconnect() && key === 'wagmi.recentConnectorId') {
return null
}
return localStorage.getItem(key)
},
setItem: (key: string, value: string) => localStorage.setItem(key, value),
removeItem: (key: string) => localStorage.removeItem(key),
},
})
export const config = createConfig({
chains: [arbitrumSepolia, bscTestnet], // 支持多链
connectors: [
injected(),
coinbaseWallet({ appName: metadata.name }),
],
transports: {
[arbitrumSepolia.id]: http(), // ARB Sepolia (421614)
[bscTestnet.id]: http(), // BNB Testnet (97)
},
storage: customStorage,
})
createWeb3Modal({
wagmiConfig: config,
projectId,

View File

@@ -16,13 +16,17 @@
"contract": "Contract",
"address": "Address",
"network": "Arbitrum Sepolia",
"connectFirst": "Please connect wallet first"
"connectFirst": "Please connect wallet first",
"set": "Set"
},
"nav": {
"wusd": "WUSD",
"usdc": "USDC",
"vaultTrading": "Vault Trading",
"factory": "Factory",
"lpPool": "LP Pool"
"lpPool": "LP Pool",
"holders": "Holders",
"lending": "Lending",
"autoTest": "Auto Test"
},
"header": {
"title": "YT Asset Test"
@@ -30,8 +34,8 @@
"footer": {
"description": "YT Asset Contract Testing Interface"
},
"wusd": {
"title": "WUSD Token",
"usdc": {
"title": "USDC Token",
"mintAmount": "Mint Amount",
"burnAmount": "Burn Amount",
"enterAmount": "Enter amount",
@@ -55,18 +59,18 @@
"idleAssets": "Idle Assets",
"totalSupply": "Total Supply",
"hardCap": "Hard Cap",
"wusdPrice": "WUSD Price",
"usdcPrice": "USDC Price",
"ytPrice": "YT Price",
"yourWusdBalance": "Your WUSD Balance",
"yourUsdcBalance": "Your USDC Balance",
"yourYtBalance": "Your YT Balance",
"buyYt": "Buy YT",
"sellYt": "Sell YT",
"wusdAmount": "WUSD Amount",
"usdcAmount": "USDC Amount",
"ytAmount": "YT Amount",
"enterWusdAmount": "Enter WUSD amount",
"enterUsdcAmount": "Enter USDC amount",
"enterYtAmount": "Enter YT amount",
"youWillReceive": "You will receive",
"approveWusd": "Approve WUSD",
"approveUsdc": "Approve USDC",
"buy": "Buy YT",
"sell": "Sell YT",
"redeemStatus": "Redeem Status",
@@ -96,12 +100,12 @@
"needApprove": "Need Approve",
"vaultAddress": "Vault Address",
"factoryAddress": "Factory Address",
"wusdContract": "WUSD Contract",
"usdcContract": "USDC Contract",
"pricePrecision": "Price Precision",
"transferYt": "Transfer YT",
"transfer": "Transfer",
"invalidAddress": "Invalid address format",
"queueWithdrawDesc": "Selling YT creates a withdrawal request and joins the queue. WUSD will be sent after admin processes the request.",
"queueWithdrawDesc": "Selling YT creates a withdrawal request and joins the queue. USDC will be sent after admin processes the request.",
"queueTotal": "Total Requests",
"queuePending": "Pending",
"queueProcessed": "Processed",
@@ -114,7 +118,7 @@
"processBatchWithdrawals": "Batch Process Withdrawals",
"batchSize": "Batch Size",
"processBatch": "Process Batch",
"processBatchHint": "Process withdrawal requests from queue in order, sending WUSD to users"
"processBatchHint": "Process withdrawal requests from queue in order, sending USDC to users"
},
"factory": {
"title": "Factory Management",
@@ -133,13 +137,13 @@
"symbol": "Symbol",
"managerAddress": "Manager Address",
"redemptionTime": "Redemption Time",
"initialWusdPrice": "Initial WUSD Price",
"initialUsdcPrice": "Initial USDC Price",
"initialYtPrice": "Initial YT Price",
"create": "Create Vault",
"updatePrices": "Update Vault Prices",
"vaultAddress": "Vault Address",
"selectVault": "Select Vault",
"newWusdPrice": "New WUSD Price",
"newUsdcPrice": "New USDC Price",
"newYtPrice": "New YT Price",
"update": "Update Prices",
"ownerConfig": "Owner Config",
@@ -216,16 +220,16 @@
"quickActions": "Quick Actions",
"run": "Run",
"running": "Running...",
"mint10000": "Mint 10000 WUSD",
"mint10000Desc": "Quick mint 10000 test WUSD",
"maxApprove": "Max Approve WUSD",
"mint10000": "Mint 10000 USDC",
"mint10000Desc": "Quick mint 10000 test USDC",
"maxApprove": "Max Approve USDC",
"maxApproveDesc": "Approve max uint256 amount",
"buyZero": "Buy Amount 0",
"buyZeroDesc": "Test depositYT(0)",
"sellZero": "Sell Amount 0",
"sellZeroDesc": "Test withdrawYT(0)",
"buyExceedBalance": "Buy Exceed Balance",
"buyExceedBalanceDesc": "Buy more than WUSD balance",
"buyExceedBalanceDesc": "Buy more than USDC balance",
"sellExceedBalance": "Sell Exceed Balance",
"sellExceedBalanceDesc": "Sell more than YT balance",
"buyExceedHardcap": "Buy Exceed Hardcap",
@@ -252,11 +256,11 @@
"cooldownRemaining": "Cooldown Remaining",
"noCooldown": "No cooldown",
"addLiquidity": "Add Liquidity",
"addLiquidityDesc": "Deposit YT tokens or WUSD to receive ytLP tokens",
"addLiquidityDesc": "Deposit YT tokens or USDC to receive ytLP tokens",
"removeLiquidity": "Remove Liquidity",
"removeLiquidityDesc": "Burn ytLP to get tokens back",
"swapTokens": "Swap Tokens",
"swapDesc": "Swap between YT tokens and WUSD in the pool",
"swapDesc": "Swap between YT tokens and USDC in the pool",
"selectToken": "Select Token",
"amount": "Amount",
"slippage": "Slippage Tolerance",
@@ -302,6 +306,132 @@
"usdyAmount": "USDY Amount",
"isStableToken": "Stable Token",
"setStableToken": "Set Stable Token",
"stableTokenHint": "Mark token as stable, affects price calculation"
"stableTokenHint": "Mark token as stable, affects price calculation",
"coreConfig": "Core Config",
"cooldownDuration": "Cooldown Duration",
"cooldownSeconds": "seconds",
"swapFees": "Swap Fees",
"setSwapFees": "Set Fees",
"swapFeeLabel": "Swap Fee",
"stableSwapFeeLabel": "Stable Swap Fee",
"taxBasisPointsLabel": "Tax Basis Points",
"stableTaxBasisPointsLabel": "Stable Tax Basis Points",
"withdrawToken": "Emergency Withdraw Token",
"withdrawTokenLabel": "Emergency Withdraw Token",
"withdraw": "Withdraw",
"receiver": "Receiver",
"tradingLimits": "Trading Limits",
"dynamicFees": "Dynamic Fees",
"dynamicFeesLabel": "Dynamic Fees",
"poolStatus": "Pool Status",
"poolStatusLabel": "Pool Status",
"running": "Running",
"pausedStatus": "Paused",
"pausePool": "Pause Pool",
"unpausePool": "Unpause Pool",
"maxSwapAmount": "Max Swap Amount",
"maxSwapAmountLabel": "Max Swap Amount",
"aumAdjustment": "AUM Adjustment",
"addition": "Addition",
"deduction": "Deduction",
"deductionLabel": "Deduction",
"maxSlippage": "Max Slippage",
"maxSlippageLabel": "Max Slippage",
"maxPriceChange": "Max Price Change",
"maxPriceChangeLabel": "Max Price Change",
"permissionManagement": "Permission Management",
"setGov": "Set Gov",
"setGovLabel": "Set Gov",
"setHandler": "Set Handler",
"setHandlerLabel": "Set Handler",
"setKeeper": "Set Keeper",
"setKeeperLabel": "Set Keeper",
"setSwapper": "Set Swapper",
"setSwapperLabel": "Set Swapper",
"setPoolManager": "Set Pool Manager",
"setPoolManagerLabel": "Set Pool Manager",
"setMinterLabel": "Set Minter",
"isActive": "Active",
"adminConfig": "Admin Config",
"debugInfo": "Debug Info",
"sameTokenWarning": "Input and output tokens are the same",
"withdrawWarning": "Warning: This will withdraw tokens from the pool",
"permissionWarning": "Warning: Permission changes are high-risk operations"
},
"holders": {
"title": "Holder Tracking",
"updateNow": "Update Now",
"lastUpdate": "Last Update",
"holders": "Holders",
"holdersList": "Holders List",
"total": "Total",
"noHolders": "No holder data available",
"rank": "Rank",
"address": "Address",
"balance": "Balance",
"holdingTime": "Holding Time",
"lastUpdated": "Last Updated",
"days": "d",
"hours": "h",
"minutes": "m"
},
"lending": {
"title": "USDC Lending System",
"contract": "Lending Contract",
"yourAccount": "Your Account",
"totalCollateral": "Total Collateral Value",
"totalBorrow": "Total Borrowed",
"availableToBorrow": "Available to Borrow",
"healthFactor": "Health Factor",
"systemInfo": "System Information",
"totalLiquidity": "Total Liquidity",
"totalBorrows": "Total Borrows",
"utilizationRate": "Utilization Rate",
"borrowAPY": "Borrow APY",
"depositCollateral": "Deposit Collateral",
"borrow": "Borrow",
"repay": "Repay",
"withdrawCollateral": "Withdraw Collateral",
"selectCollateral": "Select Collateral",
"borrowAmount": "Borrow Amount",
"repayAmount": "Repay Amount",
"yourBorrow": "Your Borrow",
"usdcBalance": "USDC Balance",
"approve": "Approve Collateral",
"approveUSDC": "Approve USDC",
"deposit": "Deposit",
"withdraw": "Withdraw",
"collateralDetails": "Collateral Details",
"asset": "Asset",
"walletBalance": "Wallet Balance",
"depositedAmount": "Deposited Amount",
"admin": {
"show": "Show Admin Config",
"hide": "Hide Admin Config",
"title": "Lending System Admin",
"notOwner": "Admin permission required",
"noPermission": "You are not an admin, view only",
"systemStatus": "System Status",
"systemPaused": "System Paused",
"pause": "Pause System",
"unpause": "Unpause System",
"pauseSent": "Pause transaction submitted",
"unpauseSent": "Unpause transaction submitted",
"collateralConfig": "Collateral Configuration",
"selectAsset": "Select Asset",
"currentConfig": "Current Configuration",
"isActive": "Is Active",
"collateralFactor": "Collateral Factor",
"liquidationThreshold": "Liquidation Threshold",
"liquidationBonus": "Liquidation Bonus",
"loadToForm": "Load to Form",
"setConfig": "Set Configuration",
"activate": "Activate Collateral",
"deactivate": "Deactivate Collateral",
"configSent": "Config transaction submitted",
"activateSent": "Activate transaction submitted",
"deactivateSent": "Deactivate transaction submitted",
"contracts": "Contract Addresses"
}
}
}

View File

@@ -16,13 +16,17 @@
"contract": "合约",
"address": "地址",
"network": "Arbitrum Sepolia",
"connectFirst": "请先连接钱包"
"connectFirst": "请先连接钱包",
"set": "设置"
},
"nav": {
"wusd": "WUSD",
"usdc": "USDC",
"vaultTrading": "金库交易",
"factory": "工厂管理",
"lpPool": "LP 流动池"
"lpPool": "LP 流动池",
"holders": "持有者",
"lending": "借贷",
"autoTest": "自动测试"
},
"header": {
"title": "YT 资产测试"
@@ -30,8 +34,8 @@
"footer": {
"description": "YT 资产合约测试界面"
},
"wusd": {
"title": "WUSD 代币",
"usdc": {
"title": "USDC 代币",
"mintAmount": "铸造数量",
"burnAmount": "销毁数量",
"enterAmount": "输入数量",
@@ -55,18 +59,18 @@
"idleAssets": "闲置资产",
"totalSupply": "总供应量",
"hardCap": "硬顶",
"wusdPrice": "WUSD 价格",
"usdcPrice": "USDC 价格",
"ytPrice": "YT 价格",
"yourWusdBalance": "你的 WUSD 余额",
"yourUsdcBalance": "你的 USDC 余额",
"yourYtBalance": "你的 YT 余额",
"buyYt": "买入 YT",
"sellYt": "卖出 YT",
"wusdAmount": "WUSD 数量",
"usdcAmount": "USDC 数量",
"ytAmount": "YT 数量",
"enterWusdAmount": "输入 WUSD 数量",
"enterUsdcAmount": "输入 USDC 数量",
"enterYtAmount": "输入 YT 数量",
"youWillReceive": "你将收到",
"approveWusd": "授权 WUSD",
"approveUsdc": "授权 USDC",
"buy": "买入 YT",
"sell": "卖出 YT",
"redeemStatus": "赎回状态",
@@ -96,12 +100,12 @@
"needApprove": "需要授权",
"vaultAddress": "金库地址",
"factoryAddress": "工厂地址",
"wusdContract": "WUSD 合约",
"usdcContract": "USDC 合约",
"pricePrecision": "价格精度",
"transferYt": "转账 YT",
"transfer": "转账",
"invalidAddress": "无效的地址格式",
"queueWithdrawDesc": "卖出 YT 将创建退出请求并加入队列,等待管理员处理后发放 WUSD。",
"queueWithdrawDesc": "卖出 YT 将创建退出请求并加入队列,等待管理员处理后发放 USDC。",
"queueTotal": "总请求数",
"queuePending": "待处理",
"queueProcessed": "已处理",
@@ -114,7 +118,7 @@
"processBatchWithdrawals": "批量处理退出",
"batchSize": "批量大小",
"processBatch": "处理退出",
"processBatchHint": "从队列中按顺序处理指定数量的退出请求,向用户发送 WUSD"
"processBatchHint": "从队列中按顺序处理指定数量的退出请求,向用户发送 USDC"
},
"factory": {
"title": "工厂管理",
@@ -133,13 +137,13 @@
"symbol": "符号",
"managerAddress": "管理员地址",
"redemptionTime": "赎回时间",
"initialWusdPrice": "初始 WUSD 价格",
"initialUsdcPrice": "初始 USDC 价格",
"initialYtPrice": "初始 YT 价格",
"create": "创建金库",
"updatePrices": "更新金库价格",
"vaultAddress": "金库地址",
"selectVault": "选择金库",
"newWusdPrice": "新 WUSD 价格",
"newUsdcPrice": "新 USDC 价格",
"newYtPrice": "新 YT 价格",
"update": "更新价格",
"ownerConfig": "Owner 配置",
@@ -216,16 +220,16 @@
"quickActions": "快速操作",
"run": "执行",
"running": "执行中...",
"mint10000": "铸造 10000 WUSD",
"mint10000Desc": "快速铸造10000个测试WUSD",
"maxApprove": "最大授权 WUSD",
"mint10000": "铸造 10000 USDC",
"mint10000Desc": "快速铸造10000个测试USDC",
"maxApprove": "最大授权 USDC",
"maxApproveDesc": "授权最大uint256额度",
"buyZero": "买入金额为0",
"buyZeroDesc": "测试 depositYT(0)",
"sellZero": "卖出金额为0",
"sellZeroDesc": "测试 withdrawYT(0)",
"buyExceedBalance": "买入超过余额",
"buyExceedBalanceDesc": "买入金额超过 WUSD 余额",
"buyExceedBalanceDesc": "买入金额超过 USDC 余额",
"sellExceedBalance": "卖出超过余额",
"sellExceedBalanceDesc": "卖出金额超过 YT 余额",
"buyExceedHardcap": "买入超过硬顶",
@@ -252,11 +256,11 @@
"cooldownRemaining": "冷却时间剩余",
"noCooldown": "无冷却",
"addLiquidity": "添加流动性",
"addLiquidityDesc": "存入 YT 代币或 WUSD 获得 ytLP 凭证",
"addLiquidityDesc": "存入 YT 代币或 USDC 获得 ytLP 凭证",
"removeLiquidity": "移除流动性",
"removeLiquidityDesc": "销毁 ytLP 获取代币",
"swapTokens": "代币互换",
"swapDesc": "在池内交换 YT 代币和 WUSD",
"swapDesc": "在池内交换 YT 代币和 USDC",
"selectToken": "选择代币",
"amount": "数量",
"slippage": "滑点容忍度",
@@ -302,6 +306,132 @@
"usdyAmount": "USDY 数量",
"isStableToken": "稳定币",
"setStableToken": "设置稳定币",
"stableTokenHint": "将代币标记为稳定币,影响价格计算方式"
"stableTokenHint": "将代币标记为稳定币,影响价格计算方式",
"coreConfig": "核心配置",
"cooldownDuration": "冷却时间",
"cooldownSeconds": "秒",
"swapFees": "交换手续费",
"setSwapFees": "设置手续费",
"swapFeeLabel": "Swap手续费",
"stableSwapFeeLabel": "稳定币Swap手续费",
"taxBasisPointsLabel": "税基点",
"stableTaxBasisPointsLabel": "稳定币税基点",
"withdrawToken": "紧急提取代币",
"withdrawTokenLabel": "紧急提取代币",
"withdraw": "提取",
"receiver": "接收者",
"tradingLimits": "交易限制",
"dynamicFees": "动态手续费",
"dynamicFeesLabel": "动态手续费",
"poolStatus": "池子状态",
"poolStatusLabel": "池子状态",
"running": "运行中",
"pausedStatus": "已暂停",
"pausePool": "暂停池子",
"unpausePool": "恢复池子",
"maxSwapAmount": "最大交换金额",
"maxSwapAmountLabel": "最大交换金额",
"aumAdjustment": "AUM 调整",
"addition": "增加",
"deduction": "减少",
"deductionLabel": "减少",
"maxSlippage": "最大滑点",
"maxSlippageLabel": "最大滑点",
"maxPriceChange": "最大价格变化",
"maxPriceChangeLabel": "最大价格变化",
"permissionManagement": "权限管理",
"setGov": "设置 Gov",
"setGovLabel": "设置 Gov",
"setHandler": "设置 Handler",
"setHandlerLabel": "设置 Handler",
"setKeeper": "设置 Keeper",
"setKeeperLabel": "设置 Keeper",
"setSwapper": "设置 Swapper",
"setSwapperLabel": "设置 Swapper",
"setPoolManager": "设置 PoolManager",
"setPoolManagerLabel": "设置 PoolManager",
"setMinterLabel": "设置 Minter",
"isActive": "激活",
"adminConfig": "管理员配置",
"debugInfo": "调试信息",
"sameTokenWarning": "输入和输出代币相同",
"withdrawWarning": "警告: 此操作将从池子中提取代币",
"permissionWarning": "警告: 权限变更是高风险操作,请确保了解影响"
},
"holders": {
"title": "持有者追踪",
"updateNow": "立即更新",
"lastUpdate": "最后更新",
"holders": "持有者",
"holdersList": "持有者列表",
"total": "总计",
"noHolders": "暂无持有者数据",
"rank": "排名",
"address": "地址",
"balance": "余额",
"holdingTime": "持有时长",
"lastUpdated": "最后更新",
"days": "天",
"hours": "小时",
"minutes": "分钟"
},
"lending": {
"title": "USDC 借贷系统",
"contract": "借贷合约",
"yourAccount": "你的账户",
"totalCollateral": "总抵押价值",
"totalBorrow": "总借款",
"availableToBorrow": "可借额度",
"healthFactor": "健康因子",
"systemInfo": "系统信息",
"totalLiquidity": "总流动性",
"totalBorrows": "总借款量",
"utilizationRate": "资金利用率",
"borrowAPY": "借款年化",
"depositCollateral": "存入抵押品",
"borrow": "借出",
"repay": "归还",
"withdrawCollateral": "提取抵押品",
"selectCollateral": "选择抵押品",
"borrowAmount": "借款金额",
"repayAmount": "归还金额",
"yourBorrow": "你的借款",
"usdcBalance": "USDC 余额",
"approve": "授权抵押品",
"approveUSDC": "授权 USDC",
"deposit": "存入",
"withdraw": "提取",
"collateralDetails": "抵押品明细",
"asset": "资产",
"walletBalance": "钱包余额",
"depositedAmount": "已存入数量",
"admin": {
"show": "显示管理员配置",
"hide": "隐藏管理员配置",
"title": "借贷系统管理",
"notOwner": "需要管理员权限",
"noPermission": "你不是管理员,只能查看配置",
"systemStatus": "系统状态",
"systemPaused": "系统暂停",
"pause": "暂停系统",
"unpause": "恢复系统",
"pauseSent": "暂停交易已提交",
"unpauseSent": "恢复交易已提交",
"collateralConfig": "抵押品配置",
"selectAsset": "选择抵押品",
"currentConfig": "当前配置",
"isActive": "是否激活",
"collateralFactor": "抵押率",
"liquidationThreshold": "清算阈值",
"liquidationBonus": "清算奖励",
"loadToForm": "加载到表单",
"setConfig": "设置配置",
"activate": "激活抵押品",
"deactivate": "停用抵押品",
"configSent": "配置交易已提交",
"activateSent": "激活交易已提交",
"deactivateSent": "停用交易已提交",
"contracts": "合约地址"
}
}
}