feat: 添加排队退出机制和样式优化

新功能 - 排队退出机制:
- 用户卖出 YT 改为申请退出,创建请求加入队列
- 显示队列进度信息(总请求数/待处理/已处理)
- 显示用户待处理请求列表
- 管理员配置区域添加批量处理退出功能

合约 ABI 更新:
- withdrawYT 返回 requestId
- getUserPendingRequests 获取用户待处理请求
- getQueueProgress 获取队列进度
- processBatchWithdrawals 批量处理退出

样式优化:
- LP 流动池页面彩色背景改为白灰色
- 金库交易页面硬顶 Swap 区域样式统一
- 硬顶判断条件修改为使用 totalSupply

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-19 18:01:08 +00:00
parent 79aceccec3
commit c8427caa01
11 changed files with 2565 additions and 286 deletions

6
frontend/.gitignore vendored
View File

@@ -22,3 +22,9 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Documentation
*.md
# Test scripts
*.mjs

View File

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

View File

@@ -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<string, boolean> = {}
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() {
<div className="factory-info">
<div className="info-row">
<span>{t('factory.factoryContract')}:</span>
<span title="工厂合约 Factory Contract - 负责创建和管理所有 YT 金库的工厂合约地址" style={{ cursor: 'help' }}>{t('factory.factoryContract')}:</span>
<code>{CONTRACTS.FACTORY}</code>
</div>
<div className="info-row">
<span>{t('factory.owner')}:</span>
<span title="所有者 Owner - 工厂合约的所有者,拥有创建金库、更新价格等最高权限" style={{ cursor: 'help' }}>{t('factory.owner')}:</span>
<code>{owner || '-'}</code>
</div>
<div className="info-row">
<span>{t('factory.vaultImpl')}:</span>
<span title="金库实现 Vault Implementation - 金库代理合约的实现地址,用于可升级架构" style={{ cursor: 'help' }}>{t('factory.vaultImpl')}:</span>
<code style={{ fontSize: '10px' }}>{vaultImplementation || '-'}</code>
</div>
<div className="info-row">
<span>{t('factory.defaultHardCap')}:</span>
<span title="默认硬顶 Default Hard Cap - 新创建金库的默认最大存款额度" style={{ cursor: 'help' }}>{t('factory.defaultHardCap')}:</span>
<strong>{defaultHardCap ? formatUnits(defaultHardCap, 18) : '0'}</strong>
</div>
<div className="info-row">
<span>{t('factory.totalVaults')}:</span>
<span title="金库总数 Total Vaults - 工厂已创建的金库数量" style={{ cursor: 'help' }}>{t('factory.totalVaults')}:</span>
<strong>{vaultCount?.toString() || '0'}</strong>
</div>
<div className="info-row">
<span>{t('factory.yourRole')}:</span>
<span title="你的角色 Your Role - 当前连接钱包在工厂合约中的角色" style={{ cursor: 'help' }}>{t('factory.yourRole')}:</span>
<strong>{isOwner ? t('factory.roleOwner') : t('factory.roleUser')}</strong>
</div>
</div>
<div className="section">
<h3>{t('factory.allVaults')}</h3>
<h3 title="所有金库 All Vaults - 工厂创建的所有 YT 资产金库列表" style={{ cursor: 'help' }}>{t('factory.allVaults')}</h3>
<div className="vault-list">
{allVaults && allVaults.length > 0 ? (
allVaults.map((vault, index) => (
<div key={index} className="vault-item">
<span>Vault {index + 1}:</span>
<code>{vault}</code>
</div>
))
allVaults.map((vault, index) => {
const isPaused = vaultPausedMap[vault.toLowerCase()] || false
return (
<div key={index} className="vault-item" style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<span>Vault {index + 1}:</span>
<code style={{ flex: 1 }}>{vault}</code>
{/* 暂停状态标签 */}
<span style={{
padding: '2px 8px',
borderRadius: '4px',
fontSize: '11px',
background: isPaused ? '#ffebee' : '#e8f5e9',
color: isPaused ? '#c62828' : '#2e7d32',
fontWeight: 'bold'
}}>
{isPaused ? t('factory.paused') : t('factory.active')}
</span>
{isOwner && (
<button
onClick={() => isPaused ? handleUnpauseVault(vault) : handlePauseVault(vault)}
disabled={isProcessing}
className="btn btn-sm"
style={{
padding: '2px 10px',
fontSize: '11px',
background: isPaused ? '#4caf50' : '#ff9800',
color: '#fff',
border: 'none',
borderRadius: '4px'
}}
title={isPaused ? t('factory.unpauseVault') : t('factory.pauseVault')}
>
{isPaused ? t('factory.unpauseVault') : t('factory.pauseVault')}
</button>
)}
</div>
)
})
) : (
<p className="text-muted">{t('factory.noVaults')}</p>
)}
@@ -342,7 +522,7 @@ export function FactoryPanel() {
{isOwner && (
<>
<div className="section">
<h3>{t('factory.createVault')}</h3>
<h3 title="创建金库 Create Vault - 部署新的 YT 资产金库合约" style={{ cursor: 'help' }}>{t('factory.createVault')}</h3>
<div className="form-grid">
<div className="form-group">
<label>{t('factory.name')}</label>
@@ -421,13 +601,142 @@ export function FactoryPanel() {
>
{isProcessing ? t('common.processing') : t('factory.create')}
</button>
{/* 批量创建金库 - 可折叠 */}
<div style={{ marginTop: '16px', padding: '12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowBatchCreate(!showBatchCreate)}
>
<h4 style={{ margin: 0, fontSize: '13px', color: '#666' }}>{t('factory.batchCreateVaults')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showBatchCreate ? '▼' : '▶'}</span>
</div>
{showBatchCreate && (
<div style={{ marginTop: '12px' }}>
{batchCreateVaults.map((vault, index) => (
<div key={index} style={{ marginBottom: '12px', padding: '10px', background: '#fff', borderRadius: '6px', border: '1px solid #e0e0e0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<span style={{ fontWeight: 'bold', fontSize: '12px', color: '#333' }}>Vault {index + 1}</span>
{batchCreateVaults.length > 1 && (
<button
onClick={() => removeBatchCreateRow(index)}
style={{ padding: '2px 8px', fontSize: '11px', background: '#f44336', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
>
{t('common.cancel')}
</button>
)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px', marginBottom: '8px' }}>
<div>
<label style={{ fontSize: '10px', color: '#666' }}>{t('factory.name')}</label>
<input
type="text"
value={vault.name}
onChange={(e) => updateBatchCreateRow(index, 'name', e.target.value)}
placeholder="YT-X"
className="input"
style={{ fontSize: '12px' }}
/>
</div>
<div>
<label style={{ fontSize: '10px', color: '#666' }}>{t('factory.symbol')}</label>
<input
type="text"
value={vault.symbol}
onChange={(e) => updateBatchCreateRow(index, 'symbol', e.target.value)}
placeholder="YTX"
className="input"
style={{ fontSize: '12px' }}
/>
</div>
<div>
<label style={{ fontSize: '10px', color: '#666' }}>{t('factory.managerAddress')}</label>
<input
type="text"
value={vault.manager}
onChange={(e) => updateBatchCreateRow(index, 'manager', e.target.value)}
placeholder="0x..."
className="input"
style={{ fontSize: '12px' }}
/>
</div>
<div>
<label style={{ fontSize: '10px', color: '#666' }}>{t('vault.hardCap')}</label>
<input
type="number"
value={vault.hardCap}
onChange={(e) => updateBatchCreateRow(index, 'hardCap', e.target.value)}
placeholder="100000"
className="input"
style={{ fontSize: '12px' }}
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
<div>
<label style={{ fontSize: '10px', color: '#666' }}>{t('factory.redemptionTime')}</label>
<input
type="datetime-local"
value={vault.redemptionTime}
onChange={(e) => updateBatchCreateRow(index, 'redemptionTime', e.target.value)}
className="input"
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
type="number"
value={vault.ytPrice}
onChange={(e) => updateBatchCreateRow(index, 'ytPrice', e.target.value)}
placeholder="1"
className="input"
style={{ fontSize: '12px' }}
/>
</div>
</div>
</div>
))}
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button
onClick={addBatchCreateRow}
className="btn btn-sm"
style={{ background: '#1976d2', color: '#fff', border: 'none' }}
>
+ {t('factory.addVaultRow')}
</button>
<button
onClick={handleBatchCreateVaults}
disabled={isProcessing || batchCreateVaults.filter(v => v.name && v.symbol && v.manager && v.hardCap && v.redemptionTime).length === 0}
className="btn btn-primary btn-sm"
style={{ flex: 1 }}
>
{isProcessing ? t('common.processing') : t('factory.createBatch')} ({batchCreateVaults.filter(v => v.name && v.symbol && v.manager && v.hardCap && v.redemptionTime).length})
</button>
</div>
</div>
)}
</div>
</div>
<div className="section">
<h3>{t('factory.updatePrices')}</h3>
<h3 title="更新价格 Update Prices - 修改指定金库的 WUSD 和 YT 价格" style={{ cursor: 'help' }}>{t('factory.updatePrices')}</h3>
<div className="form-grid">
<div className="form-group">
<label>{t('factory.vaultAddress')}</label>
<label title="金库地址 Vault Address - 选择要更新价格的金库" style={{ cursor: 'help' }}>{t('factory.vaultAddress')}</label>
<select
value={priceForm.vault}
onChange={(e) => setPriceForm({ ...priceForm, vault: e.target.value })}
@@ -479,7 +788,7 @@ export function FactoryPanel() {
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowOwnerConfig(!showOwnerConfig)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('factory.ownerConfig')}</h4>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }} title="Owner配置 Owner Config - 仅限 Factory Owner 使用,设置默认硬顶等全局参数">{t('factory.ownerConfig')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showOwnerConfig ? '▼' : '▶'}</span>
</div>
@@ -518,7 +827,7 @@ export function FactoryPanel() {
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowBatchOps(!showBatchOps)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('factory.batchOperations')}</h4>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }} title="批量操作 Batch Operations - 同时对多个金库执行更新价格、设置硬顶、设置赎回时间等操作">{t('factory.batchOperations')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showBatchOps ? '▼' : '▶'}</span>
</div>
@@ -617,7 +926,7 @@ export function FactoryPanel() {
</div>
{/* 批量设置赎回时间 */}
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.batchSetRedemptionTime')}</h5>
<div style={{ display: 'flex', gap: '8px' }}>
<input
@@ -636,6 +945,32 @@ export function FactoryPanel() {
</button>
</div>
</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={{ 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' }}
>
{isProcessing ? '...' : t('factory.pauseSelected')}
</button>
<button
onClick={handleBatchUnpauseVaults}
disabled={isProcessing || selectedVaultsForBatch.length === 0}
className="btn btn-sm"
style={{ flex: 1, background: '#4caf50', color: '#fff', border: 'none' }}
>
{isProcessing ? '...' : t('factory.unpauseSelected')}
</button>
</div>
<div style={{ fontSize: '11px', color: '#e65100', marginTop: '6px' }}>
{t('factory.pauseWarning')}
</div>
</div>
</div>
)}
</div>
@@ -646,7 +981,7 @@ export function FactoryPanel() {
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowAdvanced(!showAdvanced)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('factory.advancedFunctions')}</h4>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }} title="高级功能 Advanced Functions - 设置金库实现地址、升级金库合约等高级操作(危险)">{t('factory.advancedFunctions')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showAdvanced ? '▼' : '▶'}</span>
</div>
@@ -657,6 +992,37 @@ export function FactoryPanel() {
{t('factory.advancedWarning')}
</div>
{/* 验证金库地址 */}
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }} title="验证地址是否为有效金库 / Verify if address is a valid vault"></h5>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input
type="text"
value={checkVaultAddr}
onChange={(e) => setCheckVaultAddr(e.target.value)}
placeholder="0x... 输入地址"
className="input"
style={{ flex: 1, fontSize: '12px' }}
/>
{checkVaultAddr && checkVaultAddr.length === 42 && (
<span style={{
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold',
background: '#fff',
border: '1px solid #e0e0e0',
color: '#333'
}}>
{isValidVault ? '[OK] 有效金库' : '[NO] 不是金库'}
</span>
)}
</div>
<div style={{ fontSize: '10px', color: '#666', marginTop: '4px' }}>
使 isVault()
</div>
</div>
{/* 设置金库实现 */}
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.setVaultImplementation')}</h5>
@@ -751,7 +1117,7 @@ export function FactoryPanel() {
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowPermissionTest(!showPermissionTest)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('test.permissionTests')}</h4>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }} title="权限测试 Permission Tests - 测试工厂合约的权限控制,验证非 Owner 无法执行管理操作">{t('test.permissionTests')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showPermissionTest ? '▼' : '▶'}</span>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -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<string | null>(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<string, string> = {
@@ -75,13 +87,31 @@ export function TransactionHistory({ transactions, onClear }: Props) {
</div>
<div className="tx-hash">
{tx.hash && typeof tx.hash === 'string' && tx.hash.startsWith('0x') ? (
<a
href={`https://sepolia.arbiscan.io/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
>
{shortenHash(tx.hash)}
</a>
<>
<a
href={`https://sepolia.arbiscan.io/tx/${tx.hash}`}
target="_blank"
rel="noopener noreferrer"
>
{shortenHash(tx.hash)}
</a>
<button
onClick={() => handleCopy(tx.hash, tx.id)}
className="btn-copy"
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '2px 4px',
marginLeft: '4px',
fontSize: '11px',
color: copiedId === tx.id ? '#4caf50' : '#666',
}}
title={t('toast.copySuccess')}
>
{copiedId === tx.id ? 'OK' : 'Copy'}
</button>
</>
) : (
<span className="text-muted">-</span>
)}

View File

@@ -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<number>(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<string | null>(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() {
<h3>{vaultName || selectedVault.name} ({vaultSymbol || '-'})</h3>
<div className="info-grid">
<div className="info-item">
<span>{t('vault.totalAssets')}</span>
<span title="总资产 Total Assets - 金库中存入的 WUSD 总量,包括闲置资产和托管资产" style={{ cursor: 'help' }}>{t('vault.totalAssets')}</span>
<strong>{vaultInfo ? formatUnits(vaultInfo[0], 18) : '0'}</strong>
</div>
<div className="info-item">
<span>{t('vault.idleAssets')}</span>
<span title="闲置资产 Idle Assets - 金库中未被 Manager 提取管理的资产,可用于用户赎回" style={{ cursor: 'help' }}>{t('vault.idleAssets')}</span>
<strong>{vaultInfo ? formatUnits(vaultInfo[1], 18) : '0'}</strong>
</div>
<div className="info-item">
<span>{t('vault.totalSupply')}</span>
<span title="总供应 Total Supply - 已发行的 YT 代币总量,代表用户的份额凭证" style={{ cursor: 'help' }}>{t('vault.totalSupply')}</span>
<strong>{vaultInfo ? formatUnits(vaultInfo[3], 18) : '0'}</strong>
</div>
<div className="info-item">
<span>{t('vault.hardCap')}</span>
<span title="硬顶 Hard Cap - 金库可接受的最大存款额度,达到后无法继续存入" style={{ cursor: 'help' }}>{t('vault.hardCap')}</span>
<strong>{vaultInfo ? formatUnits(vaultInfo[4], 18) : '0'}</strong>
</div>
<div className="info-item">
<span>{t('vault.wusdPrice')}</span>
<strong>{vaultInfo ? formatUnits(vaultInfo[5], 18) : '0'}</strong>
<span title="WUSD价格 WUSD Price - 稳定币 WUSD 的计价,通常为 1.030位精度" style={{ cursor: 'help' }}>{t('vault.wusdPrice')}</span>
<strong>{vaultInfo ? formatUnits(vaultInfo[5], 30) : '0'}</strong>
</div>
<div className="info-item">
<span>{t('vault.ytPrice')}</span>
<strong>{vaultInfo ? formatUnits(vaultInfo[6], 18) : '0'}</strong>
<span title="YT价格 YT Price - YT 代币的价格30位精度用于计算买入/卖出的兑换比例" style={{ cursor: 'help' }}>{t('vault.ytPrice')}</span>
<strong>{vaultInfo ? formatUnits(vaultInfo[6], 30) : '0'}</strong>
</div>
</div>
</div>
{/* 合约信息 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px', fontSize: '12px' }}>
<div style={{ padding: '8px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', marginBottom: '2px', cursor: 'help' }} title="金库地址 Vault Address - 当前选择的 YT 资产金库合约地址">{t('vault.vaultAddress')}</div>
<code style={{ fontSize: '10px', wordBreak: 'break-all' }}>{selectedVault.address}</code>
</div>
<div style={{ padding: '8px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', marginBottom: '2px', cursor: 'help' }} title="工厂地址 Factory Address - 创建和管理所有金库的工厂合约地址">{t('vault.factoryAddress')}</div>
<code style={{ fontSize: '10px', wordBreak: 'break-all' }}>{vaultFactory || '-'}</code>
</div>
<div style={{ padding: '8px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', marginBottom: '2px', cursor: 'help' }} title="WUSD合约 WUSD Contract - 稳定币 WUSD 的 ERC20 合约地址">{t('vault.wusdContract')}</div>
<code style={{ fontSize: '10px', wordBreak: 'break-all' }}>{vaultWusdAddress || '-'}</code>
</div>
<div style={{ padding: '8px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', marginBottom: '2px', cursor: 'help' }} title="价格精度 Price Precision - 价格计算使用的精度值,通常为 1e30">{t('vault.pricePrecision')}</div>
<strong>{pricePrecision ? pricePrecision.toString() : '-'}</strong>
</div>
</div>
{/* 角色信息 */}
<div className="role-info">
<div className="info-row">
<span>Factory Owner:</span>
<span title="工厂所有者 Factory Owner - 拥有最高管理权限,可设置金库参数、更新价格、设置管理员等" style={{ cursor: 'help' }}>Factory Owner:</span>
<code className={address && factoryOwner && address.toLowerCase() === factoryOwner.toLowerCase() ? 'highlight' : ''}>
{factoryOwner || '-'}
</code>
</div>
<div className="info-row">
<span>Vault Manager:</span>
<span title="金库管理员 Vault Manager - 可管理金库资产,执行 depositManagedAssets 和 withdrawForManagement 操作" style={{ cursor: 'help' }}>Vault Manager:</span>
<code className={address && vaultManager && address.toLowerCase() === vaultManager.toLowerCase() ? 'highlight' : ''}>
{vaultManager || '-'}
</code>
</div>
<div className="info-row">
<span>{t('test.role')}:</span>
<span title="当前角色 Current Role - 你当前连接钱包在此金库中的角色权限" style={{ cursor: 'help' }}>{t('test.role')}:</span>
<strong className={
address && factoryOwner && address.toLowerCase() === factoryOwner.toLowerCase() ? 'text-success' :
address && vaultManager && address.toLowerCase() === vaultManager.toLowerCase() ? 'text-warning' : ''
@@ -722,11 +1016,11 @@ export function VaultPanel() {
<div className="balance-info">
<div className="info-row">
<span>{t('vault.yourWusdBalance')}:</span>
<span title="你的 WUSD 余额 Your WUSD Balance - 你钱包中持有的稳定币 WUSD 数量,可用于购买 YT" style={{ cursor: 'help' }}>{t('vault.yourWusdBalance')}:</span>
<strong>{wusdBalance ? formatUnits(wusdBalance, 18) : '0'}</strong>
</div>
<div className="info-row">
<span>{t('vault.yourYtBalance')} ({selectedVault.name}):</span>
<span title="你的 YT 余额 Your YT Balance - 你持有的当前金库 YT 代币数量,可在赎回期卖出换回 WUSD" style={{ cursor: 'help' }}>{t('vault.yourYtBalance')} ({selectedVault.name}):</span>
<strong>{ytBalance ? formatUnits(ytBalance, 18) : '0'}</strong>
</div>
</div>
@@ -744,6 +1038,12 @@ export function VaultPanel() {
>
{t('vault.sellYt')}
</button>
<button
className={`tab ${activeTab === 'transfer' ? 'active' : ''}`}
onClick={() => setActiveTab('transfer')}
>
{t('vault.transferYt')}
</button>
</div>
{activeTab === 'buy' && (
@@ -759,14 +1059,28 @@ export function VaultPanel() {
/>
</div>
{previewBuyAmount && buyAmount && (
<div className="preview">
{t('vault.youWillReceive')}: <strong>{formatUnits(previewBuyAmount, 18)} YT</strong>
<div style={{ marginBottom: '12px', padding: '10px', background: '#e8f5e9', borderRadius: '6px', fontSize: '13px' }}>
<span style={{ color: '#666' }}>{t('vault.youWillReceive')}: </span>
<strong style={{ color: '#2e7d32', fontSize: '15px' }}>{formatUnits(previewBuyAmount, 18)} YT</strong>
</div>
)}
{/* Debug: 显示授权状态 */}
<div className="debug-info" style={{ fontSize: '12px', color: '#999', marginBottom: '8px' }}>
: {wusdAllowance !== undefined ? formatUnits(wusdAllowance, 18) : 'loading...'} WUSD
{buyAmount && ` | 需要: ${buyAmount} | 需授权: ${needsApproval ? '是' : '否'}`}
{/* Debug: 显示预估计算详情 */}
<div className="debug-info" style={{ fontSize: '11px', color: '#999', marginBottom: '8px', background: '#f5f5f5', padding: '8px', borderRadius: '4px' }}>
<div>: {wusdAllowance !== undefined ? formatUnits(wusdAllowance, 18) : 'loading...'} WUSD
{buyAmount && ` | 需要: ${buyAmount} | 需授权: ${needsApproval ? '是' : '否'}`}
</div>
{previewBuyAmount && (
<div style={{ marginTop: '4px', fontFamily: 'monospace' }}>
previewBuy : {previewBuyAmount.toString()}
{vaultInfo && (
<>
<br />ytPrice: {(vaultInfo[6] as bigint).toString()} (: {formatUnits(vaultInfo[6] as bigint, 30)})
<br />wusdPrice: {(vaultInfo[5] as bigint).toString()} (: {formatUnits(vaultInfo[5] as bigint, 30)})
{pricePrecision && <><br />PRICE_PRECISION: {pricePrecision.toString()}</>}
</>
)}
</div>
)}
</div>
{needsApproval ? (
<button
@@ -788,15 +1102,135 @@ export function VaultPanel() {
</div>
)}
{/* 硬顶 Swap YT 交易区域 */}
{activeTab === 'buy' && (
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowHardcapSwap(!showHardcapSwap)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>
{isAtHardCap ? '⚠️ 金库已达硬顶 - 使用 LP 池 Swap 获取 YT' : '硬顶 Swap YT 交易'}
</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showHardcapSwap ? '▼' : '▶'}</span>
</div>
{(showHardcapSwap || isAtHardCap) && (
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
{isAtHardCap && (
<div style={{ marginBottom: '12px', padding: '8px', background: '#f8f9fa', borderRadius: '4px', fontSize: '12px', color: '#666' }}>
LP WUSD Swap {selectedVault.name}
</div>
)}
{/* 状态检查 */}
<div style={{ marginBottom: '12px', padding: '8px', background: '#fafafa', borderRadius: '6px', fontSize: '11px' }}>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<span>LP Swap: <strong style={{ color: lpSwapEnabled ? '#4caf50' : '#f44336' }}>{lpSwapEnabled ? '开启' : '关闭'}</strong></span>
<span>WUSD : <strong style={{ color: wusdWhitelisted ? '#4caf50' : '#f44336' }}>{wusdWhitelisted ? '✓' : '✗'}</strong></span>
<span>{selectedVault.name} : <strong style={{ color: targetYtWhitelisted ? '#4caf50' : '#f44336' }}>{targetYtWhitelisted ? '✓' : '✗'}</strong></span>
</div>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginTop: '4px' }}>
<span>WUSD : <strong>{wusdBalance ? formatUnits(wusdBalance, 18) : '0'}</strong></span>
<span>Router : <strong style={{ color: !needsHardcapSwapApproval ? '#4caf50' : '#f44336' }}>{wusdRouterAllowance !== undefined ? formatUnits(wusdRouterAllowance, 18) : '...'}</strong></span>
</div>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginTop: '4px' }}>
<span>{selectedVault.name} LP流动性: <strong>{targetYtUsdyAmount !== undefined ? formatUnits(targetYtUsdyAmount, 18) : '...'}</strong> USDY</span>
</div>
</div>
<div className="form-group" style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '12px', color: '#666' }}>WUSD </label>
<input
type="number"
value={hardcapSwapAmount}
onChange={(e) => setHardcapSwapAmount(e.target.value)}
placeholder="输入 WUSD 数量"
className="input"
style={{ fontSize: '13px' }}
/>
</div>
<div className="form-group" style={{ marginBottom: '8px' }}>
<label style={{ fontSize: '12px', color: '#666' }}> (%)</label>
<input
type="number"
value={hardcapSwapSlippage}
onChange={(e) => setHardcapSwapSlippage(e.target.value)}
placeholder="0.5"
className="input"
style={{ fontSize: '13px', width: '80px' }}
step="0.1"
/>
</div>
{/* 预览到账金额 */}
{hardcapSwapAmount && hardcapSwapPreview && (
<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(hardcapSwapPreview, 18)} {selectedVault.name}
</strong>
</div>
)}
{/* 按钮 */}
<div style={{ display: 'flex', gap: '8px' }}>
{needsHardcapSwapApproval ? (
<button
onClick={handleApproveWusdForRouter}
disabled={isProcessing}
className="btn btn-secondary btn-sm"
>
{isProcessing ? t('common.processing') : '授权 WUSD'}
</button>
) : (
<button
onClick={handleHardcapSwap}
disabled={isProcessing || !hardcapSwapAmount || !lpSwapEnabled || !wusdWhitelisted || !targetYtWhitelisted}
className="btn btn-primary btn-sm"
>
{isProcessing ? t('common.processing') : `Swap 获取 ${selectedVault.name}`}
</button>
)}
</div>
</div>
)}
</div>
)}
{activeTab === 'sell' && (
<div className="trade-section">
{/* 排队退出说明 */}
<div style={{ marginBottom: '12px', padding: '10px', background: '#f8f9fa', borderRadius: '6px', fontSize: '12px', color: '#666' }}>
{t('vault.queueWithdrawDesc')}
</div>
{/* 队列进度信息 */}
<div style={{ marginBottom: '12px', padding: '10px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '6px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px', fontSize: '12px' }}>
<div>
<span style={{ color: '#666' }}>{t('vault.queueTotal')}: </span>
<strong>{queueProgress ? (queueProgress as any)[1]?.toString() : '0'}</strong>
</div>
<div>
<span style={{ color: '#666' }}>{t('vault.queuePending')}: </span>
<strong>{queueProgress ? (queueProgress as any)[2]?.toString() : '0'}</strong>
</div>
<div>
<span style={{ color: '#666' }}>{t('vault.queueProcessed')}: </span>
<strong>{queueProgress ? (queueProgress as any)[0]?.toString() : '0'}</strong>
</div>
</div>
</div>
{/* 赎回状态提示 */}
<div className={`redeem-status-banner ${canRedeem ? 'success' : 'warning'}`}>
<span>{t('vault.redeemStatus')}: </span>
<strong>{canRedeem ? t('vault.redeemAvailable') : t('vault.redeemNotAvailable')}</strong>
{!canRedeem && timeUntilRedeem && (
{!canRedeem && countdown > 0 && (
<span className="time-remaining">
({t('vault.timeRemaining')}: {formatTimeRemaining(Number(timeUntilRedeem))})
({t('vault.timeRemaining')}: {formatTimeRemaining(countdown)})
</span>
)}
</div>
@@ -813,8 +1247,15 @@ export function VaultPanel() {
/>
</div>
{previewSellAmount && sellAmount && (
<div className="preview">
{t('vault.youWillReceive')}: <strong>{formatUnits(previewSellAmount, 18)} WUSD</strong>
<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(previewSellAmount, 18)} WUSD</strong>
</div>
)}
{/* Debug: 显示预估计算详情 */}
{previewSellAmount && (
<div className="debug-info" style={{ fontSize: '11px', color: '#999', marginBottom: '8px', background: '#f5f5f5', padding: '8px', borderRadius: '4px', fontFamily: 'monospace' }}>
previewSell: {previewSellAmount.toString()}
</div>
)}
<button
@@ -822,7 +1263,81 @@ export function VaultPanel() {
disabled={isProcessing || !sellAmount || !canRedeem}
className="btn btn-primary"
>
{isProcessing ? t('common.processing') : !canRedeem ? t('vault.redeemLocked') : t('vault.sell')}
{isProcessing ? t('common.processing') : !canRedeem ? t('vault.redeemLocked') : t('vault.requestWithdraw')}
</button>
{/* 用户待处理请求列表 */}
{userPendingRequests && (userPendingRequests as any[]).length > 0 && (
<div style={{ marginTop: '16px' }}>
<h4 style={{ fontSize: '13px', color: '#666', marginBottom: '8px' }}>{t('vault.yourPendingRequests')}</h4>
<div style={{ border: '1px solid #e0e0e0', borderRadius: '6px', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '11px' }}>
<thead>
<tr style={{ background: '#f8f9fa' }}>
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #e0e0e0' }}>YT {t('common.amount')}</th>
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #e0e0e0' }}>WUSD {t('common.amount')}</th>
<th style={{ padding: '8px', textAlign: 'left', borderBottom: '1px solid #e0e0e0' }}>{t('vault.requestTime')}</th>
<th style={{ padding: '8px', textAlign: 'center', borderBottom: '1px solid #e0e0e0' }}>{t('vault.status')}</th>
</tr>
</thead>
<tbody>
{(userPendingRequests as any[]).map((req, idx) => (
<tr key={idx} style={{ borderBottom: idx < (userPendingRequests as any[]).length - 1 ? '1px solid #f0f0f0' : 'none' }}>
<td style={{ padding: '8px' }}>{formatUnits(req.ytAmount, 18)}</td>
<td style={{ padding: '8px' }}>{formatUnits(req.wusdAmount, 18)}</td>
<td style={{ padding: '8px' }}>{new Date(Number(req.requestTime) * 1000).toLocaleString()}</td>
<td style={{ padding: '8px', textAlign: 'center' }}>
<span style={{
padding: '2px 8px',
borderRadius: '4px',
fontSize: '10px',
background: req.processed ? '#e8f5e9' : '#fff3e0',
color: req.processed ? '#2e7d32' : '#e65100'
}}>
{req.processed ? t('vault.processed') : t('vault.pending')}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
)}
{activeTab === 'transfer' && (
<div className="trade-section">
<div className="form-group">
<label>{t('vault.toAddress')}</label>
<input
type="text"
value={transferYtTo}
onChange={(e) => setTransferYtTo(e.target.value)}
placeholder="0x..."
className="input"
/>
</div>
<div className="form-group">
<label>{t('vault.ytAmount')}</label>
<input
type="number"
value={transferYtAmount}
onChange={(e) => setTransferYtAmount(e.target.value)}
placeholder={t('vault.enterYtAmount')}
className="input"
/>
</div>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '12px' }}>
{t('vault.yourYtBalance')}: <strong>{ytBalance ? formatUnits(ytBalance, 18) : '0'}</strong> {selectedVault.name}
</div>
<button
onClick={handleTransferYt}
disabled={isProcessing || !transferYtTo || !transferYtAmount}
className="btn btn-primary"
>
{isProcessing ? t('common.processing') : t('vault.transfer')}
</button>
</div>
)}
@@ -833,7 +1348,7 @@ export function VaultPanel() {
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowAdminConfig(!showAdminConfig)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('vault.adminConfig')}</h4>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }} title="管理员配置 Admin Config - 仅限 Factory Owner 使用,可更新价格、设置硬顶、赎回时间等">{t('vault.adminConfig')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showAdminConfig ? '▼' : '▶'}</span>
</div>
@@ -848,39 +1363,70 @@ export function VaultPanel() {
</div>
{/* 更新价格 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.updatePrices')}</h4>
<div className="form-grid" style={{ marginBottom: '16px' }}>
<div className="form-group">
<label>{t('vault.wusdPrice')}</label>
<input
type="number"
value={priceUpdateForm.wusdPrice}
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, wusdPrice: e.target.value })}
placeholder="1"
className="input"
step="0.01"
/>
<h4 style={{ fontSize: '14px', marginBottom: '12px', cursor: 'help' }} title="更新价格 Update Prices - 修改 WUSD 和 YT 的计价,影响买卖兑换比例">{t('vault.updatePrices')}</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '16px' }}>
{/* WUSD 价格 */}
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px', border: '1px solid #dee2e6' }}>
<label style={{ fontSize: '12px', color: '#666', display: 'block', marginBottom: '4px' }}>{t('vault.wusdPrice')}</label>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input
type="number"
value={priceUpdateForm.wusdPrice}
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, wusdPrice: e.target.value })}
placeholder="1"
className="input"
style={{ fontSize: '13px', flex: 1 }}
step="0.01"
/>
<button
onClick={handleUpdateWusdPriceOnly}
disabled={isPending || isConfirming || !vaultInfo}
className="btn btn-primary btn-sm"
style={{ whiteSpace: 'nowrap' }}
>
{isPending || isConfirming ? '...' : '更新'}
</button>
</div>
<p style={{ fontSize: '10px', color: '#999', margin: '4px 0 0 0' }}>
: {vaultInfo ? formatUnits((vaultInfo as any)[5], 18) : '-'} (18)
</p>
</div>
<div className="form-group">
<label>{t('vault.ytPrice')}</label>
<input
type="number"
value={priceUpdateForm.ytPrice}
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, ytPrice: e.target.value })}
placeholder="1"
className="input"
step="0.01"
/>
</div>
<div className="form-group" style={{ display: 'flex', alignItems: 'flex-end' }}>
<button onClick={handleUpdatePrices} disabled={isPending || isConfirming} className="btn btn-primary">
{isPending || isConfirming ? '...' : t('vault.updatePrices')}
</button>
{/* YT 价格 */}
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px', border: '1px solid #dee2e6' }}>
<label style={{ fontSize: '12px', color: '#666', display: 'block', marginBottom: '4px' }}>{t('vault.ytPrice')}</label>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input
type="number"
value={priceUpdateForm.ytPrice}
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, ytPrice: e.target.value })}
placeholder="1"
className="input"
style={{ fontSize: '13px', flex: 1 }}
step="0.01"
/>
<button
onClick={handleUpdateYtPriceOnly}
disabled={isPending || isConfirming || !vaultInfo}
className="btn btn-secondary btn-sm"
style={{ whiteSpace: 'nowrap' }}
>
{isPending || isConfirming ? '...' : '更新'}
</button>
</div>
<p style={{ fontSize: '10px', color: '#999', margin: '4px 0 0 0' }}>
: {vaultInfo ? formatUnits((vaultInfo as any)[6], 30) : '-'} (30)
</p>
</div>
</div>
{/* 同时更新两个价格 */}
<div style={{ marginBottom: '16px' }}>
<button onClick={handleUpdatePrices} disabled={isPending || isConfirming} className="btn btn-outline" style={{ fontSize: '12px' }}>
{isPending || isConfirming ? '...' : '同时更新两个价格'}
</button>
</div>
{/* 设置硬顶 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.setHardCap')}</h4>
<h4 style={{ fontSize: '14px', marginBottom: '12px', cursor: 'help' }} title="设置硬顶 Set Hard Cap - 设置金库可接受的最大存款额度">{t('vault.setHardCap')}</h4>
<div className="form-grid" style={{ marginBottom: '16px' }}>
<div className="form-group">
<label>{t('vault.newHardCap')}</label>
@@ -900,7 +1446,7 @@ export function VaultPanel() {
</div>
{/* 设置赎回时间 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.setRedemptionTime')}</h4>
<h4 style={{ fontSize: '14px', marginBottom: '12px', cursor: 'help' }} title="设置赎回时间 Set Redemption Time - 设置下一次允许用户赎回 YT 的时间点">{t('vault.setRedemptionTime')}</h4>
<div className="form-grid" style={{ marginBottom: '16px' }}>
<div className="form-group">
<label>{t('vault.newRedemptionTime')}</label>
@@ -919,8 +1465,8 @@ export function VaultPanel() {
</div>
{/* 设置管理员 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.setManager')}</h4>
<div className="form-grid">
<h4 style={{ fontSize: '14px', marginBottom: '12px', cursor: 'help' }} title="设置管理员 Set Manager - 指定新的金库管理员地址,管理员可管理托管资产">{t('vault.setManager')}</h4>
<div className="form-grid" style={{ marginBottom: '16px' }}>
<div className="form-group">
<label>{t('vault.newManager')}</label>
<input
@@ -937,6 +1483,52 @@ export function VaultPanel() {
</button>
</div>
</div>
{/* 批量处理退出请求 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px', cursor: 'help' }} title="批量处理退出请求 - 处理排队中的用户退出请求,发送 WUSD 给用户">{t('vault.processBatchWithdrawals')}</h4>
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
{/* 队列进度信息 */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px', fontSize: '12px', marginBottom: '12px' }}>
<div>
<span style={{ color: '#666' }}>{t('vault.queueTotal')}: </span>
<strong>{queueProgress ? (queueProgress as any)[1]?.toString() : '0'}</strong>
</div>
<div>
<span style={{ color: '#666' }}>{t('vault.queuePending')}: </span>
<strong>{queueProgress ? (queueProgress as any)[2]?.toString() : '0'}</strong>
</div>
<div>
<span style={{ color: '#666' }}>{t('vault.queueProcessed')}: </span>
<strong>{queueProgress ? (queueProgress as any)[0]?.toString() : '0'}</strong>
</div>
</div>
<div className="form-grid">
<div className="form-group">
<label>{t('vault.batchSize')}</label>
<input
type="number"
value={batchSize}
onChange={(e) => setBatchSize(e.target.value)}
placeholder="10"
className="input"
min="1"
max="100"
/>
</div>
<div className="form-group" style={{ display: 'flex', alignItems: 'flex-end' }}>
<button
onClick={handleProcessBatchWithdrawals}
disabled={isPending || isConfirming || !batchSize || (queueProgress && (queueProgress as any)[2]?.toString() === '0')}
className="btn btn-primary"
>
{isPending || isConfirming ? '...' : t('vault.processBatch')}
</button>
</div>
</div>
<p style={{ fontSize: '11px', color: '#999', margin: '8px 0 0 0' }}>
{t('vault.processBatchHint')}
</p>
</div>
</div>
)}
</div>
@@ -947,7 +1539,7 @@ export function VaultPanel() {
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowManagerPanel(!showManagerPanel)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('vault.managerPanel')}</h4>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }} title="管理员资产面板 Manager Panel - 仅限 Vault Manager 使用,可存入和提取托管资产">{t('vault.managerPanel')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showManagerPanel ? '▼' : '▶'}</span>
</div>
@@ -957,7 +1549,7 @@ export function VaultPanel() {
<div style={{ marginBottom: '16px', padding: '8px', background: '#f8f9fa', borderRadius: '4px' }}>
<div className="info-row">
<span>{t('vault.managedAssets')}:</span>
<strong>{managedAssets ? formatUnits(managedAssets, 18) : '0'} WUSD</strong>
<strong>{vaultInfo ? formatUnits(vaultInfo[2], 18) : '0'} WUSD</strong>
</div>
<div className="info-row">
<span>{t('vault.idleAssets')}:</span>
@@ -971,7 +1563,7 @@ export function VaultPanel() {
</div>
{/* 存入托管资产 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.depositManagedAssets')}</h4>
<h4 style={{ fontSize: '14px', marginBottom: '12px', cursor: 'help' }} title="存入托管资产 Deposit Managed Assets - Manager 将外部资产存回金库,增加闲置资产">{t('vault.depositManagedAssets')}</h4>
<div style={{ marginBottom: '16px' }}>
<input
type="number"
@@ -1010,7 +1602,7 @@ export function VaultPanel() {
</div>
{/* 提取托管资产 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.withdrawForManagement')}</h4>
<h4 style={{ fontSize: '14px', marginBottom: '12px', cursor: 'help' }} title="提取托管资产 Withdraw For Management - Manager 从金库提取资产进行外部管理投资">{t('vault.withdrawForManagement')}</h4>
<div style={{ marginBottom: '8px' }}>
<input
type="number"
@@ -1045,7 +1637,7 @@ export function VaultPanel() {
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowBoundaryTest(!showBoundaryTest)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('test.boundaryTests')}</h4>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }} title="边界测试 Boundary Tests - 测试合约的边界条件和错误处理,验证合约安全性">{t('test.boundaryTests')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showBoundaryTest ? '▼' : '▶'}</span>
</div>
@@ -1060,7 +1652,7 @@ export function VaultPanel() {
{canRedeem ? t('test.yes') : t('test.no')}
</strong>
<span style={{ marginLeft: '16px' }}>{t('test.timeToRedeem')}: </span>
<strong>{timeUntilRedeem ? `${Number(timeUntilRedeem)}s` : '0s'}</strong>
<strong>{countdown > 0 ? formatTimeRemaining(countdown) : '0s'}</strong>
</div>
<div className="test-grid">

View File

@@ -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 (
<div className="panel">
<h2>{t('wusd.title')}</h2>
<div className="info-row">
<span>{t('common.contract')}:</span>
<code>{CONTRACTS.WUSD}</code>
{/* 代币信息卡片 */}
<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 className="info-row">
<span>{t('common.balance')}:</span>
<strong>
{/* 用户余额 */}
<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
? formatUnits(balance, decimals)
? Number(formatUnits(balance, decimals)).toLocaleString()
: '0'} {symbol || 'WUSD'}
</strong>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
{/* 铸造 */}
<div>
<div className="form-group">
<label>{t('wusd.mintAmount')}</label>
{/* 铸造和销毁 */}
<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}
@@ -273,10 +346,9 @@ export function WUSDPanel() {
</button>
</div>
{/* 销毁 */}
<div>
<div className="form-group">
<label>{t('wusd.burnAmount')}</label>
<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}
@@ -296,13 +368,51 @@ export function WUSDPanel() {
</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' }}>{t('test.boundaryTests')}</h4>
<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>

View File

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

View File

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

View File

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

View File

@@ -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": "英文",