diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..52f4da2 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,9 @@ dist-ssr *.njsproj *.sln *.sw? + +# Documentation +*.md + +# Test scripts +*.mjs diff --git a/frontend/src/App.css b/frontend/src/App.css index 7635f28..0cd8e39 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -320,7 +320,7 @@ body { /* Balance Info */ .balance-info { - background: #e3f2fd; + background: #f8f9fa; padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; @@ -907,11 +907,11 @@ body { /* LP Panel Styles */ .pool-info { - background: #f0f7ff; + background: #f8f9fa; padding: 16px; border-radius: 8px; margin-bottom: 20px; - border: 1px solid #bbdefb; + border: 1px solid #e0e0e0; } .lp-tabs { diff --git a/frontend/src/components/FactoryPanel.tsx b/frontend/src/components/FactoryPanel.tsx index dc4392b..26ce51c 100644 --- a/frontend/src/components/FactoryPanel.tsx +++ b/frontend/src/components/FactoryPanel.tsx @@ -1,8 +1,8 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' +import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' import { parseUnits, formatUnits } from 'viem' -import { CONTRACTS, GAS_CONFIG, FACTORY_ABI } from '../config/contracts' +import { CONTRACTS, GAS_CONFIG, FACTORY_ABI, VAULT_ABI } from '../config/contracts' export function FactoryPanel() { const { t } = useTranslation() @@ -33,6 +33,19 @@ export function FactoryPanel() { const [upgradeVaultAddr, setUpgradeVaultAddr] = useState('') const [upgradeImplAddr, setUpgradeImplAddr] = useState('') const [batchRedemptionTime, setBatchRedemptionTime] = useState('') + const [checkVaultAddr, setCheckVaultAddr] = useState('') + const [showBatchCreate, setShowBatchCreate] = useState(false) + const [batchCreateVaults, setBatchCreateVaults] = useState<{ + name: string + symbol: string + manager: string + hardCap: string + redemptionTime: string + wusdPrice: string + ytPrice: string + }[]>([ + { name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', wusdPrice: '1', ytPrice: '1' } + ]) const { data: allVaults, refetch: refetchVaults } = useReadContract({ address: CONTRACTS.FACTORY, @@ -40,6 +53,32 @@ export function FactoryPanel() { functionName: 'getAllVaults', }) + // 批量读取所有金库的暂停状态 + const pausedContracts = useMemo(() => { + if (!allVaults || allVaults.length === 0) return [] + return allVaults.map((addr: string) => ({ + address: addr as `0x${string}`, + abi: VAULT_ABI, + functionName: 'paused' as const, + })) + }, [allVaults]) + + const { data: pausedResults, refetch: refetchPaused } = useReadContracts({ + contracts: pausedContracts, + }) + + // 构建金库暂停状态映射 + const vaultPausedMap = useMemo(() => { + const map: Record = {} + if (allVaults && pausedResults) { + allVaults.forEach((addr: string, i: number) => { + const result = pausedResults[i] + map[addr.toLowerCase()] = result?.status === 'success' ? (result.result as boolean) : false + }) + } + return map + }, [allVaults, pausedResults]) + const { data: vaultCount } = useReadContract({ address: CONTRACTS.FACTORY, abi: FACTORY_ABI, @@ -64,6 +103,16 @@ export function FactoryPanel() { functionName: 'vaultImplementation', }) + // 验证地址是否为有效金库 + const { data: isValidVault } = useReadContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'isVault', + args: checkVaultAddr && checkVaultAddr.startsWith('0x') && checkVaultAddr.length === 42 + ? [checkVaultAddr as `0x${string}`] + : undefined, + }) + const { writeContract, data: hash, isPending, reset, error: writeError, status: writeStatus } = useWriteContract() const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ @@ -73,6 +122,7 @@ export function FactoryPanel() { useEffect(() => { if (isSuccess) { refetchVaults() + refetchPaused() refetchDefaultHardCap() reset() } @@ -101,8 +151,8 @@ export function FactoryPanel() { parseUnits(createForm.hardCap, 18), CONTRACTS.WUSD, BigInt(redemptionTimestamp), - parseUnits(createForm.initialWusdPrice, 18), - parseUnits(createForm.initialYtPrice, 18), + parseUnits(createForm.initialWusdPrice, 30), // wusdPrice 使用 30 位精度 + parseUnits(createForm.initialYtPrice, 30), // ytPrice 使用 30 位精度 ], gas: GAS_CONFIG.VERY_COMPLEX, maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, @@ -117,8 +167,8 @@ export function FactoryPanel() { functionName: 'updateVaultPrices', args: [ priceForm.vault as `0x${string}`, - parseUnits(priceForm.wusdPrice, 18), - parseUnits(priceForm.ytPrice, 18), + parseUnits(priceForm.wusdPrice, 30), // wusdPrice 使用 30 位精度 + parseUnits(priceForm.ytPrice, 30), // ytPrice 使用 30 位精度 ], gas: GAS_CONFIG.STANDARD, maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, @@ -142,8 +192,8 @@ export function FactoryPanel() { // 批量更新价格 const handleBatchUpdatePrices = () => { if (selectedVaultsForBatch.length === 0) return - const wusdPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.wusdPrice, 18)) - const ytPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.ytPrice, 18)) + const wusdPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.wusdPrice, 30)) // wusdPrice 使用 30 位精度 + const ytPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.ytPrice, 30)) // ytPrice 使用 30 位精度 writeContract({ address: CONTRACTS.FACTORY, abi: FACTORY_ABI, @@ -203,6 +253,104 @@ export function FactoryPanel() { }) } + // 暂停单个金库 + const handlePauseVault = (vaultAddr: string) => { + writeContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'pauseVault', + args: [vaultAddr as `0x${string}`], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 恢复单个金库 + const handleUnpauseVault = (vaultAddr: string) => { + writeContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'unpauseVault', + args: [vaultAddr as `0x${string}`], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 批量暂停金库 + const handleBatchPauseVaults = () => { + if (selectedVaultsForBatch.length === 0) return + writeContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'pauseVaultBatch', + args: [selectedVaultsForBatch as `0x${string}`[]], + gas: GAS_CONFIG.VERY_COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 批量恢复金库 + const handleBatchUnpauseVaults = () => { + if (selectedVaultsForBatch.length === 0) return + writeContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'unpauseVaultBatch', + args: [selectedVaultsForBatch as `0x${string}`[]], + gas: GAS_CONFIG.VERY_COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 批量创建金库 + const handleBatchCreateVaults = () => { + // 验证所有金库配置 + const validVaults = batchCreateVaults.filter(v => v.name && v.symbol && v.manager && v.hardCap && v.redemptionTime) + if (validVaults.length === 0) return + + 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 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)) + + writeContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'createVaultBatch', + args: [names, symbols, managers, hardCaps, CONTRACTS.WUSD, redemptionTimes, wusdPrices, ytPrices], + gas: GAS_CONFIG.VERY_COMPLEX * BigInt(validVaults.length), + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 添加新的金库配置行 + const addBatchCreateRow = () => { + setBatchCreateVaults([...batchCreateVaults, { name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', wusdPrice: '1', ytPrice: '1' }]) + } + + // 删除金库配置行 + const removeBatchCreateRow = (index: number) => { + if (batchCreateVaults.length > 1) { + setBatchCreateVaults(batchCreateVaults.filter((_, i) => i !== index)) + } + } + + // 更新金库配置行 + const updateBatchCreateRow = (index: number, field: string, value: string) => { + const updated = [...batchCreateVaults] + updated[index] = { ...updated[index], [field]: value } + setBatchCreateVaults(updated) + } + // 设置金库实现地址 const handleSetVaultImplementation = () => { if (!newImplementation) return @@ -255,7 +403,7 @@ export function FactoryPanel() { address: CONTRACTS.FACTORY, abi: FACTORY_ABI, functionName: 'updateVaultPrices', - args: [testVault as `0x${string}`, parseUnits('1', 18), parseUnits('1', 18)], + args: [testVault as `0x${string}`, parseUnits('1', 30), parseUnits('1', 30)], // wusdPrice 和 ytPrice 都使用 30 位精度 gas: GAS_CONFIG.STANDARD, maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, @@ -298,41 +446,73 @@ export function FactoryPanel() {
- {t('factory.factoryContract')}: + {t('factory.factoryContract')}: {CONTRACTS.FACTORY}
- {t('factory.owner')}: + {t('factory.owner')}: {owner || '-'}
- {t('factory.vaultImpl')}: + {t('factory.vaultImpl')}: {vaultImplementation || '-'}
- {t('factory.defaultHardCap')}: + {t('factory.defaultHardCap')}: {defaultHardCap ? formatUnits(defaultHardCap, 18) : '0'}
- {t('factory.totalVaults')}: + {t('factory.totalVaults')}: {vaultCount?.toString() || '0'}
- {t('factory.yourRole')}: + {t('factory.yourRole')}: {isOwner ? t('factory.roleOwner') : t('factory.roleUser')}
-

{t('factory.allVaults')}

+

{t('factory.allVaults')}

{allVaults && allVaults.length > 0 ? ( - allVaults.map((vault, index) => ( -
- Vault {index + 1}: - {vault} -
- )) + allVaults.map((vault, index) => { + const isPaused = vaultPausedMap[vault.toLowerCase()] || false + return ( +
+ Vault {index + 1}: + {vault} + {/* 暂停状态标签 */} + + {isPaused ? t('factory.paused') : t('factory.active')} + + {isOwner && ( + + )} +
+ ) + }) ) : (

{t('factory.noVaults')}

)} @@ -342,7 +522,7 @@ export function FactoryPanel() { {isOwner && ( <>
-

{t('factory.createVault')}

+

{t('factory.createVault')}

@@ -421,13 +601,142 @@ export function FactoryPanel() { > {isProcessing ? t('common.processing') : t('factory.create')} + + {/* 批量创建金库 - 可折叠 */} +
+
setShowBatchCreate(!showBatchCreate)} + > +

{t('factory.batchCreateVaults')}

+ {showBatchCreate ? '▼' : '▶'} +
+ + {showBatchCreate && ( +
+ {batchCreateVaults.map((vault, index) => ( +
+
+ Vault {index + 1} + {batchCreateVaults.length > 1 && ( + + )} +
+
+
+ + updateBatchCreateRow(index, 'name', e.target.value)} + placeholder="YT-X" + className="input" + style={{ fontSize: '12px' }} + /> +
+
+ + updateBatchCreateRow(index, 'symbol', e.target.value)} + placeholder="YTX" + className="input" + style={{ fontSize: '12px' }} + /> +
+
+ + updateBatchCreateRow(index, 'manager', e.target.value)} + placeholder="0x..." + className="input" + style={{ fontSize: '12px' }} + /> +
+
+ + updateBatchCreateRow(index, 'hardCap', e.target.value)} + placeholder="100000" + className="input" + style={{ fontSize: '12px' }} + /> +
+
+
+
+ + updateBatchCreateRow(index, 'redemptionTime', e.target.value)} + className="input" + style={{ fontSize: '12px' }} + /> +
+
+ + updateBatchCreateRow(index, 'wusdPrice', e.target.value)} + placeholder="1" + className="input" + style={{ fontSize: '12px' }} + /> +
+
+ + updateBatchCreateRow(index, 'ytPrice', e.target.value)} + placeholder="1" + className="input" + style={{ fontSize: '12px' }} + /> +
+
+
+ ))} + +
+ + +
+
+ )} +
-

{t('factory.updatePrices')}

+

{t('factory.updatePrices')}

- +
+ + {/* 批量暂停/恢复金库 */} +
+
{t('factory.batchPauseUnpause')}
+
+ + +
+
+ {t('factory.pauseWarning')} +
+
)}
@@ -646,7 +981,7 @@ export function FactoryPanel() { style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }} onClick={() => setShowAdvanced(!showAdvanced)} > -

{t('factory.advancedFunctions')}

+

{t('factory.advancedFunctions')}

