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:
@@ -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">
|
||||
|
||||
999
frontend/src/components/AutoTestPanel.tsx
Normal file
999
frontend/src/components/AutoTestPanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
248
frontend/src/components/HoldersPanel.css
Normal file
248
frontend/src/components/HoldersPanel.css
Normal 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;
|
||||
}
|
||||
}
|
||||
268
frontend/src/components/HoldersPanel.tsx
Normal file
268
frontend/src/components/HoldersPanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
1017
frontend/src/components/LP/LPAdminConfig.tsx
Normal file
1017
frontend/src/components/LP/LPAdminConfig.tsx
Normal file
File diff suppressed because it is too large
Load Diff
247
frontend/src/components/LP/LPBoundaryTest.tsx
Normal file
247
frontend/src/components/LP/LPBoundaryTest.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
153
frontend/src/components/LP/LPDebugInfo.tsx
Normal file
153
frontend/src/components/LP/LPDebugInfo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
882
frontend/src/components/LP/LPOperations.tsx
Normal file
882
frontend/src/components/LP/LPOperations.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
203
frontend/src/components/LP/LPPanelNew.tsx
Normal file
203
frontend/src/components/LP/LPPanelNew.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
110
frontend/src/components/LP/LPPoolInfo.tsx
Normal file
110
frontend/src/components/LP/LPPoolInfo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
231
frontend/src/components/LP/LPTokenBalances.tsx
Normal file
231
frontend/src/components/LP/LPTokenBalances.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
202
frontend/src/components/LP/LPUsdyPanel.tsx
Normal file
202
frontend/src/components/LP/LPUsdyPanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
15
frontend/src/components/LP/index.ts
Normal file
15
frontend/src/components/LP/index.ts
Normal 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'
|
||||
134
frontend/src/components/LP/types.ts
Normal file
134
frontend/src/components/LP/types.ts
Normal 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
|
||||
525
frontend/src/components/LP/useLPAdminActions.ts
Normal file
525
frontend/src/components/LP/useLPAdminActions.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
449
frontend/src/components/LP/useLPPoolData.ts
Normal file
449
frontend/src/components/LP/useLPPoolData.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
208
frontend/src/components/LP/useLPTransactions.ts
Normal file
208
frontend/src/components/LP/useLPTransactions.ts
Normal 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
1208
frontend/src/components/LendingAdminPanel.tsx
Normal file
1208
frontend/src/components/LendingAdminPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
313
frontend/src/components/LendingPanel.css
Normal file
313
frontend/src/components/LendingPanel.css
Normal 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;
|
||||
}
|
||||
}
|
||||
2248
frontend/src/components/LendingPanel.tsx
Normal file
2248
frontend/src/components/LendingPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 }}>
|
||||
|
||||
@@ -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',
|
||||
|
||||
297
frontend/src/components/USDCPanel.tsx
Normal file
297
frontend/src/components/USDCPanel.tsx
Normal 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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "合约地址"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user