{showAdvanced ? '▼' : '▶'}
@@ -657,6 +992,37 @@ export function FactoryPanel() { {t('factory.advancedWarning')}
+ {/* 验证金库地址 */} +
+
验证金库地址
+
+ setCheckVaultAddr(e.target.value)} + placeholder="0x... 输入地址" + className="input" + style={{ flex: 1, fontSize: '12px' }} + /> + {checkVaultAddr && checkVaultAddr.length === 42 && ( + + {isValidVault ? '[OK] 有效金库' : '[NO] 不是金库'} + + )} +
+
+ 使用 isVault() 函数验证地址是否是通过工厂创建的有效金库合约 +
+
+ {/* 设置金库实现 */}
{t('factory.setVaultImplementation')}
@@ -751,7 +1117,7 @@ export function FactoryPanel() { style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }} onClick={() => setShowPermissionTest(!showPermissionTest)} > -

{t('test.permissionTests')}

+

{t('test.permissionTests')}

{showPermissionTest ? '▼' : '▶'}
diff --git a/frontend/src/components/LPPanel.tsx b/frontend/src/components/LPPanel.tsx index 396536a..b101e03 100644 --- a/frontend/src/components/LPPanel.tsx +++ b/frontend/src/components/LPPanel.tsx @@ -12,7 +12,9 @@ import { YT_PRICE_FEED_ABI, WUSD_ABI, YT_ASSET_VAULT_ABI, - FACTORY_ABI + FACTORY_ABI, + USDY_ABI, + VAULT_ABI } from '../config/contracts' import { useTransactions } from '../context/TransactionContext' import type { TransactionType } from '../context/TransactionContext' @@ -21,31 +23,16 @@ import { TransactionHistory } from './TransactionHistory' /** * ============================================================================ - * 已知问题 (Known Issue) - 2024-12-17 + * LP 流动性池面板 * ============================================================================ * - * 【问题描述】 - * LP 流动性池操作 (addLiquidity, removeLiquidity, swapYT) 当前会失败 - * 错误: PriceChangeTooLarge (0xa8eb64ed) - * - * 【根本原因】 - * YTPriceFeed 合约需要从 YT 代币读取 assetPrice 变量,但 YTAssetVault 合约 - * 只有 ytPrice,没有 assetPrice。 - * - * 调用链: - * addLiquidity() → YTVault.buyUSDY() → YTPriceFeed.getMinPrice() - * → 读取 YT-A.assetPrice() → 不存在,返回异常值 - * → currentPrice vs cachedPrice 差异 > 5% → PriceChangeTooLarge - * - * 【解决方案】(需要合约端修复) - * YTAssetVault 合约需要添加: - * function assetPrice() external view returns (uint256) { - * return ytPrice; - * } + * 功能: + * - 添加流动性 (addLiquidity): 存入 YT 代币获得 ytLP + * - 移除流动性 (removeLiquidity): 销毁 ytLP 获取代币 + * - 代币互换 (swapYT): 在池内交换 YT 代币 * * 【文档参考】 - * - ytLp池子合约流程文档.md 第 320-322 行 (价格获取流程) - * - 合约文档.md 第 4093-4118 行 (YTPriceFeed.getPriceInfo ABI) + * - ytLp池子合约流程文档.md * ============================================================================ */ @@ -129,6 +116,10 @@ export function LPPanel() { token: CONTRACTS.VAULTS.YT_A, isStable: false, }) + // USDY Vault 白名单管理状态 + const [showUsdyPanel, setShowUsdyPanel] = useState(false) + const [vaultToAdd, setVaultToAdd] = useState('') + const [vaultToRemove, setVaultToRemove] = useState('') // ===== 动态获取 LP 池代币列表 ===== const { data: rawPoolTokenAddresses } = useReadContract({ @@ -137,6 +128,13 @@ export function LPPanel() { functionName: 'getAllPoolTokens', }) + // ===== 从 Factory 获取所有创建的金库(用于管理员配置下拉框)===== + const { data: factoryVaults } = useReadContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'getAllVaults', + }) + // 去重处理(合约可能返回重复地址) const poolTokenAddresses = useMemo(() => { if (!rawPoolTokenAddresses || rawPoolTokenAddresses.length === 0) return [] @@ -162,6 +160,19 @@ export function LPPanel() { contracts: tokenInfoContracts, }) + // 批量读取 Factory 金库的 symbol 和 name(用于管理员配置下拉框) + 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 [] @@ -222,6 +233,21 @@ export function LPPanel() { 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 } = useReadContracts({ + contracts: tokenWeightsContracts, + }) + // 构建代币列表(动态) const POOL_TOKENS = useMemo(() => { if (!poolTokenAddresses || poolTokenAddresses.length === 0) return [] @@ -232,6 +258,7 @@ export function LPPanel() { 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, symbol: symbol || `Token ${index}`, @@ -240,9 +267,87 @@ export function LPPanel() { isWhitelisted, usdyAmount, isStable, + weight, } }) - }, [poolTokenAddresses, tokenInfoResults, balanceResults, whitelistResults, usdyAmountsResults, stableTokensResults]) + }, [poolTokenAddresses, tokenInfoResults, balanceResults, whitelistResults, usdyAmountsResults, stableTokensResults, tokenWeightsResults]) + + // ===== ALL_AVAILABLE_TOKENS: 合并 POOL_TOKENS + Factory金库 (用于管理员配置下拉框) ===== + // 这个列表包含: + // 1. 已在 LP 池中的代币 (POOL_TOKENS) + // 2. Factory 创建的所有金库代币(包括还没加入 LP 池的新金库) + // source: 'pool' = 已在LP池, 'factory' = 新创建的金库 + type AvailableToken = { address: string; symbol: string; name: string; source: 'pool' | 'factory'; isStable?: boolean } + const ALL_AVAILABLE_TOKENS = useMemo((): AvailableToken[] => { + const poolAddressSet = new Set(poolTokenAddresses?.map((a: string) => a.toLowerCase()) || []) + + // Factory 金库中不在 POOL_TOKENS 里的 + 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', + }) + } + }) + } + + // 合并:POOL_TOKENS(带source标记)+ Factory独有代币 + WUSD + const poolWithSource: AvailableToken[] = POOL_TOKENS.map(t => ({ + address: t.address, + symbol: t.symbol, + name: t.name, + source: 'pool' as const, + isStable: t.isStable, + })) + const wusdInList = [...poolWithSource, ...factoryOnlyTokens].some( + t => t.address.toLowerCase() === CONTRACTS.WUSD.toLowerCase() + ) + + const result: AvailableToken[] = [...poolWithSource, ...factoryOnlyTokens] + if (!wusdInList) { + result.push({ + address: CONTRACTS.WUSD, + symbol: 'WUSD', + name: 'Wrapped USD', + source: 'pool', // WUSD 作为基础代币归为 pool + }) + } + + return result + }, [POOL_TOKENS, poolTokenAddresses, factoryVaults, factoryVaultInfoResults]) + + // 当 POOL_TOKENS 加载完成后,同步表单初始值 + useEffect(() => { + if (POOL_TOKENS.length >= 2) { + // 使用 POOL_TOKENS 中的实际地址更新 swapForm + const whitelistedTokens = POOL_TOKENS.filter(t => t.isWhitelisted) + if (whitelistedTokens.length >= 2) { + setSwapForm(prev => ({ + ...prev, + tokenIn: whitelistedTokens[0].address, + tokenOut: whitelistedTokens[1].address, + })) + } + // 同步 addLiquidityForm + if (whitelistedTokens.length >= 1) { + setAddLiquidityForm(prev => ({ + ...prev, + token: whitelistedTokens[0].address, + })) + setRemoveLiquidityForm(prev => ({ + ...prev, + token: whitelistedTokens[0].address, + })) + } + } + }, [POOL_TOKENS.length]) // 只在 POOL_TOKENS 长度变化时触发 // Read pool data const { data: ytLPBalance, refetch: refetchBalance } = useReadContract({ @@ -258,6 +363,24 @@ export function LPPanel() { 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 } = useReadContract({ address: CONTRACTS.YT_REWARD_ROUTER, abi: YT_REWARD_ROUTER_ABI, @@ -321,13 +444,6 @@ export function LPPanel() { functionName: 'ytPrice', }) - // 读取诊断代币的 assetPrice - const { data: diagAssetPrice } = useReadContract({ - address: diagToken as `0x${string}`, - abi: YT_ASSET_VAULT_ABI, - functionName: 'assetPrice', - }) - // 诊断代币的动态读取 const { data: diagTokenDecimals } = useReadContract({ address: CONTRACTS.YT_VAULT, @@ -458,17 +574,17 @@ export function LPPanel() { }) // 读取紧急模式状态 - const { data: emergencyMode } = useReadContract({ + const { data: emergencyMode, refetch: refetchEmergencyMode } = useReadContract({ address: CONTRACTS.YT_VAULT, abi: YT_VAULT_ABI, functionName: 'emergencyMode', }) - // 读取 swap 开关状态 - const { data: swapEnabled } = useReadContract({ + // 读取 swap 开关状态 (合约实际使用 isSwapEnabled) + const { data: swapEnabled, refetch: refetchSwapEnabled } = useReadContract({ address: CONTRACTS.YT_VAULT, abi: YT_VAULT_ABI, - functionName: 'swapEnabled', + functionName: 'isSwapEnabled', }) // 读取总权重 @@ -478,6 +594,85 @@ export function LPPanel() { functionName: 'totalTokenWeights', }) + // ===== USDY 信息读取 ===== + const { data: usdyTotalSupply, refetch: refetchUsdySupply } = useReadContract({ + address: CONTRACTS.USDY, + abi: USDY_ABI, + functionName: 'totalSupply', + }) + + const { data: usdyOwner } = useReadContract({ + address: CONTRACTS.USDY, + abi: USDY_ABI, + functionName: 'owner', + }) + + const { data: usdySymbol } = useReadContract({ + address: CONTRACTS.USDY, + abi: USDY_ABI, + functionName: 'symbol', + }) + + // 读取 PoolManager 持有的 USDY 余额 + const { data: poolManagerUsdyBalance } = useReadContract({ + address: CONTRACTS.USDY, + abi: USDY_ABI, + functionName: 'balanceOf', + args: [CONTRACTS.YT_POOL_MANAGER], + }) + + // 读取 YTVault 是否在 USDY 白名单中 + const { data: isYtVaultInWhitelist, refetch: refetchVaultWhitelist } = useReadContract({ + address: CONTRACTS.USDY, + abi: USDY_ABI, + functionName: 'vaults', + args: [CONTRACTS.YT_VAULT], + }) + + // 判断当前用户是否是 USDY Owner + const isUsdyOwner = address && usdyOwner && address.toLowerCase() === (usdyOwner as string).toLowerCase() + + // 计算池子总 USDY 和目标比例 + const poolStats = useMemo(() => { + if (!POOL_TOKENS.length || !totalTokenWeights) return null + + // 计算总 USDY(所有代币的 usdyAmount 之和) + const totalUsdy = POOL_TOKENS.reduce((sum, token) => { + return sum + (token.usdyAmount || 0n) + }, 0n) + + // 为每个代币计算目标比例和偏差 + const tokensWithRatio = POOL_TOKENS.map(token => { + const weight = token.weight || 0n + const currentUsdy = token.usdyAmount || 0n + + // 目标比例 = weight / totalTokenWeights + const targetRatio = totalTokenWeights > 0n + ? Number(weight) / Number(totalTokenWeights) * 100 + : 0 + + // 当前比例 = currentUsdy / totalUsdy + const currentRatio = totalUsdy > 0n + ? Number(currentUsdy) / Number(totalUsdy) * 100 + : 0 + + // 偏差 = 当前比例 - 目标比例 + const deviation = currentRatio - targetRatio + + return { + ...token, + targetRatio, + currentRatio, + deviation, + } + }) + + return { + totalUsdy, + tokens: tokensWithRatio, + } + }, [POOL_TOKENS, totalTokenWeights]) + // 读取用户账户价值 const { data: accountValue } = useReadContract({ address: CONTRACTS.YT_REWARD_ROUTER, @@ -486,6 +681,82 @@ export function LPPanel() { args: address ? [address] : undefined, }) + // ===== RewardRouter 关联合约地址 ===== + const { data: routerYtLP } = useReadContract({ + address: CONTRACTS.YT_REWARD_ROUTER, + abi: YT_REWARD_ROUTER_ABI, + functionName: 'ytLP', + }) + + const { data: routerYtPoolManager } = useReadContract({ + address: CONTRACTS.YT_REWARD_ROUTER, + abi: YT_REWARD_ROUTER_ABI, + functionName: 'ytPoolManager', + }) + + const { data: routerYtVault } = useReadContract({ + address: CONTRACTS.YT_REWARD_ROUTER, + abi: YT_REWARD_ROUTER_ABI, + functionName: 'ytVault', + }) + + // ===== YTVault 常量 ===== + const { data: basisPointsDivisor } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'BASIS_POINTS_DIVISOR', + }) + + const { data: vaultPricePrecision } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'PRICE_PRECISION', + }) + + const { data: usdyDecimals } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'USDY_DECIMALS', + }) + + // ===== YTVault 池子价值 ===== + const { data: poolValue } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getPoolValue', + args: [true], + }) + + // ===== 选中代币的手续费率和价格 ===== + const { data: selectedTokenMaxPrice } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getMaxPrice', + args: [priceConfigForm.token as `0x${string}`], + }) + + const { data: selectedTokenSwapFee } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getSwapFeeBasisPoints', + args: [priceConfigForm.token as `0x${string}`, CONTRACTS.WUSD as `0x${string}`, parseUnits('1000', 18)], + }) + + const { data: selectedTokenRedemptionFee } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getRedemptionFeeBasisPoints', + args: [priceConfigForm.token as `0x${string}`, parseUnits('1000', 18)], + }) + + // ===== YTPriceFeed getMaxPrice ===== + const { data: priceFeedMaxPrice } = useReadContract({ + address: CONTRACTS.YT_PRICE_FEED, + abi: YT_PRICE_FEED_ABI, + functionName: 'getMaxPrice', + args: [priceConfigForm.token as `0x${string}`], + }) + // Check token allowance for the selected token const { data: tokenAllowance, refetch: refetchAllowance } = useReadContract({ address: addLiquidityForm.token as `0x${string}`, @@ -502,6 +773,88 @@ export function LPPanel() { args: address ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined, }) + // Check swap tokenIn allowance + const { data: swapTokenInAllowance, refetch: refetchSwapAllowance } = useReadContract({ + address: swapForm.tokenIn as `0x${string}`, + abi: WUSD_ABI, + functionName: 'allowance', + args: address ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined, + }) + + // Check swap tokenIn paused status + const { data: swapTokenInPaused } = useReadContract({ + address: swapForm.tokenIn as `0x${string}`, + abi: VAULT_ABI, + functionName: 'paused', + }) + + // Check swap tokenOut paused status + const { data: swapTokenOutPaused } = useReadContract({ + address: swapForm.tokenOut as `0x${string}`, + abi: VAULT_ABI, + functionName: 'paused', + }) + + // Swap 预览: 读取 tokenIn 的 minPrice + const { data: swapTokenInMinPrice } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getMinPrice', + args: [swapForm.tokenIn as `0x${string}`], + }) + + // Swap 预览: 读取 tokenOut 的 maxPrice + const { data: swapTokenOutMaxPrice } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getMaxPrice', + args: [swapForm.tokenOut as `0x${string}`], + }) + + // 计算 swap 预估输出: amountOut = amountIn * minPriceIn / maxPriceOut + const swapPreviewAmount = useMemo(() => { + if (!swapForm.amount || !swapTokenInMinPrice || !swapTokenOutMaxPrice) return null + if (swapTokenOutMaxPrice === 0n) return null + try { + const amountIn = parseUnits(swapForm.amount, 18) + // 价格精度是 30 位,计算: amountIn * minPriceIn / maxPriceOut + const amountOut = (amountIn * swapTokenInMinPrice) / swapTokenOutMaxPrice + return amountOut + } catch { + return null + } + }, [swapForm.amount, swapTokenInMinPrice, swapTokenOutMaxPrice]) + + // 添加流动性预览: 读取选中代币的价格 + const { data: addLiquidityTokenPrice } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getMinPrice', + args: [addLiquidityForm.token as `0x${string}`], + }) + + // 读取 ytLP 价格 + const { data: ytLPPriceData } = useReadContract({ + address: CONTRACTS.YT_REWARD_ROUTER, + abi: YT_REWARD_ROUTER_ABI, + functionName: 'getYtLPPrice', + }) + + // 计算添加流动性预估输出: ytLPAmount ≈ tokenAmount * tokenPrice / ytLPPrice + // tokenPrice 是 30 位精度,ytLPPrice 是 18 位精度,差 12 位 + const addLiquidityPreviewAmount = useMemo(() => { + if (!addLiquidityForm.amount || !addLiquidityTokenPrice || !ytLPPriceData) return null + if (ytLPPriceData === 0n) return null + try { + const amountIn = parseUnits(addLiquidityForm.amount, 18) + const PRECISION_DIFF = 10n ** 12n // tokenPrice(30位) - ytLPPrice(18位) = 12位 + const ytLPAmount = (amountIn * addLiquidityTokenPrice) / ytLPPriceData / PRECISION_DIFF + return ytLPAmount + } catch { + return null + } + }, [addLiquidityForm.amount, addLiquidityTokenPrice, ytLPPriceData]) + const { writeContract, data: hash, isPending, error: writeError, reset, status: writeStatus } = useWriteContract() const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({ @@ -549,6 +902,11 @@ export function LPPanel() { refetchTokenBalances() refetchAllowance() refetchYtLPAllowance() + refetchSwapAllowance() + refetchSwapEnabled() + refetchEmergencyMode() + refetchUsdySupply() + refetchVaultWhitelist() reset() setAddLiquidityForm(f => ({ ...f, amount: '' })) setRemoveLiquidityForm(f => ({ ...f, amount: '' })) @@ -657,6 +1015,28 @@ export function LPPanel() { }) } + // Approve tokenIn for swap + const handleApproveSwapToken = () => { + if (!address) return + recordTx('approve', undefined, getTokenSymbol(swapForm.tokenIn)) + writeContract({ + address: swapForm.tokenIn as `0x${string}`, + abi: WUSD_ABI, + functionName: 'approve', + args: [CONTRACTS.YT_REWARD_ROUTER, parseUnits('1000000000', 18)], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // Check if swap needs approval + const needsSwapApproval = (): boolean => { + if (!swapTokenInAllowance || !swapForm.amount) return false + const amountIn = parseUnits(swapForm.amount || '0', 18) + return swapTokenInAllowance < amountIn + } + // Add liquidity const handleAddLiquidity = () => { if (!address || !addLiquidityForm.amount) return @@ -893,6 +1273,35 @@ export function LPPanel() { }) } + // ===== USDY Vault 白名单管理 (仅 USDY Owner) ===== + const handleAddUsdyVault = () => { + if (!address || !vaultToAdd) return + recordTx('test', vaultToAdd, 'AddUsdyVault') + writeContract({ + address: CONTRACTS.USDY, + abi: USDY_ABI, + functionName: 'addVault', + args: [vaultToAdd as `0x${string}`], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + const handleRemoveUsdyVault = () => { + if (!address || !vaultToRemove) return + recordTx('test', vaultToRemove, 'RemoveUsdyVault') + writeContract({ + address: CONTRACTS.USDY, + abi: USDY_ABI, + functionName: 'removeVault', + args: [vaultToRemove as `0x${string}`], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + // Boundary test function const runBoundaryTest = (testType: string) => { if (!address) return @@ -1061,66 +1470,153 @@ export function LPPanel() { {/* Pool Info - Compact Layout */}
- {/* 合约地址行 */} -
- Router: {CONTRACTS.YT_REWARD_ROUTER.slice(0, 8)}...{CONTRACTS.YT_REWARD_ROUTER.slice(-6)} - ytLP: {CONTRACTS.YT_LP_TOKEN.slice(0, 8)}...{CONTRACTS.YT_LP_TOKEN.slice(-6)} + {/* ytLP 代币信息 */} +
+ 名称: {ytLPName || 'ytLP'} + 符号: {ytLPSymbol || 'ytLP'} + 精度: {ytLPDecimals?.toString() || '18'} + 合约: {CONTRACTS.YT_LP_TOKEN.slice(0, 8)}...{CONTRACTS.YT_LP_TOKEN.slice(-6)}
{/* 池子数据 + 用户数据 - 网格布局 */}
-
AUM: {aumInUsdy ? Number(formatUnits(aumInUsdy, 18)).toFixed(2) : '0'} USDY
-
ytLP价格: {ytLPPrice ? Number(formatUnits(ytLPPrice, 30)).toFixed(6) : '1'}
-
总供应: {ytLPTotalSupply ? Number(formatUnits(ytLPTotalSupply, 18)).toFixed(2) : '0'}
-
你的ytLP: {ytLPBalance ? Number(formatUnits(ytLPBalance, 18)).toFixed(2) : '0'}
-
冷却: {formatCooldown(getCooldownRemaining())}
-
账户价值: ${accountValue ? Number(formatUnits(accountValue, 30)).toFixed(6) : '0'}
-
+
+ AUM: {aumInUsdy ? Number(formatUnits(aumInUsdy, 18)).toFixed(2) : '0'} USDY +
+
+ ytLP价格: {ytLPPrice ? Number(formatUnits(ytLPPrice, 30)).toFixed(6) : '1'} +
+
+ 总供应: {ytLPTotalSupply ? Number(formatUnits(ytLPTotalSupply, 18)).toFixed(2) : '0'} +
+
+ 你的ytLP: {ytLPBalance ? Number(formatUnits(ytLPBalance, 18)).toFixed(2) : '0'} +
+
+ 冷却: {formatCooldown(getCooldownRemaining())} +
+
+ 账户价值: ${accountValue ? Number(formatUnits(accountValue, 30)).toFixed(6) : '0'} +
+
紧急模式:{' '} {emergencyMode ? 'ON' : '正常'}
-
+
Swap:{' '} {swapEnabled ? '开启' : '关闭'}
-
总权重: {totalTokenWeights ? totalTokenWeights.toString() : '0'}
+
+ 总权重: {totalTokenWeights ? totalTokenWeights.toString() : '0'} +
+
+ 池价值: ${poolValue ? Number(formatUnits(poolValue, 30)).toFixed(2) : '0'} +
+
+ + {/* RewardRouter 关联合约地址 */} +
+
Router关联合约:
+
+ ytLP: {routerYtLP ? `${(routerYtLP as string).slice(0, 8)}...${(routerYtLP as string).slice(-6)}` : '-'} + ytPoolManager: {routerYtPoolManager ? `${(routerYtPoolManager as string).slice(0, 8)}...${(routerYtPoolManager as string).slice(-6)}` : '-'} + ytVault: {routerYtVault ? `${(routerYtVault as string).slice(0, 8)}...${(routerYtVault as string).slice(-6)}` : '-'} +
+
+ + {/* YTVault 常量 */} +
+
Vault常量:
+
+ BASIS_POINTS_DIVISOR: {basisPointsDivisor?.toString() || '-'} + PRICE_PRECISION: {vaultPricePrecision ? `1e${vaultPricePrecision.toString().length - 1}` : '-'} + USDY_DECIMALS: {usdyDecimals?.toString() || '-'} +
- {/* 用户代币余额及白名单状态 - 动态渲染 */} + {/* 代币余额及池子比例 - 动态渲染 */}

{t('lp.yourTokenBalances')}

- - - - - - - - - - - - {POOL_TOKENS.length === 0 ? ( - - +
+
{t('lp.selectToken')}{t('common.balance')}{t('lp.usdyAmount')}{t('lp.isStableToken')}{t('lp.whitelisted')}
Loading...
+ + + + + + + + + + - ) : ( - POOL_TOKENS.map((token) => ( - - - - - - + + + {!poolStats ? ( + + - )) + ) : ( + poolStats.tokens.map((token) => ( + + + + + + + + + + + )) + )} + + {poolStats && ( + + + + + + + + + + + + )} - -
{t('lp.selectToken')}{t('common.balance')}USDYWeightTargetCurrentDeviationStable
{token.symbol}{token.balance ? formatUnits(token.balance, 18) : '0'}{token.usdyAmount ? formatUnits(token.usdyAmount, 18) : '0'} - {token.isStable ? Yes : No} - - {token.isWhitelisted ? Yes : No} -
Loading...
+ {token.symbol} + {!token.isWhitelisted && (No WL)} + + {token.balance ? Number(formatUnits(token.balance, 18)).toFixed(2) : '0'} + + {token.usdyAmount ? Number(formatUnits(token.usdyAmount, 18)).toFixed(2) : '0'} + + {token.weight ? token.weight.toString() : '0'} + + {token.targetRatio.toFixed(1)}% + + {token.currentRatio.toFixed(1)}% + + = 5 ? 'bold' : 'normal' + }}> + {token.deviation >= 0 ? '+' : ''}{token.deviation.toFixed(1)}% + + + {token.isStable ? Yes : -} +
Total- + {Number(formatUnits(poolStats.totalUsdy, 18)).toFixed(2)} + + {totalTokenWeights?.toString() || '0'} + 100%100%--
+ +
+

+ Deviation: 偏差值。正值表示该代币过多(卖出费率较高),负值表示不足(买入费率较低) +

{/* Tab Navigation */} @@ -1160,12 +1656,12 @@ export function LPPanel() { className="input" style={{ fontSize: '13px' }} > - {POOL_TOKENS.map((token) => ( + {POOL_TOKENS.filter(t => t.isWhitelisted).map((token) => ( ))} - {!POOL_TOKENS.some(t => t.address.toLowerCase() === CONTRACTS.WUSD.toLowerCase()) && ( + {!POOL_TOKENS.some(t => t.address.toLowerCase() === CONTRACTS.WUSD.toLowerCase() && t.isWhitelisted) && ( )} @@ -1195,13 +1691,39 @@ export function LPPanel() {
+ {/* 当前代币状态检查 */} + {(() => { + const currentToken = POOL_TOKENS.find(t => t.address.toLowerCase() === addLiquidityForm.token.toLowerCase()) + const tokenBalance = currentToken?.balance + const isWhitelisted = currentToken?.isWhitelisted + const hasEnoughBalance = tokenBalance && addLiquidityForm.amount && tokenBalance >= parseUnits(addLiquidityForm.amount || '0', 18) + const hasEnoughAllowance = tokenAllowance && addLiquidityForm.amount && tokenAllowance >= parseUnits(addLiquidityForm.amount || '0', 18) + + return ( +
+
+ 白名单: {isWhitelisted ? '✓' : '✗'} + 余额: {tokenBalance ? formatUnits(tokenBalance, 18) : '0'} + 授权: {tokenAllowance !== undefined ? formatUnits(tokenAllowance, 18) : '...'} +
+ {!isWhitelisted &&
⚠️ 代币未在白名单中,无法添加流动性
} + {isWhitelisted && !hasEnoughBalance && addLiquidityForm.amount &&
⚠️ 余额不足
} +
+ ) + })()} + + {/* 添加流动性预览到账金额 */} + {addLiquidityForm.amount && addLiquidityPreviewAmount && ( +
+ {t('vault.youWillReceive')}: + + {formatUnits(addLiquidityPreviewAmount, 18)} ytLP + +
+ )} + {/* 授权额度 + 按钮 - 紧凑行 */}
- - 授权: BigInt(0) ? '#4caf50' : '#f44336' }}> - {tokenAllowance !== undefined ? formatUnits(tokenAllowance, 18) : '...'} - - {needsApproval() ? (
@@ -1269,7 +1793,6 @@ export function LPPanel() {

价格信息

合约.ytPrice: {diagYtPrice !== undefined ? `$${formatUnits(diagYtPrice, 18)}` : 'N/A'}
-
合约.assetPrice: {diagAssetPrice !== undefined ? `$${formatUnits(diagAssetPrice, 30)}` : 'N/A'}
lastPrice: {diagLastPrice !== undefined ? `$${formatUnits(diagLastPrice, 30)}` : 'N/A'}
spread: {diagSpread !== undefined ? `${diagSpread.toString()} bps` : 'N/A'}
@@ -1299,7 +1822,7 @@ export function LPPanel() { const diffPercent = Number(diff) / 100 const isOk = diff <= BigInt(500) return ( -

+

价格差异: {diffPercent.toFixed(2)}% {isOk ? '(OK)' : '(超过5%!)'}

) @@ -1315,22 +1838,21 @@ export function LPPanel() { if (!diagWhitelisted) issues.push('未加入白名单') if (!diagTokenDecimals || diagTokenDecimals === 0n) issues.push('decimals未配置') if (!diagMaxUsdyAmount || diagMaxUsdyAmount === 0n) issues.push('maxUsdyAmount未配置') - if (diagAssetPrice === undefined) issues.push('assetPrice()不存在') if (diagGetMinPrice === undefined) issues.push('getMinPrice()失败') if (diagLastPrice !== undefined && diagLastPrice === 0n) issues.push('lastPrice为0') if (issues.length > 0) { return ( -
-

配置问题:

-
    +
    +

    配置问题:

    +
      {issues.map((issue, i) =>
    • {issue}
    • )}
    ) } return ( -

    +

    配置检查通过

    ) @@ -1361,12 +1883,12 @@ export function LPPanel() { onChange={(e) => setRemoveLiquidityForm({ ...removeLiquidityForm, token: e.target.value })} className="input" > - {POOL_TOKENS.map((token) => ( + {POOL_TOKENS.filter(t => t.isWhitelisted).map((token) => ( ))} - {!POOL_TOKENS.some(t => t.address.toLowerCase() === CONTRACTS.WUSD.toLowerCase()) && ( + {!POOL_TOKENS.some(t => t.address.toLowerCase() === CONTRACTS.WUSD.toLowerCase() && t.isWhitelisted) && ( )} @@ -1439,12 +1961,12 @@ export function LPPanel() { onChange={(e) => setSwapForm({ ...swapForm, tokenIn: e.target.value })} className="input" > - {POOL_TOKENS.map((token) => ( + {POOL_TOKENS.filter(t => t.isWhitelisted).map((token) => ( ))} - {!POOL_TOKENS.some(t => t.address.toLowerCase() === CONTRACTS.WUSD.toLowerCase()) && ( + {!POOL_TOKENS.some(t => t.address.toLowerCase() === CONTRACTS.WUSD.toLowerCase() && t.isWhitelisted) && ( )} @@ -1457,12 +1979,12 @@ export function LPPanel() { onChange={(e) => setSwapForm({ ...swapForm, tokenOut: e.target.value })} className="input" > - {POOL_TOKENS.map((token) => ( + {POOL_TOKENS.filter(t => t.isWhitelisted).map((token) => ( ))} - {!POOL_TOKENS.some(t => t.address.toLowerCase() === CONTRACTS.WUSD.toLowerCase()) && ( + {!POOL_TOKENS.some(t => t.address.toLowerCase() === CONTRACTS.WUSD.toLowerCase() && t.isWhitelisted) && ( )} @@ -1492,13 +2014,84 @@ export function LPPanel() {
- + {/* Swap 状态检查 */} + {(() => { + // 检查 POOL_TOKENS 是否已加载 + const isLoading = POOL_TOKENS.length === 0 + const tokenInInfo = POOL_TOKENS.find(t => t.address.toLowerCase() === swapForm.tokenIn.toLowerCase()) + const tokenOutInfo = POOL_TOKENS.find(t => t.address.toLowerCase() === swapForm.tokenOut.toLowerCase()) + const tokenInBalance = tokenInInfo?.balance + const tokenInWhitelisted = tokenInInfo?.isWhitelisted + const tokenOutWhitelisted = tokenOutInfo?.isWhitelisted + const hasEnoughBalance = tokenInBalance && swapForm.amount && tokenInBalance >= parseUnits(swapForm.amount || '0', 18) + const hasEnoughAllowance = swapTokenInAllowance !== undefined && swapForm.amount && swapTokenInAllowance >= parseUnits(swapForm.amount || '0', 18) + const tokenInIsPaused = swapTokenInPaused === true + const tokenOutIsPaused = swapTokenOutPaused === true + + const issues: string[] = [] + if (!swapEnabled) issues.push('Swap 功能已关闭') + if (!isLoading && !tokenInWhitelisted) issues.push(`${tokenInInfo?.symbol || 'TokenIn'} 未在白名单`) + if (!isLoading && !tokenOutWhitelisted) issues.push(`${tokenOutInfo?.symbol || 'TokenOut'} 未在白名单`) + if (tokenInIsPaused) issues.push(`${tokenInInfo?.symbol || 'TokenIn'} 金库已暂停`) + if (tokenOutIsPaused) issues.push(`${tokenOutInfo?.symbol || 'TokenOut'} 金库已暂停`) + if (!isLoading && !hasEnoughBalance && swapForm.amount) issues.push('余额不足') + if (!hasEnoughAllowance && swapForm.amount) issues.push('授权不足') + + return ( +
+ {isLoading ? ( +
{t('common.loading')}
+ ) : ( + <> +
+ Swap: {swapEnabled ? '开启' : '关闭'} + {tokenInInfo?.symbol || 'In'} 白名单: {tokenInWhitelisted ? '✓' : '✗'} + {tokenOutInfo?.symbol || 'Out'} 白名单: {tokenOutWhitelisted ? '✓' : '✗'} + {tokenInInfo?.symbol || 'In'} 暂停: {tokenInIsPaused ? '是' : '否'} + {tokenOutInfo?.symbol || 'Out'} 暂停: {tokenOutIsPaused ? '是' : '否'} +
+
+ 余额: {tokenInBalance ? formatUnits(tokenInBalance, 18) : '0'} + 授权: {swapTokenInAllowance !== undefined ? formatUnits(swapTokenInAllowance, 18) : '...'} +
+ {issues.length > 0 && ( +
+ ⚠️ {issues.join(' | ')} +
+ )} + + )} +
+ ) + })()} + + {/* Swap 预览到账金额 */} + {swapForm.amount && swapPreviewAmount && ( +
+ {t('vault.youWillReceive')}: + + {formatUnits(swapPreviewAmount, 18)} {getTokenSymbol(swapForm.tokenOut)} + +
+ )} + + {needsSwapApproval() ? ( + + ) : ( + + )}
)} @@ -1626,15 +2219,15 @@ export function LPPanel() {
{/* 权限状态 */}
-
+
YTPriceFeed Gov: {priceFeedGov ? `${(priceFeedGov as string).slice(0, 6)}...${(priceFeedGov as string).slice(-4)}` : '...'} - + {address && priceFeedGov && address.toLowerCase() === (priceFeedGov as string).toLowerCase() ? '[OK]' : '[NO]'}
-
+
YTVault Gov: {vaultGov ? `${(vaultGov as string).slice(0, 6)}...${(vaultGov as string).slice(-4)}` : '...'} - + {address && vaultGov && address.toLowerCase() === (vaultGov as string).toLowerCase() ? '[OK]' : '[NO]'}
@@ -1648,17 +2241,21 @@ export function LPPanel() {
{/* 价格对比 - 关键诊断信息 */}
价格对比 {selectedTokenYtPrice && selectedTokenLastPrice && selectedTokenYtPrice !== selectedTokenLastPrice && '[不匹配!]'} @@ -1723,10 +2320,30 @@ export function LPPanel() { )}
- {/* 当前价差 */} + {/* 当前价差 + 价格 + 手续费 */}
- 当前价差: - {selectedTokenSpread !== undefined ? `${selectedTokenSpread.toString()} bps` : 'N/A'} +
+
+ 价差: + {selectedTokenSpread !== undefined ? `${selectedTokenSpread.toString()} bps` : 'N/A'} +
+
+ getMaxPrice (Vault): + {selectedTokenMaxPrice !== undefined ? `$${Number(formatUnits(selectedTokenMaxPrice, 30)).toFixed(6)}` : 'N/A'} +
+
+ getMaxPrice (Feed): + {priceFeedMaxPrice !== undefined ? `$${Number(formatUnits(priceFeedMaxPrice, 30)).toFixed(6)}` : 'N/A'} +
+
+ SwapFee: + {selectedTokenSwapFee !== undefined ? `${selectedTokenSwapFee.toString()} bps` : 'N/A'} +
+
+ RedemptionFee: + {selectedTokenRedemptionFee !== undefined ? `${selectedTokenRedemptionFee.toString()} bps` : 'N/A'} +
+
{/* 设置价格 + 设置价差 */} @@ -1751,13 +2368,50 @@ export function LPPanel() {
+ {/* 设置 ytPrice */} +
+
+ +
+ setYtPriceForm({ ytPrice: e.target.value })} + placeholder="1.0" + className="input" + style={{ fontSize: '12px', width: '100px' }} + step="0.01" + /> + USD (30位精度) + + + 当前: {selectedTokenYtPrice ? formatUnits(selectedTokenYtPrice as bigint, 30) : 'N/A'} + +
+

+ 此操作将保持 wusdPrice 不变 ({selectedTokenWusdPrice ? formatUnits(selectedTokenWusdPrice as bigint, 18) : 'N/A'}),仅更新 ytPrice +

+
+
+ {/* 设置稳定币 + WUSD价格源 */}
+ )} +
+ + {/* USDY 内部计价代币信息 */} +
+
setShowUsdyPanel(!showUsdyPanel)} + > +

+ USDY 内部计价代币 +

+ {showUsdyPanel ? '▼' : '▶'} +
+ + {showUsdyPanel && ( +
+ {/* USDY 基础信息 */} +
+
+ 合约地址: +
+ {CONTRACTS.USDY.slice(0, 10)}...{CONTRACTS.USDY.slice(-8)} +
+
+
+ 符号: +
+ {usdySymbol?.toString() || 'USDY'} +
+
+
+ Owner: +
+ {usdyOwner ? `${(usdyOwner as string).slice(0, 6)}...${(usdyOwner as string).slice(-4)}` : '...'} + {isUsdyOwner && [You]} +
+
+
+ + {/* USDY 供应量信息 */} +
+
+ + 总供应量: + +
+ {usdyTotalSupply ? Number(formatUnits(usdyTotalSupply, 18)).toLocaleString(undefined, { maximumFractionDigits: 2 }) : '0'} USDY +
+
+
+ + PoolManager 余额: + +
+ {poolManagerUsdyBalance ? Number(formatUnits(poolManagerUsdyBalance, 18)).toLocaleString(undefined, { maximumFractionDigits: 2 }) : '0'} USDY +
+
+
+ + {/* YTVault 白名单状态 */} +
+
+ YTVault 白名单状态 +
+
+ YTVault ({CONTRACTS.YT_VAULT.slice(0, 6)}...{CONTRACTS.YT_VAULT.slice(-4)}): + + {isYtVaultInWhitelist ? '[OK] 已在白名单' : '[NO] 未在白名单'} + +
+
+ 提示: 只有在 USDY 白名单中的 Vault 合约才能 mint/burn USDY +
+
+ + {/* USDY Vault 白名单管理 (仅 Owner 可见) */} + {isUsdyOwner && ( +
+
+ Vault 白名单管理 (仅 Owner) +
+ + {/* 添加 Vault 到白名单 */} +
+ +
+ setVaultToAdd(e.target.value)} + placeholder="0x... Vault 合约地址" + className="input" + style={{ fontSize: '11px', flex: 1 }} + /> + +
+
+ + {/* 从白名单移除 Vault */} +
+ +
+ setVaultToRemove(e.target.value)} + placeholder="0x... Vault 合约地址" + className="input" + style={{ fontSize: '11px', flex: 1 }} + /> + +
+
+ + {/* 快速操作 */} +
+ 快速操作: +
+ + +
+
+
+ )} + + {/* USDY 说明 */} +
+ USDY 说明: +
    +
  • USDY 是内部计价代币,类似于 GMX 的 USDG
  • +
  • 当用户存入代币时,YTVault 自动 mint 对应价值的 USDY
  • +
  • 当用户取出代币时,YTVault 自动 burn 对应的 USDY
  • +
  • USDY 不对外流通,仅用于池内价值计算
  • +
  • 只有白名单中的 Vault 才能 mint/burn USDY
  • +
)} diff --git a/frontend/src/components/TransactionHistory.tsx b/frontend/src/components/TransactionHistory.tsx index 29a3548..cf11f1b 100644 --- a/frontend/src/components/TransactionHistory.tsx +++ b/frontend/src/components/TransactionHistory.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { useTranslation } from 'react-i18next' import type { TransactionRecord } from '../hooks/useTransactionHistory' @@ -8,6 +9,17 @@ interface Props { export function TransactionHistory({ transactions, onClear }: Props) { const { t } = useTranslation() + const [copiedId, setCopiedId] = useState(null) + + const handleCopy = async (hash: string, txId: string) => { + try { + await navigator.clipboard.writeText(hash) + setCopiedId(txId) + setTimeout(() => setCopiedId(null), 1500) + } catch (err) { + console.error('Failed to copy:', err) + } + } const getTypeLabel = (type: string) => { const labels: Record = { @@ -75,13 +87,31 @@ export function TransactionHistory({ transactions, onClear }: Props) {
{tx.hash && typeof tx.hash === 'string' && tx.hash.startsWith('0x') ? ( - - {shortenHash(tx.hash)} - + <> + + {shortenHash(tx.hash)} + + + ) : ( - )} diff --git a/frontend/src/components/VaultPanel.tsx b/frontend/src/components/VaultPanel.tsx index 9cce6c8..8a8b9f1 100644 --- a/frontend/src/components/VaultPanel.tsx +++ b/frontend/src/components/VaultPanel.tsx @@ -2,7 +2,8 @@ import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' import { parseUnits, formatUnits, maxUint256 } from 'viem' -import { CONTRACTS, GAS_CONFIG, VAULT_ABI, WUSD_ABI, FACTORY_ABI } from '../config/contracts' +import { CONTRACTS, GAS_CONFIG, VAULT_ABI, WUSD_ABI, FACTORY_ABI, YT_REWARD_ROUTER_ABI, YT_VAULT_ABI } from '../config/contracts' +import { useMemo } from 'react' import { useTransactions } from '../context/TransactionContext' import type { TransactionType } from '../context/TransactionContext' import { useToast } from './Toast' @@ -24,7 +25,7 @@ export function VaultPanel() { const [selectedVault, setSelectedVault] = useState<{ name: string; address: string }>(DEFAULT_VAULTS[0]) const [buyAmount, setBuyAmount] = useState('') const [sellAmount, setSellAmount] = useState('') - const [activeTab, setActiveTab] = useState<'buy' | 'sell'>('buy') + const [activeTab, setActiveTab] = useState<'buy' | 'sell' | 'transfer'>('buy') const [showBoundaryTest, setShowBoundaryTest] = useState(false) const [testResult, setTestResult] = useState<{ type: 'success' | 'error' | 'pending', msg: string } | null>(null) const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null) @@ -42,6 +43,21 @@ export function VaultPanel() { const [withdrawManagedAmount, setWithdrawManagedAmount] = useState('') const [withdrawToAddress, setWithdrawToAddress] = useState('') + // YT 转账状态 + const [transferYtTo, setTransferYtTo] = useState('') + const [transferYtAmount, setTransferYtAmount] = useState('') + + // 倒计时状态 + const [countdown, setCountdown] = useState(0) + + // 排队退出 - 批量处理 + const [batchSize, setBatchSize] = useState('10') + + // 硬顶 Swap 状态 + const [showHardcapSwap, setShowHardcapSwap] = useState(false) + const [hardcapSwapAmount, setHardcapSwapAmount] = useState('') + const [hardcapSwapSlippage, setHardcapSwapSlippage] = useState('0.5') + // 从合约动态读取所有金库地址 const { data: allVaultsData } = useReadContract({ address: CONTRACTS.FACTORY, @@ -152,13 +168,6 @@ export function VaultPanel() { functionName: 'manager', }) - // 读取 managedAssets (Manager 管理的资产) - const { data: managedAssets, refetch: refetchManagedAssets } = useReadContract({ - address: selectedVault.address as `0x${string}`, - abi: VAULT_ABI, - functionName: 'managedAssets', - }) - // 读取 Manager 对 Vault 的 WUSD 授权额度 (用于 depositManagedAssets) const { data: managerWusdAllowance, refetch: refetchManagerAllowance } = useReadContract({ address: CONTRACTS.WUSD, @@ -173,6 +182,133 @@ export function VaultPanel() { functionName: 'owner', }) + // 读取金库的 factory 地址 + const { data: vaultFactory } = useReadContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'factory', + }) + + // 读取金库的 wusdAddress + const { data: vaultWusdAddress } = useReadContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'wusdAddress', + }) + + // 读取 PRICE_PRECISION 常量 + const { data: pricePrecision } = useReadContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'PRICE_PRECISION', + }) + + // ===== 硬顶 Swap 相关数据读取 ===== + // WUSD 对 YT_REWARD_ROUTER 的授权额度 + const { data: wusdRouterAllowance, refetch: refetchWusdRouterAllowance } = useReadContract({ + address: CONTRACTS.WUSD, + abi: WUSD_ABI, + functionName: 'allowance', + args: address ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined, + }) + + // 读取 LP 池的 swap 开关状态 + const { data: lpSwapEnabled } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'isSwapEnabled', + }) + + // 读取 WUSD 在 LP 池中的白名单状态 + const { data: wusdWhitelisted } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'whitelistedTokens', + args: [CONTRACTS.WUSD], + }) + + // 读取目标 YT 在 LP 池中的白名单状态 + const { data: targetYtWhitelisted } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'whitelistedTokens', + args: [selectedVault.address as `0x${string}`], + }) + + // 读取 WUSD 的价格 (用于 swap 预览) + const { data: wusdMinPrice } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getMinPrice', + args: [CONTRACTS.WUSD], + }) + + // 读取目标 YT 的价格 (用于 swap 预览) + const { data: targetYtMaxPrice } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getMaxPrice', + args: [selectedVault.address as `0x${string}`], + }) + + // 读取目标 YT 在 LP 池中的 USDY 余额 (流动性) + const { data: targetYtUsdyAmount } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'usdyAmounts', + args: [selectedVault.address as `0x${string}`], + }) + + // ===== 排队退出相关数据读取 ===== + // 读取队列进度 + const { data: queueProgress, refetch: refetchQueueProgress } = useReadContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'getQueueProgress', + }) + + // 读取用户待处理请求 + const { data: userPendingRequests, refetch: refetchUserPendingRequests } = useReadContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'getUserPendingRequests', + args: address ? [address] : undefined, + }) + + + // 计算是否达到硬顶 (使用 totalSupply 判断) + const isAtHardCap = useMemo(() => { + if (!vaultInfo) return false + const totalSupply = vaultInfo[3] as bigint + const hardCap = vaultInfo[4] as bigint + return hardCap > 0n && totalSupply >= hardCap + }, [vaultInfo]) + + // 计算硬顶 swap 预览金额 + const hardcapSwapPreview = useMemo(() => { + if (!hardcapSwapAmount || !wusdMinPrice || !targetYtMaxPrice) return null + if (targetYtMaxPrice === 0n) return null + try { + const amountIn = parseUnits(hardcapSwapAmount, 18) + // WUSD 价格和 YT 价格都是 30 位精度 + const amountOut = (amountIn * wusdMinPrice) / targetYtMaxPrice + return amountOut + } catch { + return null + } + }, [hardcapSwapAmount, wusdMinPrice, targetYtMaxPrice]) + + // 检查硬顶 swap 是否需要授权 + const needsHardcapSwapApproval = useMemo(() => { + if (!wusdRouterAllowance || !hardcapSwapAmount) return false + try { + const amountIn = parseUnits(hardcapSwapAmount, 18) + return wusdRouterAllowance < amountIn + } catch { + return false + } + }, [wusdRouterAllowance, hardcapSwapAmount]) + const { writeContract, data: hash, isPending, reset, error: writeError, status: writeStatus } = useWriteContract() const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({ @@ -220,12 +356,17 @@ export function VaultPanel() { refetchYtBalance() refetchWusdBalance() refetchAllowance() - refetchManagedAssets() refetchManagerAllowance() + refetchWusdRouterAllowance() + refetchQueueProgress() + refetchUserPendingRequests() setBuyAmount('') setSellAmount('') setDepositManagedAmount('') setWithdrawManagedAmount('') + setTransferYtTo('') + setTransferYtAmount('') + setHardcapSwapAmount('') reset() } }, [isSuccess]) @@ -337,6 +478,49 @@ export function VaultPanel() { }) } + // 授权 WUSD 给 YT_REWARD_ROUTER (用于硬顶 swap) + const handleApproveWusdForRouter = async () => { + if (!address) return + recordTx('approve', undefined, 'WUSD->Router') + writeContract({ + address: CONTRACTS.WUSD, + abi: WUSD_ABI, + functionName: 'approve', + args: [CONTRACTS.YT_REWARD_ROUTER, maxUint256], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 硬顶 Swap: 用 WUSD 换取目标 YT + const handleHardcapSwap = async () => { + if (!hardcapSwapAmount || !address) return + const amountIn = parseUnits(hardcapSwapAmount, 18) + // 计算最小输出 (考虑滑点) + const slippageBps = Math.floor(parseFloat(hardcapSwapSlippage) * 100) // 0.5% -> 50 + let minOut = 0n + if (hardcapSwapPreview) { + minOut = hardcapSwapPreview - (hardcapSwapPreview * BigInt(slippageBps) / 10000n) + } + recordTx('swap', hardcapSwapAmount, `WUSD -> ${selectedVault.name}`) + writeContract({ + address: CONTRACTS.YT_REWARD_ROUTER, + abi: YT_REWARD_ROUTER_ABI, + functionName: 'swapYT', + args: [ + CONTRACTS.WUSD, // tokenIn + selectedVault.address as `0x${string}`, // tokenOut (目标 YT) + amountIn, // amountIn + minOut, // minOut + address // receiver + ], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + const handleBuy = async () => { if (!buyAmount) return recordTx('buy', buyAmount, 'WUSD') @@ -367,11 +551,12 @@ export function VaultPanel() { // ===== 管理员功能 ===== - // 更新金库价格 (Factory.updateVaultPrices) + // 更新金库价格 (Factory.updateVaultPrices) - 同时更新两个 + // wusdPrice: 18位精度, ytPrice: 30位精度 const handleUpdatePrices = () => { if (!address) return const wusdPrice = parseUnits(priceUpdateForm.wusdPrice, 18) - const ytPrice = parseUnits(priceUpdateForm.ytPrice, 18) + const ytPrice = parseUnits(priceUpdateForm.ytPrice, 30) recordTx('test', `${priceUpdateForm.wusdPrice}/${priceUpdateForm.ytPrice}`, 'UpdatePrices') writeContract({ address: CONTRACTS.FACTORY, @@ -384,6 +569,40 @@ export function VaultPanel() { }) } + // 单独更新 wusdPrice (保持 ytPrice 不变) + const handleUpdateWusdPriceOnly = () => { + if (!address || !vaultInfo) return + const newWusdPrice = parseUnits(priceUpdateForm.wusdPrice, 18) + const currentYtPrice = (vaultInfo as any)[6] as bigint // 保持当前 ytPrice (30位精度) + recordTx('test', priceUpdateForm.wusdPrice, 'UpdateWusdPrice') + writeContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'updateVaultPrices', + args: [selectedVault.address as `0x${string}`, newWusdPrice, currentYtPrice], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + + // 单独更新 ytPrice (保持 wusdPrice 不变) + const handleUpdateYtPriceOnly = () => { + if (!address || !vaultInfo) return + const currentWusdPrice = (vaultInfo as any)[5] as bigint // 保持当前 wusdPrice (18位精度) + const newYtPrice = parseUnits(priceUpdateForm.ytPrice, 30) // ytPrice 是30位精度 + recordTx('test', priceUpdateForm.ytPrice, 'UpdateYtPrice') + writeContract({ + address: CONTRACTS.FACTORY, + abi: FACTORY_ABI, + functionName: 'updateVaultPrices', + args: [selectedVault.address as `0x${string}`, currentWusdPrice, newYtPrice], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + // 设置硬顶 (Factory.setHardCap) const handleSetHardCap = () => { if (!address || !hardCapForm) return @@ -431,6 +650,25 @@ export function VaultPanel() { }) } + // ===== 排队退出 - 批量处理功能 ===== + + // 批量处理退出请求 (Manager/Factory Owner 调用) + const handleProcessBatchWithdrawals = () => { + if (!address || !batchSize) return + const size = parseInt(batchSize, 10) + if (isNaN(size) || size <= 0) return + recordTx('test', batchSize, 'ProcessBatch') + writeContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'processBatchWithdrawals', + args: [BigInt(size)], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + // ===== Manager 资产管理功能 ===== // Manager 授权 WUSD 给 Vault (用于 depositManagedAssets) @@ -484,6 +722,26 @@ export function VaultPanel() { }) } + // YT 代币转账 + const handleTransferYt = () => { + if (!address || !transferYtTo || !transferYtAmount) return + // 验证地址格式 + if (!transferYtTo.startsWith('0x') || transferYtTo.length !== 42) { + showToast('error', t('vault.invalidAddress')) + return + } + recordTx('transfer', transferYtAmount, vaultSymbol || 'YT') + writeContract({ + address: selectedVault.address as `0x${string}`, + abi: VAULT_ABI, + functionName: 'transfer', + args: [transferYtTo as `0x${string}`, parseUnits(transferYtAmount, 18)], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } + // 边界测试是否正在运行 const [isTestRunning, setIsTestRunning] = useState(false) const testTypeRef = useRef(null) @@ -630,6 +888,22 @@ export function VaultPanel() { return parts.join(' ') } + // 同步合约值到倒计时状态,并启动定时器 + useEffect(() => { + if (timeUntilRedeem !== undefined) { + setCountdown(Number(timeUntilRedeem)) + } + }, [timeUntilRedeem]) + + // 倒计时定时器 + useEffect(() => { + if (countdown <= 0) return + const timer = setInterval(() => { + setCountdown(prev => Math.max(0, prev - 1)) + }, 1000) + return () => clearInterval(timer) + }, [countdown > 0]) + // 计算按钮是否应该禁用 - 排除错误状态 const isProcessing = (isPending || isConfirming) && writeStatus !== 'error' @@ -668,48 +942,68 @@ export function VaultPanel() {

{vaultName || selectedVault.name} ({vaultSymbol || '-'})

- {t('vault.totalAssets')} + {t('vault.totalAssets')} {vaultInfo ? formatUnits(vaultInfo[0], 18) : '0'}
- {t('vault.idleAssets')} + {t('vault.idleAssets')} {vaultInfo ? formatUnits(vaultInfo[1], 18) : '0'}
- {t('vault.totalSupply')} + {t('vault.totalSupply')} {vaultInfo ? formatUnits(vaultInfo[3], 18) : '0'}
- {t('vault.hardCap')} + {t('vault.hardCap')} {vaultInfo ? formatUnits(vaultInfo[4], 18) : '0'}
- {t('vault.wusdPrice')} - {vaultInfo ? formatUnits(vaultInfo[5], 18) : '0'} + {t('vault.wusdPrice')} + {vaultInfo ? formatUnits(vaultInfo[5], 30) : '0'}
- {t('vault.ytPrice')} - {vaultInfo ? formatUnits(vaultInfo[6], 18) : '0'} + {t('vault.ytPrice')} + {vaultInfo ? formatUnits(vaultInfo[6], 30) : '0'}
+ {/* 合约信息 */} +
+
+
{t('vault.vaultAddress')}
+ {selectedVault.address} +
+
+
{t('vault.factoryAddress')}
+ {vaultFactory || '-'} +
+
+
{t('vault.wusdContract')}
+ {vaultWusdAddress || '-'} +
+
+
{t('vault.pricePrecision')}
+ {pricePrecision ? pricePrecision.toString() : '-'} +
+
+ {/* 角色信息 */}
- Factory Owner: + Factory Owner: {factoryOwner || '-'}
- Vault Manager: + Vault Manager: {vaultManager || '-'}
- {t('test.role')}: + {t('test.role')}:
- {t('vault.yourWusdBalance')}: + {t('vault.yourWusdBalance')}: {wusdBalance ? formatUnits(wusdBalance, 18) : '0'}
- {t('vault.yourYtBalance')} ({selectedVault.name}): + {t('vault.yourYtBalance')} ({selectedVault.name}): {ytBalance ? formatUnits(ytBalance, 18) : '0'}
@@ -744,6 +1038,12 @@ export function VaultPanel() { > {t('vault.sellYt')} +
{activeTab === 'buy' && ( @@ -759,14 +1059,28 @@ export function VaultPanel() { />
{previewBuyAmount && buyAmount && ( -
- {t('vault.youWillReceive')}: {formatUnits(previewBuyAmount, 18)} YT +
+ {t('vault.youWillReceive')}: + {formatUnits(previewBuyAmount, 18)} YT
)} - {/* Debug: 显示授权状态 */} -
- 授权额度: {wusdAllowance !== undefined ? formatUnits(wusdAllowance, 18) : 'loading...'} WUSD - {buyAmount && ` | 需要: ${buyAmount} | 需授权: ${needsApproval ? '是' : '否'}`} + {/* Debug: 显示预估计算详情 */} +
+
授权额度: {wusdAllowance !== undefined ? formatUnits(wusdAllowance, 18) : 'loading...'} WUSD + {buyAmount && ` | 需要: ${buyAmount} | 需授权: ${needsApproval ? '是' : '否'}`} +
+ {previewBuyAmount && ( +
+ previewBuy 原始值: {previewBuyAmount.toString()} + {vaultInfo && ( + <> +
ytPrice: {(vaultInfo[6] as bigint).toString()} (显示: {formatUnits(vaultInfo[6] as bigint, 30)}) +
wusdPrice: {(vaultInfo[5] as bigint).toString()} (显示: {formatUnits(vaultInfo[5] as bigint, 30)}) + {pricePrecision && <>
PRICE_PRECISION: {pricePrecision.toString()}} + + )} +
+ )}
{needsApproval ? ( + ) : ( + + )} +
+
+ )} +
+ )} + {activeTab === 'sell' && (
+ {/* 排队退出说明 */} +
+ {t('vault.queueWithdrawDesc')} +
+ + {/* 队列进度信息 */} +
+
+
+ {t('vault.queueTotal')}: + {queueProgress ? (queueProgress as any)[1]?.toString() : '0'} +
+
+ {t('vault.queuePending')}: + {queueProgress ? (queueProgress as any)[2]?.toString() : '0'} +
+
+ {t('vault.queueProcessed')}: + {queueProgress ? (queueProgress as any)[0]?.toString() : '0'} +
+
+
+ {/* 赎回状态提示 */}
{t('vault.redeemStatus')}: {canRedeem ? t('vault.redeemAvailable') : t('vault.redeemNotAvailable')} - {!canRedeem && timeUntilRedeem && ( + {!canRedeem && countdown > 0 && ( - ({t('vault.timeRemaining')}: {formatTimeRemaining(Number(timeUntilRedeem))}) + ({t('vault.timeRemaining')}: {formatTimeRemaining(countdown)}) )}
@@ -813,8 +1247,15 @@ export function VaultPanel() { />
{previewSellAmount && sellAmount && ( -
- {t('vault.youWillReceive')}: {formatUnits(previewSellAmount, 18)} WUSD +
+ {t('vault.youWillReceive')}: + {formatUnits(previewSellAmount, 18)} WUSD +
+ )} + {/* Debug: 显示预估计算详情 */} + {previewSellAmount && ( +
+ previewSell: {previewSellAmount.toString()}
)} + + {/* 用户待处理请求列表 */} + {userPendingRequests && (userPendingRequests as any[]).length > 0 && ( +
+

{t('vault.yourPendingRequests')}

+
+ + + + + + + + + + + {(userPendingRequests as any[]).map((req, idx) => ( + + + + + + + ))} + +
YT {t('common.amount')}WUSD {t('common.amount')}{t('vault.requestTime')}{t('vault.status')}
{formatUnits(req.ytAmount, 18)}{formatUnits(req.wusdAmount, 18)}{new Date(Number(req.requestTime) * 1000).toLocaleString()} + + {req.processed ? t('vault.processed') : t('vault.pending')} + +
+
+
+ )} +
+ )} + + {activeTab === 'transfer' && ( +
+
+ + setTransferYtTo(e.target.value)} + placeholder="0x..." + className="input" + /> +
+
+ + setTransferYtAmount(e.target.value)} + placeholder={t('vault.enterYtAmount')} + className="input" + /> +
+
+ {t('vault.yourYtBalance')}: {ytBalance ? formatUnits(ytBalance, 18) : '0'} {selectedVault.name} +
+
)} @@ -833,7 +1348,7 @@ export function VaultPanel() { style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }} onClick={() => setShowAdminConfig(!showAdminConfig)} > -

{t('vault.adminConfig')}

+

{t('vault.adminConfig')}

{showAdminConfig ? '▼' : '▶'}
@@ -848,39 +1363,70 @@ export function VaultPanel() {
{/* 更新价格 */} -

{t('vault.updatePrices')}

-
-
- - setPriceUpdateForm({ ...priceUpdateForm, wusdPrice: e.target.value })} - placeholder="1" - className="input" - step="0.01" - /> +

{t('vault.updatePrices')}

+
+ {/* WUSD 价格 */} +
+ +
+ setPriceUpdateForm({ ...priceUpdateForm, wusdPrice: e.target.value })} + placeholder="1" + className="input" + style={{ fontSize: '13px', flex: 1 }} + step="0.01" + /> + +
+

+ 当前: {vaultInfo ? formatUnits((vaultInfo as any)[5], 18) : '-'} (18位精度) +

-
- - setPriceUpdateForm({ ...priceUpdateForm, ytPrice: e.target.value })} - placeholder="1" - className="input" - step="0.01" - /> -
-
- + {/* YT 价格 */} +
+ +
+ setPriceUpdateForm({ ...priceUpdateForm, ytPrice: e.target.value })} + placeholder="1" + className="input" + style={{ fontSize: '13px', flex: 1 }} + step="0.01" + /> + +
+

+ 当前: {vaultInfo ? formatUnits((vaultInfo as any)[6], 30) : '-'} (30位精度) +

+ {/* 同时更新两个价格 */} +
+ +
{/* 设置硬顶 */} -

{t('vault.setHardCap')}

+

{t('vault.setHardCap')}

@@ -900,7 +1446,7 @@ export function VaultPanel() {
{/* 设置赎回时间 */} -

{t('vault.setRedemptionTime')}

+

{t('vault.setRedemptionTime')}

@@ -919,8 +1465,8 @@ export function VaultPanel() {
{/* 设置管理员 */} -

{t('vault.setManager')}

-
+

{t('vault.setManager')}

+
+ + {/* 批量处理退出请求 */} +

{t('vault.processBatchWithdrawals')}

+
+ {/* 队列进度信息 */} +
+
+ {t('vault.queueTotal')}: + {queueProgress ? (queueProgress as any)[1]?.toString() : '0'} +
+
+ {t('vault.queuePending')}: + {queueProgress ? (queueProgress as any)[2]?.toString() : '0'} +
+
+ {t('vault.queueProcessed')}: + {queueProgress ? (queueProgress as any)[0]?.toString() : '0'} +
+
+
+
+ + setBatchSize(e.target.value)} + placeholder="10" + className="input" + min="1" + max="100" + /> +
+
+ +
+
+

+ {t('vault.processBatchHint')} +

+
)}
@@ -947,7 +1539,7 @@ export function VaultPanel() { style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }} onClick={() => setShowManagerPanel(!showManagerPanel)} > -

{t('vault.managerPanel')}

+

{t('vault.managerPanel')}

{showManagerPanel ? '▼' : '▶'}
@@ -957,7 +1549,7 @@ export function VaultPanel() {
{t('vault.managedAssets')}: - {managedAssets ? formatUnits(managedAssets, 18) : '0'} WUSD + {vaultInfo ? formatUnits(vaultInfo[2], 18) : '0'} WUSD
{t('vault.idleAssets')}: @@ -971,7 +1563,7 @@ export function VaultPanel() {
{/* 存入托管资产 */} -

{t('vault.depositManagedAssets')}

+

{t('vault.depositManagedAssets')}

{/* 提取托管资产 */} -

{t('vault.withdrawForManagement')}

+

{t('vault.withdrawForManagement')}

setShowBoundaryTest(!showBoundaryTest)} > -

{t('test.boundaryTests')}

+

{t('test.boundaryTests')}

{showBoundaryTest ? '▼' : '▶'}
@@ -1060,7 +1652,7 @@ export function VaultPanel() { {canRedeem ? t('test.yes') : t('test.no')}
{t('test.timeToRedeem')}: - {timeUntilRedeem ? `${Number(timeUntilRedeem)}s` : '0s'} + {countdown > 0 ? formatTimeRemaining(countdown) : '0s'}
diff --git a/frontend/src/components/WUSDPanel.tsx b/frontend/src/components/WUSDPanel.tsx index b974833..b3ea2bf 100644 --- a/frontend/src/components/WUSDPanel.tsx +++ b/frontend/src/components/WUSDPanel.tsx @@ -15,6 +15,8 @@ export function WUSDPanel() { 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) @@ -37,6 +39,24 @@ export function WUSDPanel() { 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({ @@ -81,7 +101,11 @@ export function WUSDPanel() { pendingTxRef.current = null } refetchBalance() + refetchTotalSupply() setMintAmount('') + setBurnAmount('') + setTransferTo('') + setTransferAmount('') } }, [isSuccess]) @@ -182,6 +206,25 @@ export function WUSDPanel() { }) } + 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 @@ -237,24 +280,54 @@ export function WUSDPanel() { return (

{t('wusd.title')}

-
- {t('common.contract')}: - {CONTRACTS.WUSD} + + {/* 代币信息卡片 */} +
+
+
名称
+ {name || 'WUSD'} +
+
+
符号
+ {symbol || 'WUSD'} +
+
+
精度
+ {decimals?.toString() || '18'} +
+
+
{t('common.contract')}
+ {CONTRACTS.WUSD} +
+
+
{t('wusd.owner')}
+ {owner ? `${(owner as string).slice(0, 10)}...${(owner as string).slice(-8)}` : '-'} +
+
+
{t('wusd.totalSupply')}
+ + {totalSupply !== undefined && decimals !== undefined + ? Number(formatUnits(totalSupply, decimals)).toLocaleString() + : '0'} + +
-
- {t('common.balance')}: - + + {/* 用户余额 */} +
+
{t('common.balance')}
+ {balance !== undefined && decimals !== undefined - ? formatUnits(balance, decimals) + ? Number(formatUnits(balance, decimals)).toLocaleString() : '0'} {symbol || 'WUSD'}
-
- {/* 铸造 */} -
-
- + {/* 铸造和销毁 */} +
+
+
+
- {/* 销毁 */} -
-
- +
+
+
+ {/* 转账功能 */} +
+

{t('wusd.transfer')}

+
+
+
+ + setTransferTo(e.target.value)} + placeholder="0x..." + className="input" + style={{ fontSize: '12px' }} + /> +
+
+ + setTransferAmount(e.target.value)} + placeholder={t('wusd.enterAmount')} + className="input" + /> +
+
+ +
+
+ {/* 边界测试区域 */}
setShowBoundaryTest(!showBoundaryTest)} > -

{t('test.boundaryTests')}

+

{t('test.boundaryTests')}

{showBoundaryTest ? '▼' : '▶'}
diff --git a/frontend/src/config/contracts.ts b/frontend/src/config/contracts.ts index 04eed60..c135626 100644 --- a/frontend/src/config/contracts.ts +++ b/frontend/src/config/contracts.ts @@ -1,6 +1,5 @@ // Gas配置 - 用于解决Arbitrum测试网gas预估问题 // YT Asset Vault ABI - 用于读取 YT 代币本身的价格 -// 注意: YT代币需要实现 assetPrice() 接口(30位精度),供 YTPriceFeed 读取 export const YT_ASSET_VAULT_ABI = [ { inputs: [], @@ -15,14 +14,6 @@ export const YT_ASSET_VAULT_ABI = [ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' - }, - // YTPriceFeed 期望的接口:assetPrice() - 30位精度 - { - inputs: [], - name: 'assetPrice', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function' } ] as const @@ -43,21 +34,23 @@ export const GAS_CONFIG = { } export const CONTRACTS = { - FACTORY: '0x6DaB73519DbaFf23F36FEd24110e2ef5Cfc8aAC9' as const, - WUSD: '0x939cf46F7A4d05da2a37213E7379a8b04528F590' as const, + // 12月18日新部署的合约地址 + FACTORY: '0x982716f32F10BCB5B5944c1473a8992354bF632b' as const, + WUSD: '0x6d2bf81a631dFE19B2f348aE92cF6Ef41ca2DF98' as const, // fork测试用 VAULTS: { + // 旧金库地址(需要通过Factory.getAllVaults()动态获取新金库) YT_A: '0x0cA35994F033685E7a57ef9bc5d00dd3cf927330' as const, YT_B: '0x333805C9EE75f59Aa2Cc79DfDe2499F920c7b408' as const, YT_C: '0x6DF0ED6f0345F601A206974973dE9fC970598587' as const, YT_D: '0x5d91FD16fa85547b0784c377A47BF7706D7875d3' as const, }, - // LP Pool contracts - YT_REWARD_ROUTER: '0x51eEF57eC57c867AC23945f0ce21aA5A9a2C246c' as const, - YT_LP_TOKEN: '0x1b96F219E8aeE557DD8bD905a6c72cc64eA5BD7B' as const, - YT_POOL_MANAGER: '0x14246886a1E1202cb6b5a2db793eF3359d536302' as const, - YT_VAULT: '0x19982e5145ca5401A1084c0BF916c0E0bB343Af9' as const, - USDY: '0x631Bd6834C50f6d2B07035c9253b4a19132E888c' as const, - YT_PRICE_FEED: '0x0f2d930EE73972132E3a36b7eD6F709Af6E5B879' as const, + // LP Pool contracts - 12月18日新部署 + YT_REWARD_ROUTER: '0x953758c02ec49F1f67fE2a8E3F67C434FeC5aB9d' as const, + YT_LP_TOKEN: '0xf5206D958f692556603806A8f65bB106E23d1776' as const, + YT_POOL_MANAGER: '0xe3068a25D6eEda551Cd12CC291813A4fe0e4AbB6' as const, + YT_VAULT: '0xbC2e4f06601B92B3F430962a8f0a7E8c378ce54e' as const, + USDY: '0x54551451E14D3d3418e4Aa9F31e9E8573fd37053' as const, + YT_PRICE_FEED: '0x9364D3aF669886883C26EC0ff32000719491452A' as const, } export const FACTORY_ABI = [ @@ -265,6 +258,89 @@ export const FACTORY_ABI = [ outputs: [], stateMutability: 'nonpayable', type: 'function' + }, + // 批量创建金库 + { + inputs: [ + { internalType: 'string[]', name: '_names', type: 'string[]' }, + { internalType: 'string[]', name: '_symbols', type: 'string[]' }, + { internalType: 'address[]', name: '_managers', type: 'address[]' }, + { internalType: 'uint256[]', name: '_hardCaps', type: 'uint256[]' }, + { internalType: 'address', name: '_wusd', type: 'address' }, + { internalType: 'uint256[]', name: '_redemptionTimes', type: 'uint256[]' }, + { internalType: 'uint256[]', name: '_initialWusdPrices', type: 'uint256[]' }, + { internalType: 'uint256[]', name: '_initialYtPrices', type: 'uint256[]' } + ], + name: 'createVaultBatch', + outputs: [{ internalType: 'address[]', name: 'vaults', type: 'address[]' }], + stateMutability: 'nonpayable', + type: 'function' + }, + // 暂停金库 + { + inputs: [{ internalType: 'address', name: '_vault', type: 'address' }], + name: 'pauseVault', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // 批量暂停金库 + { + inputs: [{ internalType: 'address[]', name: '_vaults', type: 'address[]' }], + name: 'pauseVaultBatch', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // 恢复金库 + { + inputs: [{ internalType: 'address', name: '_vault', type: 'address' }], + name: 'unpauseVault', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // 批量恢复金库 + { + inputs: [{ internalType: 'address[]', name: '_vaults', type: 'address[]' }], + name: 'unpauseVaultBatch', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // 设置默认硬顶 + { + inputs: [{ internalType: 'uint256', name: '_defaultHardCap', type: 'uint256' }], + name: 'setDefaultHardCap', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // 读取默认硬顶 + { + inputs: [], + name: 'defaultHardCap', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + // 获取金库详情(扩展) + { + inputs: [{ internalType: 'address', name: '_vault', type: 'address' }], + name: 'getVaultInfo', + outputs: [ + { internalType: 'bool', name: 'exists', type: 'bool' }, + { internalType: 'uint256', name: 'totalAssets', type: 'uint256' }, + { internalType: 'uint256', name: 'idleAssets', type: 'uint256' }, + { internalType: 'uint256', name: 'managedAssets', type: 'uint256' }, + { internalType: 'uint256', name: 'totalSupply', type: 'uint256' }, + { internalType: 'uint256', name: 'hardCap', type: 'uint256' }, + { internalType: 'uint256', name: 'wusdPrice', type: 'uint256' }, + { internalType: 'uint256', name: 'ytPrice', type: 'uint256' }, + { internalType: 'uint256', name: 'nextRedemptionTime', type: 'uint256' } + ], + stateMutability: 'view', + type: 'function' } ] as const @@ -452,13 +528,122 @@ export const VAULT_ABI = [ stateMutability: 'nonpayable', type: 'function' }, + // 用户提交退出请求(排队机制),返回 requestId { inputs: [{ internalType: 'uint256', name: '_ytAmount', type: 'uint256' }], name: 'withdrawYT', - outputs: [{ internalType: 'uint256', name: 'wusdAmount', type: 'uint256' }], + outputs: [{ internalType: 'uint256', name: 'requestId', type: 'uint256' }], stateMutability: 'nonpayable', type: 'function' }, + // 获取用户待处理的退出请求列表 + { + inputs: [{ internalType: 'address', name: '_user', type: 'address' }], + name: 'getUserPendingRequests', + outputs: [ + { + components: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'uint256', name: 'ytAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'wusdAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'requestTime', type: 'uint256' }, + { internalType: 'bool', name: 'processed', type: 'bool' } + ], + internalType: 'struct YTAssetVault.WithdrawRequest[]', + name: 'pendingRequests', + type: 'tuple[]' + } + ], + stateMutability: 'view', + type: 'function' + }, + // 获取用户的所有请求ID + { + inputs: [{ internalType: 'address', name: '_user', type: 'address' }], + name: 'getUserRequestIds', + outputs: [{ internalType: 'uint256[]', name: '', type: 'uint256[]' }], + stateMutability: 'view', + type: 'function' + }, + // 获取单个请求详情 + { + inputs: [{ internalType: 'uint256', name: '_requestId', type: 'uint256' }], + name: 'getRequestDetails', + outputs: [ + { + components: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'uint256', name: 'ytAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'wusdAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'requestTime', type: 'uint256' }, + { internalType: 'bool', name: 'processed', type: 'bool' } + ], + internalType: 'struct YTAssetVault.WithdrawRequest', + name: 'request', + type: 'tuple' + } + ], + stateMutability: 'view', + type: 'function' + }, + // 获取待处理请求数量 + { + inputs: [], + name: 'getPendingRequestsCount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + // 获取队列进度 + { + inputs: [], + name: 'getQueueProgress', + outputs: [ + { internalType: 'uint256', name: 'currentIndex', type: 'uint256' }, + { internalType: 'uint256', name: 'totalRequests', type: 'uint256' }, + { internalType: 'uint256', name: 'pendingRequests', type: 'uint256' } + ], + stateMutability: 'view', + type: 'function' + }, + // 待处理请求数量(状态变量) + { + inputs: [], + name: 'pendingRequestsCount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + // 请求ID计数器 + { + inputs: [], + name: 'requestIdCounter', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + // 批量处理退出请求(Manager/Factory 调用) + { + inputs: [{ internalType: 'uint256', name: '_batchSize', type: 'uint256' }], + name: 'processBatchWithdrawals', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // 请求映射 + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'withdrawRequests', + outputs: [ + { internalType: 'address', name: 'user', type: 'address' }, + { internalType: 'uint256', name: 'ytAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'wusdAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'requestTime', type: 'uint256' }, + { internalType: 'bool', name: 'processed', type: 'bool' } + ], + stateMutability: 'view', + type: 'function' + }, { inputs: [], name: 'wusdAddress', @@ -527,6 +712,14 @@ export const VAULT_ABI = [ outputs: [], stateMutability: 'nonpayable', type: 'function' + }, + // 读取暂停状态 + { + inputs: [], + name: 'paused', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function' } ] as const @@ -1134,10 +1327,10 @@ export const YT_VAULT_ABI = [ stateMutability: 'view', type: 'function' }, - // swap 开关 + // swap 开关 (合约实际使用 isSwapEnabled) { inputs: [], - name: 'swapEnabled', + name: 'isSwapEnabled', outputs: [{ internalType: 'bool', name: '', type: 'bool' }], stateMutability: 'view', type: 'function' @@ -1167,3 +1360,94 @@ export const YT_VAULT_ABI = [ type: 'function' } ] as const + +// USDY 合约 ABI (内部计价代币) +export const USDY_ABI = [ + // 标准 ERC20 函数 + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + // Owner + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function' + }, + // Vault 白名单管理 + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'vaults', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: '_vault', type: 'address' }], + name: 'addVault', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [{ internalType: 'address', name: '_vault', type: 'address' }], + name: 'removeVault', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + // mint/burn (仅白名单 Vault 可调用) + { + inputs: [ + { internalType: 'address', name: '_account', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' } + ], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { internalType: 'address', name: '_account', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' } + ], + name: 'burn', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + } +] as const diff --git a/frontend/src/hooks/useTransactionHistory.ts b/frontend/src/hooks/useTransactionHistory.ts index c31cacc..d4ca670 100644 --- a/frontend/src/hooks/useTransactionHistory.ts +++ b/frontend/src/hooks/useTransactionHistory.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' -export type TransactionType = 'mint' | 'burn' | 'buy' | 'sell' | 'approve' | 'create_vault' | 'update_price' | 'test' | 'addLiquidity' | 'removeLiquidity' | 'swap' +export type TransactionType = 'mint' | 'burn' | 'buy' | 'sell' | 'approve' | 'create_vault' | 'update_price' | 'test' | 'addLiquidity' | 'removeLiquidity' | 'swap' | 'transfer' export interface TransactionRecord { id: string diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index bdf9cc7..d7150c7 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -39,7 +39,13 @@ "burn": "Burn", "minting": "Minting...", "confirming": "Confirming...", - "mintSuccess": "Mint successful!" + "mintSuccess": "Mint successful!", + "owner": "Contract Owner", + "totalSupply": "Total Supply", + "transfer": "Transfer", + "toAddress": "To Address", + "transferAmount": "Transfer Amount", + "invalidAddress": "Invalid address format" }, "vault": { "title": "Vault Trading", @@ -87,7 +93,28 @@ "toAddress": "To Address", "defaultSelf": "default: self", "approvedAmount": "Approved Amount", - "needApprove": "Need Approve" + "needApprove": "Need Approve", + "vaultAddress": "Vault Address", + "factoryAddress": "Factory Address", + "wusdContract": "WUSD 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.", + "queueTotal": "Total Requests", + "queuePending": "Pending", + "queueProcessed": "Processed", + "requestWithdraw": "Request Withdraw", + "yourPendingRequests": "Your Pending Requests", + "requestTime": "Request Time", + "status": "Status", + "processed": "Processed", + "pending": "Pending", + "processBatchWithdrawals": "Batch Process Withdrawals", + "batchSize": "Batch Size", + "processBatch": "Process Batch", + "processBatchHint": "Process withdrawal requests from queue in order, sending WUSD to users" }, "factory": { "title": "Factory Management", @@ -134,7 +161,18 @@ "upgrade": "Upgrade", "batchUpgradeVault": "Batch Upgrade Vaults", "batchUpgrade": "Batch Upgrade", - "selectInBatchSection": "select in batch section" + "selectInBatchSection": "select in batch section", + "batchPauseUnpause": "Batch Pause/Unpause Vaults", + "pauseSelected": "Pause Selected", + "unpauseSelected": "Unpause Selected", + "pauseWarning": "Warning: Pausing a vault will block all buy/sell operations", + "pauseVault": "Pause", + "unpauseVault": "Unpause", + "paused": "Paused", + "active": "Active", + "batchCreateVaults": "Batch Create Vaults", + "addVaultRow": "Add Vault", + "createBatch": "Create Batch" }, "language": { "en": "English", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 4752e37..ad8553c 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -39,7 +39,13 @@ "burn": "销毁", "minting": "铸造中...", "confirming": "确认中...", - "mintSuccess": "铸造成功!" + "mintSuccess": "铸造成功!", + "owner": "合约所有者", + "totalSupply": "总供应量", + "transfer": "转账", + "toAddress": "接收地址", + "transferAmount": "转账数量", + "invalidAddress": "无效的地址格式" }, "vault": { "title": "金库交易", @@ -87,7 +93,28 @@ "toAddress": "接收地址", "defaultSelf": "默认为自己", "approvedAmount": "已授权额度", - "needApprove": "需要授权" + "needApprove": "需要授权", + "vaultAddress": "金库地址", + "factoryAddress": "工厂地址", + "wusdContract": "WUSD 合约", + "pricePrecision": "价格精度", + "transferYt": "转账 YT", + "transfer": "转账", + "invalidAddress": "无效的地址格式", + "queueWithdrawDesc": "卖出 YT 将创建退出请求并加入队列,等待管理员处理后发放 WUSD。", + "queueTotal": "总请求数", + "queuePending": "待处理", + "queueProcessed": "已处理", + "requestWithdraw": "申请退出", + "yourPendingRequests": "你的待处理请求", + "requestTime": "请求时间", + "status": "状态", + "processed": "已处理", + "pending": "等待中", + "processBatchWithdrawals": "批量处理退出", + "batchSize": "批量大小", + "processBatch": "处理退出", + "processBatchHint": "从队列中按顺序处理指定数量的退出请求,向用户发送 WUSD" }, "factory": { "title": "工厂管理", @@ -134,7 +161,18 @@ "upgrade": "升级", "batchUpgradeVault": "批量升级金库", "batchUpgrade": "批量升级", - "selectInBatchSection": "在批量操作区选择" + "selectInBatchSection": "在批量操作区选择", + "batchPauseUnpause": "批量暂停/恢复金库", + "pauseSelected": "暂停所选金库", + "unpauseSelected": "恢复所选金库", + "pauseWarning": "警告: 暂停金库将阻止所有买入/卖出操作", + "pauseVault": "暂停", + "unpauseVault": "恢复", + "paused": "已暂停", + "active": "运行中", + "batchCreateVaults": "批量创建金库", + "addVaultRow": "添加金库", + "createBatch": "批量创建" }, "language": { "en": "英文",