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:
6
frontend/.gitignore
vendored
6
frontend/.gitignore
vendored
@@ -22,3 +22,9 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Test scripts
|
||||||
|
*.mjs
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ body {
|
|||||||
|
|
||||||
/* Balance Info */
|
/* Balance Info */
|
||||||
.balance-info {
|
.balance-info {
|
||||||
background: #e3f2fd;
|
background: #f8f9fa;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
@@ -907,11 +907,11 @@ body {
|
|||||||
|
|
||||||
/* LP Panel Styles */
|
/* LP Panel Styles */
|
||||||
.pool-info {
|
.pool-info {
|
||||||
background: #f0f7ff;
|
background: #f8f9fa;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border: 1px solid #bbdefb;
|
border: 1px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lp-tabs {
|
.lp-tabs {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 { 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() {
|
export function FactoryPanel() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -33,6 +33,19 @@ export function FactoryPanel() {
|
|||||||
const [upgradeVaultAddr, setUpgradeVaultAddr] = useState('')
|
const [upgradeVaultAddr, setUpgradeVaultAddr] = useState('')
|
||||||
const [upgradeImplAddr, setUpgradeImplAddr] = useState('')
|
const [upgradeImplAddr, setUpgradeImplAddr] = useState('')
|
||||||
const [batchRedemptionTime, setBatchRedemptionTime] = 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({
|
const { data: allVaults, refetch: refetchVaults } = useReadContract({
|
||||||
address: CONTRACTS.FACTORY,
|
address: CONTRACTS.FACTORY,
|
||||||
@@ -40,6 +53,32 @@ export function FactoryPanel() {
|
|||||||
functionName: 'getAllVaults',
|
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({
|
const { data: vaultCount } = useReadContract({
|
||||||
address: CONTRACTS.FACTORY,
|
address: CONTRACTS.FACTORY,
|
||||||
abi: FACTORY_ABI,
|
abi: FACTORY_ABI,
|
||||||
@@ -64,6 +103,16 @@ export function FactoryPanel() {
|
|||||||
functionName: 'vaultImplementation',
|
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 { writeContract, data: hash, isPending, reset, error: writeError, status: writeStatus } = useWriteContract()
|
||||||
|
|
||||||
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
|
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
|
||||||
@@ -73,6 +122,7 @@ export function FactoryPanel() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
refetchVaults()
|
refetchVaults()
|
||||||
|
refetchPaused()
|
||||||
refetchDefaultHardCap()
|
refetchDefaultHardCap()
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
@@ -101,8 +151,8 @@ export function FactoryPanel() {
|
|||||||
parseUnits(createForm.hardCap, 18),
|
parseUnits(createForm.hardCap, 18),
|
||||||
CONTRACTS.WUSD,
|
CONTRACTS.WUSD,
|
||||||
BigInt(redemptionTimestamp),
|
BigInt(redemptionTimestamp),
|
||||||
parseUnits(createForm.initialWusdPrice, 18),
|
parseUnits(createForm.initialWusdPrice, 30), // wusdPrice 使用 30 位精度
|
||||||
parseUnits(createForm.initialYtPrice, 18),
|
parseUnits(createForm.initialYtPrice, 30), // ytPrice 使用 30 位精度
|
||||||
],
|
],
|
||||||
gas: GAS_CONFIG.VERY_COMPLEX,
|
gas: GAS_CONFIG.VERY_COMPLEX,
|
||||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||||
@@ -117,8 +167,8 @@ export function FactoryPanel() {
|
|||||||
functionName: 'updateVaultPrices',
|
functionName: 'updateVaultPrices',
|
||||||
args: [
|
args: [
|
||||||
priceForm.vault as `0x${string}`,
|
priceForm.vault as `0x${string}`,
|
||||||
parseUnits(priceForm.wusdPrice, 18),
|
parseUnits(priceForm.wusdPrice, 30), // wusdPrice 使用 30 位精度
|
||||||
parseUnits(priceForm.ytPrice, 18),
|
parseUnits(priceForm.ytPrice, 30), // ytPrice 使用 30 位精度
|
||||||
],
|
],
|
||||||
gas: GAS_CONFIG.STANDARD,
|
gas: GAS_CONFIG.STANDARD,
|
||||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||||
@@ -142,8 +192,8 @@ export function FactoryPanel() {
|
|||||||
// 批量更新价格
|
// 批量更新价格
|
||||||
const handleBatchUpdatePrices = () => {
|
const handleBatchUpdatePrices = () => {
|
||||||
if (selectedVaultsForBatch.length === 0) return
|
if (selectedVaultsForBatch.length === 0) return
|
||||||
const wusdPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.wusdPrice, 18))
|
const wusdPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.wusdPrice, 30)) // wusdPrice 使用 30 位精度
|
||||||
const ytPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.ytPrice, 18))
|
const ytPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.ytPrice, 30)) // ytPrice 使用 30 位精度
|
||||||
writeContract({
|
writeContract({
|
||||||
address: CONTRACTS.FACTORY,
|
address: CONTRACTS.FACTORY,
|
||||||
abi: FACTORY_ABI,
|
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 = () => {
|
const handleSetVaultImplementation = () => {
|
||||||
if (!newImplementation) return
|
if (!newImplementation) return
|
||||||
@@ -255,7 +403,7 @@ export function FactoryPanel() {
|
|||||||
address: CONTRACTS.FACTORY,
|
address: CONTRACTS.FACTORY,
|
||||||
abi: FACTORY_ABI,
|
abi: FACTORY_ABI,
|
||||||
functionName: 'updateVaultPrices',
|
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,
|
gas: GAS_CONFIG.STANDARD,
|
||||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||||
@@ -298,41 +446,73 @@ export function FactoryPanel() {
|
|||||||
|
|
||||||
<div className="factory-info">
|
<div className="factory-info">
|
||||||
<div className="info-row">
|
<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>
|
<code>{CONTRACTS.FACTORY}</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<span>{t('factory.owner')}:</span>
|
<span title="所有者 Owner - 工厂合约的所有者,拥有创建金库、更新价格等最高权限" style={{ cursor: 'help' }}>{t('factory.owner')}:</span>
|
||||||
<code>{owner || '-'}</code>
|
<code>{owner || '-'}</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<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>
|
<code style={{ fontSize: '10px' }}>{vaultImplementation || '-'}</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<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>
|
<strong>{defaultHardCap ? formatUnits(defaultHardCap, 18) : '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<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>
|
<strong>{vaultCount?.toString() || '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<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>
|
<strong>{isOwner ? t('factory.roleOwner') : t('factory.roleUser')}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>{t('factory.allVaults')}</h3>
|
<h3 title="所有金库 All Vaults - 工厂创建的所有 YT 资产金库列表" style={{ cursor: 'help' }}>{t('factory.allVaults')}</h3>
|
||||||
<div className="vault-list">
|
<div className="vault-list">
|
||||||
{allVaults && allVaults.length > 0 ? (
|
{allVaults && allVaults.length > 0 ? (
|
||||||
allVaults.map((vault, index) => (
|
allVaults.map((vault, index) => {
|
||||||
<div key={index} className="vault-item">
|
const isPaused = vaultPausedMap[vault.toLowerCase()] || false
|
||||||
<span>Vault {index + 1}:</span>
|
return (
|
||||||
<code>{vault}</code>
|
<div key={index} className="vault-item" style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
</div>
|
<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>
|
<p className="text-muted">{t('factory.noVaults')}</p>
|
||||||
)}
|
)}
|
||||||
@@ -342,7 +522,7 @@ export function FactoryPanel() {
|
|||||||
{isOwner && (
|
{isOwner && (
|
||||||
<>
|
<>
|
||||||
<div className="section">
|
<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-grid">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t('factory.name')}</label>
|
<label>{t('factory.name')}</label>
|
||||||
@@ -421,13 +601,142 @@ export function FactoryPanel() {
|
|||||||
>
|
>
|
||||||
{isProcessing ? t('common.processing') : t('factory.create')}
|
{isProcessing ? t('common.processing') : t('factory.create')}
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div className="section">
|
<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-grid">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t('factory.vaultAddress')}</label>
|
<label title="金库地址 Vault Address - 选择要更新价格的金库" style={{ cursor: 'help' }}>{t('factory.vaultAddress')}</label>
|
||||||
<select
|
<select
|
||||||
value={priceForm.vault}
|
value={priceForm.vault}
|
||||||
onChange={(e) => setPriceForm({ ...priceForm, vault: e.target.value })}
|
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' }}
|
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||||
onClick={() => setShowOwnerConfig(!showOwnerConfig)}
|
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>
|
<span style={{ color: '#999', fontSize: '16px' }}>{showOwnerConfig ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -518,7 +827,7 @@ export function FactoryPanel() {
|
|||||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||||
onClick={() => setShowBatchOps(!showBatchOps)}
|
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>
|
<span style={{ color: '#999', fontSize: '16px' }}>{showBatchOps ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -617,7 +926,7 @@ export function FactoryPanel() {
|
|||||||
</div>
|
</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>
|
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.batchSetRedemptionTime')}</h5>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
<input
|
<input
|
||||||
@@ -636,6 +945,32 @@ export function FactoryPanel() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -646,7 +981,7 @@ export function FactoryPanel() {
|
|||||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
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>
|
<span style={{ color: '#999', fontSize: '16px' }}>{showAdvanced ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -657,6 +992,37 @@ export function FactoryPanel() {
|
|||||||
{t('factory.advancedWarning')}
|
{t('factory.advancedWarning')}
|
||||||
</div>
|
</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' }}>
|
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
|
||||||
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.setVaultImplementation')}</h5>
|
<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' }}
|
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||||
onClick={() => setShowPermissionTest(!showPermissionTest)}
|
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>
|
<span style={{ color: '#999', fontSize: '16px' }}>{showPermissionTest ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { TransactionRecord } from '../hooks/useTransactionHistory'
|
import type { TransactionRecord } from '../hooks/useTransactionHistory'
|
||||||
|
|
||||||
@@ -8,6 +9,17 @@ interface Props {
|
|||||||
|
|
||||||
export function TransactionHistory({ transactions, onClear }: Props) {
|
export function TransactionHistory({ transactions, onClear }: Props) {
|
||||||
const { t } = useTranslation()
|
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 getTypeLabel = (type: string) => {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
@@ -75,13 +87,31 @@ export function TransactionHistory({ transactions, onClear }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="tx-hash">
|
<div className="tx-hash">
|
||||||
{tx.hash && typeof tx.hash === 'string' && tx.hash.startsWith('0x') ? (
|
{tx.hash && typeof tx.hash === 'string' && tx.hash.startsWith('0x') ? (
|
||||||
<a
|
<>
|
||||||
href={`https://sepolia.arbiscan.io/tx/${tx.hash}`}
|
<a
|
||||||
target="_blank"
|
href={`https://sepolia.arbiscan.io/tx/${tx.hash}`}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
{shortenHash(tx.hash)}
|
>
|
||||||
</a>
|
{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>
|
<span className="text-muted">-</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useState, useEffect, useRef } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
|
import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
|
||||||
import { parseUnits, formatUnits, maxUint256 } from 'viem'
|
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 { useTransactions } from '../context/TransactionContext'
|
||||||
import type { TransactionType } from '../context/TransactionContext'
|
import type { TransactionType } from '../context/TransactionContext'
|
||||||
import { useToast } from './Toast'
|
import { useToast } from './Toast'
|
||||||
@@ -24,7 +25,7 @@ export function VaultPanel() {
|
|||||||
const [selectedVault, setSelectedVault] = useState<{ name: string; address: string }>(DEFAULT_VAULTS[0])
|
const [selectedVault, setSelectedVault] = useState<{ name: string; address: string }>(DEFAULT_VAULTS[0])
|
||||||
const [buyAmount, setBuyAmount] = useState('')
|
const [buyAmount, setBuyAmount] = useState('')
|
||||||
const [sellAmount, setSellAmount] = 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 [showBoundaryTest, setShowBoundaryTest] = useState(false)
|
||||||
const [testResult, setTestResult] = useState<{ type: 'success' | 'error' | 'pending', msg: string } | null>(null)
|
const [testResult, setTestResult] = useState<{ type: 'success' | 'error' | 'pending', msg: string } | null>(null)
|
||||||
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: 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 [withdrawManagedAmount, setWithdrawManagedAmount] = useState('')
|
||||||
const [withdrawToAddress, setWithdrawToAddress] = 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({
|
const { data: allVaultsData } = useReadContract({
|
||||||
address: CONTRACTS.FACTORY,
|
address: CONTRACTS.FACTORY,
|
||||||
@@ -152,13 +168,6 @@ export function VaultPanel() {
|
|||||||
functionName: 'manager',
|
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)
|
// 读取 Manager 对 Vault 的 WUSD 授权额度 (用于 depositManagedAssets)
|
||||||
const { data: managerWusdAllowance, refetch: refetchManagerAllowance } = useReadContract({
|
const { data: managerWusdAllowance, refetch: refetchManagerAllowance } = useReadContract({
|
||||||
address: CONTRACTS.WUSD,
|
address: CONTRACTS.WUSD,
|
||||||
@@ -173,6 +182,133 @@ export function VaultPanel() {
|
|||||||
functionName: 'owner',
|
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 { writeContract, data: hash, isPending, reset, error: writeError, status: writeStatus } = useWriteContract()
|
||||||
|
|
||||||
const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({
|
const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({
|
||||||
@@ -220,12 +356,17 @@ export function VaultPanel() {
|
|||||||
refetchYtBalance()
|
refetchYtBalance()
|
||||||
refetchWusdBalance()
|
refetchWusdBalance()
|
||||||
refetchAllowance()
|
refetchAllowance()
|
||||||
refetchManagedAssets()
|
|
||||||
refetchManagerAllowance()
|
refetchManagerAllowance()
|
||||||
|
refetchWusdRouterAllowance()
|
||||||
|
refetchQueueProgress()
|
||||||
|
refetchUserPendingRequests()
|
||||||
setBuyAmount('')
|
setBuyAmount('')
|
||||||
setSellAmount('')
|
setSellAmount('')
|
||||||
setDepositManagedAmount('')
|
setDepositManagedAmount('')
|
||||||
setWithdrawManagedAmount('')
|
setWithdrawManagedAmount('')
|
||||||
|
setTransferYtTo('')
|
||||||
|
setTransferYtAmount('')
|
||||||
|
setHardcapSwapAmount('')
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
}, [isSuccess])
|
}, [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 () => {
|
const handleBuy = async () => {
|
||||||
if (!buyAmount) return
|
if (!buyAmount) return
|
||||||
recordTx('buy', buyAmount, 'WUSD')
|
recordTx('buy', buyAmount, 'WUSD')
|
||||||
@@ -367,11 +551,12 @@ export function VaultPanel() {
|
|||||||
|
|
||||||
// ===== 管理员功能 =====
|
// ===== 管理员功能 =====
|
||||||
|
|
||||||
// 更新金库价格 (Factory.updateVaultPrices)
|
// 更新金库价格 (Factory.updateVaultPrices) - 同时更新两个
|
||||||
|
// wusdPrice: 18位精度, ytPrice: 30位精度
|
||||||
const handleUpdatePrices = () => {
|
const handleUpdatePrices = () => {
|
||||||
if (!address) return
|
if (!address) return
|
||||||
const wusdPrice = parseUnits(priceUpdateForm.wusdPrice, 18)
|
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')
|
recordTx('test', `${priceUpdateForm.wusdPrice}/${priceUpdateForm.ytPrice}`, 'UpdatePrices')
|
||||||
writeContract({
|
writeContract({
|
||||||
address: CONTRACTS.FACTORY,
|
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)
|
// 设置硬顶 (Factory.setHardCap)
|
||||||
const handleSetHardCap = () => {
|
const handleSetHardCap = () => {
|
||||||
if (!address || !hardCapForm) return
|
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 资产管理功能 =====
|
||||||
|
|
||||||
// Manager 授权 WUSD 给 Vault (用于 depositManagedAssets)
|
// 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 [isTestRunning, setIsTestRunning] = useState(false)
|
||||||
const testTypeRef = useRef<string | null>(null)
|
const testTypeRef = useRef<string | null>(null)
|
||||||
@@ -630,6 +888,22 @@ export function VaultPanel() {
|
|||||||
return parts.join(' ')
|
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'
|
const isProcessing = (isPending || isConfirming) && writeStatus !== 'error'
|
||||||
|
|
||||||
@@ -668,48 +942,68 @@ export function VaultPanel() {
|
|||||||
<h3>{vaultName || selectedVault.name} ({vaultSymbol || '-'})</h3>
|
<h3>{vaultName || selectedVault.name} ({vaultSymbol || '-'})</h3>
|
||||||
<div className="info-grid">
|
<div className="info-grid">
|
||||||
<div className="info-item">
|
<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>
|
<strong>{vaultInfo ? formatUnits(vaultInfo[0], 18) : '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-item">
|
<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>
|
<strong>{vaultInfo ? formatUnits(vaultInfo[1], 18) : '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-item">
|
<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>
|
<strong>{vaultInfo ? formatUnits(vaultInfo[3], 18) : '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-item">
|
<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>
|
<strong>{vaultInfo ? formatUnits(vaultInfo[4], 18) : '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-item">
|
<div className="info-item">
|
||||||
<span>{t('vault.wusdPrice')}</span>
|
<span title="WUSD价格 WUSD Price - 稳定币 WUSD 的计价,通常为 1.0,30位精度" style={{ cursor: 'help' }}>{t('vault.wusdPrice')}</span>
|
||||||
<strong>{vaultInfo ? formatUnits(vaultInfo[5], 18) : '0'}</strong>
|
<strong>{vaultInfo ? formatUnits(vaultInfo[5], 30) : '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-item">
|
<div className="info-item">
|
||||||
<span>{t('vault.ytPrice')}</span>
|
<span title="YT价格 YT Price - YT 代币的价格,30位精度,用于计算买入/卖出的兑换比例" style={{ cursor: 'help' }}>{t('vault.ytPrice')}</span>
|
||||||
<strong>{vaultInfo ? formatUnits(vaultInfo[6], 18) : '0'}</strong>
|
<strong>{vaultInfo ? formatUnits(vaultInfo[6], 30) : '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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="role-info">
|
||||||
<div className="info-row">
|
<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' : ''}>
|
<code className={address && factoryOwner && address.toLowerCase() === factoryOwner.toLowerCase() ? 'highlight' : ''}>
|
||||||
{factoryOwner || '-'}
|
{factoryOwner || '-'}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<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' : ''}>
|
<code className={address && vaultManager && address.toLowerCase() === vaultManager.toLowerCase() ? 'highlight' : ''}>
|
||||||
{vaultManager || '-'}
|
{vaultManager || '-'}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<span>{t('test.role')}:</span>
|
<span title="当前角色 Current Role - 你当前连接钱包在此金库中的角色权限" style={{ cursor: 'help' }}>{t('test.role')}:</span>
|
||||||
<strong className={
|
<strong className={
|
||||||
address && factoryOwner && address.toLowerCase() === factoryOwner.toLowerCase() ? 'text-success' :
|
address && factoryOwner && address.toLowerCase() === factoryOwner.toLowerCase() ? 'text-success' :
|
||||||
address && vaultManager && address.toLowerCase() === vaultManager.toLowerCase() ? 'text-warning' : ''
|
address && vaultManager && address.toLowerCase() === vaultManager.toLowerCase() ? 'text-warning' : ''
|
||||||
@@ -722,11 +1016,11 @@ export function VaultPanel() {
|
|||||||
|
|
||||||
<div className="balance-info">
|
<div className="balance-info">
|
||||||
<div className="info-row">
|
<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>
|
<strong>{wusdBalance ? formatUnits(wusdBalance, 18) : '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<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>
|
<strong>{ytBalance ? formatUnits(ytBalance, 18) : '0'}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -744,6 +1038,12 @@ export function VaultPanel() {
|
|||||||
>
|
>
|
||||||
{t('vault.sellYt')}
|
{t('vault.sellYt')}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={`tab ${activeTab === 'transfer' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('transfer')}
|
||||||
|
>
|
||||||
|
{t('vault.transferYt')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === 'buy' && (
|
{activeTab === 'buy' && (
|
||||||
@@ -759,14 +1059,28 @@ export function VaultPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{previewBuyAmount && buyAmount && (
|
{previewBuyAmount && buyAmount && (
|
||||||
<div className="preview">
|
<div style={{ marginBottom: '12px', padding: '10px', background: '#e8f5e9', borderRadius: '6px', fontSize: '13px' }}>
|
||||||
{t('vault.youWillReceive')}: <strong>{formatUnits(previewBuyAmount, 18)} YT</strong>
|
<span style={{ color: '#666' }}>{t('vault.youWillReceive')}: </span>
|
||||||
|
<strong style={{ color: '#2e7d32', fontSize: '15px' }}>{formatUnits(previewBuyAmount, 18)} YT</strong>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Debug: 显示授权状态 */}
|
{/* Debug: 显示预估计算详情 */}
|
||||||
<div className="debug-info" style={{ fontSize: '12px', color: '#999', marginBottom: '8px' }}>
|
<div className="debug-info" style={{ fontSize: '11px', color: '#999', marginBottom: '8px', background: '#f5f5f5', padding: '8px', borderRadius: '4px' }}>
|
||||||
授权额度: {wusdAllowance !== undefined ? formatUnits(wusdAllowance, 18) : 'loading...'} WUSD
|
<div>授权额度: {wusdAllowance !== undefined ? formatUnits(wusdAllowance, 18) : 'loading...'} WUSD
|
||||||
{buyAmount && ` | 需要: ${buyAmount} | 需授权: ${needsApproval ? '是' : '否'}`}
|
{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>
|
</div>
|
||||||
{needsApproval ? (
|
{needsApproval ? (
|
||||||
<button
|
<button
|
||||||
@@ -788,15 +1102,135 @@ export function VaultPanel() {
|
|||||||
</div>
|
</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' && (
|
{activeTab === 'sell' && (
|
||||||
<div className="trade-section">
|
<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'}`}>
|
<div className={`redeem-status-banner ${canRedeem ? 'success' : 'warning'}`}>
|
||||||
<span>{t('vault.redeemStatus')}: </span>
|
<span>{t('vault.redeemStatus')}: </span>
|
||||||
<strong>{canRedeem ? t('vault.redeemAvailable') : t('vault.redeemNotAvailable')}</strong>
|
<strong>{canRedeem ? t('vault.redeemAvailable') : t('vault.redeemNotAvailable')}</strong>
|
||||||
{!canRedeem && timeUntilRedeem && (
|
{!canRedeem && countdown > 0 && (
|
||||||
<span className="time-remaining">
|
<span className="time-remaining">
|
||||||
({t('vault.timeRemaining')}: {formatTimeRemaining(Number(timeUntilRedeem))})
|
({t('vault.timeRemaining')}: {formatTimeRemaining(countdown)})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -813,8 +1247,15 @@ export function VaultPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{previewSellAmount && sellAmount && (
|
{previewSellAmount && sellAmount && (
|
||||||
<div className="preview">
|
<div style={{ marginBottom: '12px', padding: '10px', background: '#f8f9fa', borderRadius: '6px', fontSize: '13px' }}>
|
||||||
{t('vault.youWillReceive')}: <strong>{formatUnits(previewSellAmount, 18)} WUSD</strong>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -822,7 +1263,81 @@ export function VaultPanel() {
|
|||||||
disabled={isProcessing || !sellAmount || !canRedeem}
|
disabled={isProcessing || !sellAmount || !canRedeem}
|
||||||
className="btn btn-primary"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -833,7 +1348,7 @@ export function VaultPanel() {
|
|||||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||||
onClick={() => setShowAdminConfig(!showAdminConfig)}
|
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>
|
<span style={{ color: '#999', fontSize: '16px' }}>{showAdminConfig ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -848,39 +1363,70 @@ export function VaultPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 更新价格 */}
|
{/* 更新价格 */}
|
||||||
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.updatePrices')}</h4>
|
<h4 style={{ fontSize: '14px', marginBottom: '12px', cursor: 'help' }} title="更新价格 Update Prices - 修改 WUSD 和 YT 的计价,影响买卖兑换比例">{t('vault.updatePrices')}</h4>
|
||||||
<div className="form-grid" style={{ marginBottom: '16px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '16px' }}>
|
||||||
<div className="form-group">
|
{/* WUSD 价格 */}
|
||||||
<label>{t('vault.wusdPrice')}</label>
|
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px', border: '1px solid #dee2e6' }}>
|
||||||
<input
|
<label style={{ fontSize: '12px', color: '#666', display: 'block', marginBottom: '4px' }}>{t('vault.wusdPrice')}</label>
|
||||||
type="number"
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
value={priceUpdateForm.wusdPrice}
|
<input
|
||||||
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, wusdPrice: e.target.value })}
|
type="number"
|
||||||
placeholder="1"
|
value={priceUpdateForm.wusdPrice}
|
||||||
className="input"
|
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, wusdPrice: e.target.value })}
|
||||||
step="0.01"
|
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>
|
||||||
<div className="form-group">
|
{/* YT 价格 */}
|
||||||
<label>{t('vault.ytPrice')}</label>
|
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px', border: '1px solid #dee2e6' }}>
|
||||||
<input
|
<label style={{ fontSize: '12px', color: '#666', display: 'block', marginBottom: '4px' }}>{t('vault.ytPrice')}</label>
|
||||||
type="number"
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
value={priceUpdateForm.ytPrice}
|
<input
|
||||||
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, ytPrice: e.target.value })}
|
type="number"
|
||||||
placeholder="1"
|
value={priceUpdateForm.ytPrice}
|
||||||
className="input"
|
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, ytPrice: e.target.value })}
|
||||||
step="0.01"
|
placeholder="1"
|
||||||
/>
|
className="input"
|
||||||
</div>
|
style={{ fontSize: '13px', flex: 1 }}
|
||||||
<div className="form-group" style={{ display: 'flex', alignItems: 'flex-end' }}>
|
step="0.01"
|
||||||
<button onClick={handleUpdatePrices} disabled={isPending || isConfirming} className="btn btn-primary">
|
/>
|
||||||
{isPending || isConfirming ? '...' : t('vault.updatePrices')}
|
<button
|
||||||
</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>
|
</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-grid" style={{ marginBottom: '16px' }}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t('vault.newHardCap')}</label>
|
<label>{t('vault.newHardCap')}</label>
|
||||||
@@ -900,7 +1446,7 @@ export function VaultPanel() {
|
|||||||
</div>
|
</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-grid" style={{ marginBottom: '16px' }}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t('vault.newRedemptionTime')}</label>
|
<label>{t('vault.newRedemptionTime')}</label>
|
||||||
@@ -919,8 +1465,8 @@ export function VaultPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 设置管理员 */}
|
{/* 设置管理员 */}
|
||||||
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.setManager')}</h4>
|
<h4 style={{ fontSize: '14px', marginBottom: '12px', cursor: 'help' }} title="设置管理员 Set Manager - 指定新的金库管理员地址,管理员可管理托管资产">{t('vault.setManager')}</h4>
|
||||||
<div className="form-grid">
|
<div className="form-grid" style={{ marginBottom: '16px' }}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>{t('vault.newManager')}</label>
|
<label>{t('vault.newManager')}</label>
|
||||||
<input
|
<input
|
||||||
@@ -937,6 +1483,52 @@ export function VaultPanel() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -947,7 +1539,7 @@ export function VaultPanel() {
|
|||||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||||
onClick={() => setShowManagerPanel(!showManagerPanel)}
|
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>
|
<span style={{ color: '#999', fontSize: '16px' }}>{showManagerPanel ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -957,7 +1549,7 @@ export function VaultPanel() {
|
|||||||
<div style={{ marginBottom: '16px', padding: '8px', background: '#f8f9fa', borderRadius: '4px' }}>
|
<div style={{ marginBottom: '16px', padding: '8px', background: '#f8f9fa', borderRadius: '4px' }}>
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<span>{t('vault.managedAssets')}:</span>
|
<span>{t('vault.managedAssets')}:</span>
|
||||||
<strong>{managedAssets ? formatUnits(managedAssets, 18) : '0'} WUSD</strong>
|
<strong>{vaultInfo ? formatUnits(vaultInfo[2], 18) : '0'} WUSD</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<span>{t('vault.idleAssets')}:</span>
|
<span>{t('vault.idleAssets')}:</span>
|
||||||
@@ -971,7 +1563,7 @@ export function VaultPanel() {
|
|||||||
</div>
|
</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' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -1010,7 +1602,7 @@ export function VaultPanel() {
|
|||||||
</div>
|
</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' }}>
|
<div style={{ marginBottom: '8px' }}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -1045,7 +1637,7 @@ export function VaultPanel() {
|
|||||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||||
onClick={() => setShowBoundaryTest(!showBoundaryTest)}
|
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>
|
<span style={{ color: '#999', fontSize: '16px' }}>{showBoundaryTest ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1060,7 +1652,7 @@ export function VaultPanel() {
|
|||||||
{canRedeem ? t('test.yes') : t('test.no')}
|
{canRedeem ? t('test.yes') : t('test.no')}
|
||||||
</strong>
|
</strong>
|
||||||
<span style={{ marginLeft: '16px' }}>{t('test.timeToRedeem')}: </span>
|
<span style={{ marginLeft: '16px' }}>{t('test.timeToRedeem')}: </span>
|
||||||
<strong>{timeUntilRedeem ? `${Number(timeUntilRedeem)}s` : '0s'}</strong>
|
<strong>{countdown > 0 ? formatTimeRemaining(countdown) : '0s'}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="test-grid">
|
<div className="test-grid">
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export function WUSDPanel() {
|
|||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const [mintAmount, setMintAmount] = useState('')
|
const [mintAmount, setMintAmount] = useState('')
|
||||||
const [burnAmount, setBurnAmount] = useState('')
|
const [burnAmount, setBurnAmount] = useState('')
|
||||||
|
const [transferTo, setTransferTo] = useState('')
|
||||||
|
const [transferAmount, setTransferAmount] = useState('')
|
||||||
const [showBoundaryTest, setShowBoundaryTest] = useState(false)
|
const [showBoundaryTest, setShowBoundaryTest] = useState(false)
|
||||||
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
|
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
|
||||||
|
|
||||||
@@ -37,6 +39,24 @@ export function WUSDPanel() {
|
|||||||
functionName: 'decimals',
|
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 { writeContract, data: hash, isPending, error: writeError, reset, status: writeStatus } = useWriteContract()
|
||||||
|
|
||||||
const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({
|
const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({
|
||||||
@@ -81,7 +101,11 @@ export function WUSDPanel() {
|
|||||||
pendingTxRef.current = null
|
pendingTxRef.current = null
|
||||||
}
|
}
|
||||||
refetchBalance()
|
refetchBalance()
|
||||||
|
refetchTotalSupply()
|
||||||
setMintAmount('')
|
setMintAmount('')
|
||||||
|
setBurnAmount('')
|
||||||
|
setTransferTo('')
|
||||||
|
setTransferAmount('')
|
||||||
}
|
}
|
||||||
}, [isSuccess])
|
}, [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) => {
|
const runBoundaryTest = (testType: string) => {
|
||||||
if (!address || !decimals) return
|
if (!address || !decimals) return
|
||||||
@@ -237,24 +280,54 @@ export function WUSDPanel() {
|
|||||||
return (
|
return (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<h2>{t('wusd.title')}</h2>
|
<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>
|
||||||
<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
|
{balance !== undefined && decimals !== undefined
|
||||||
? formatUnits(balance, decimals)
|
? Number(formatUnits(balance, decimals)).toLocaleString()
|
||||||
: '0'} {symbol || 'WUSD'}
|
: '0'} {symbol || 'WUSD'}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
{/* 铸造和销毁 */}
|
||||||
{/* 铸造 */}
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
|
||||||
<div>
|
<div style={{ padding: '12px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
|
||||||
<div className="form-group">
|
<div className="form-group" style={{ marginBottom: '8px' }}>
|
||||||
<label>{t('wusd.mintAmount')}</label>
|
<label style={{ fontSize: '13px', fontWeight: 500, cursor: 'help' }} title="铸造数量 Mint Amount - 输入要铸造的 WUSD 数量(仅 Owner 可用)">{t('wusd.mintAmount')}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={mintAmount}
|
value={mintAmount}
|
||||||
@@ -273,10 +346,9 @@ export function WUSDPanel() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 销毁 */}
|
<div style={{ padding: '12px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
|
||||||
<div>
|
<div className="form-group" style={{ marginBottom: '8px' }}>
|
||||||
<div className="form-group">
|
<label style={{ fontSize: '13px', fontWeight: 500, cursor: 'help' }} title="销毁数量 Burn Amount - 输入要销毁的 WUSD 数量(仅 Owner 可用)">{t('wusd.burnAmount')}</label>
|
||||||
<label>{t('wusd.burnAmount')}</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={burnAmount}
|
value={burnAmount}
|
||||||
@@ -296,13 +368,51 @@ export function WUSDPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</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={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
|
||||||
<div
|
<div
|
||||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||||
onClick={() => setShowBoundaryTest(!showBoundaryTest)}
|
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>
|
<span style={{ color: '#999', fontSize: '16px' }}>{showBoundaryTest ? '▼' : '▶'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Gas配置 - 用于解决Arbitrum测试网gas预估问题
|
// Gas配置 - 用于解决Arbitrum测试网gas预估问题
|
||||||
// YT Asset Vault ABI - 用于读取 YT 代币本身的价格
|
// YT Asset Vault ABI - 用于读取 YT 代币本身的价格
|
||||||
// 注意: YT代币需要实现 assetPrice() 接口(30位精度),供 YTPriceFeed 读取
|
|
||||||
export const YT_ASSET_VAULT_ABI = [
|
export const YT_ASSET_VAULT_ABI = [
|
||||||
{
|
{
|
||||||
inputs: [],
|
inputs: [],
|
||||||
@@ -15,14 +14,6 @@ export const YT_ASSET_VAULT_ABI = [
|
|||||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||||
stateMutability: 'view',
|
stateMutability: 'view',
|
||||||
type: 'function'
|
type: 'function'
|
||||||
},
|
|
||||||
// YTPriceFeed 期望的接口:assetPrice() - 30位精度
|
|
||||||
{
|
|
||||||
inputs: [],
|
|
||||||
name: 'assetPrice',
|
|
||||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
|
||||||
stateMutability: 'view',
|
|
||||||
type: 'function'
|
|
||||||
}
|
}
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
@@ -43,21 +34,23 @@ export const GAS_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CONTRACTS = {
|
export const CONTRACTS = {
|
||||||
FACTORY: '0x6DaB73519DbaFf23F36FEd24110e2ef5Cfc8aAC9' as const,
|
// 12月18日新部署的合约地址
|
||||||
WUSD: '0x939cf46F7A4d05da2a37213E7379a8b04528F590' as const,
|
FACTORY: '0x982716f32F10BCB5B5944c1473a8992354bF632b' as const,
|
||||||
|
WUSD: '0x6d2bf81a631dFE19B2f348aE92cF6Ef41ca2DF98' as const, // fork测试用
|
||||||
VAULTS: {
|
VAULTS: {
|
||||||
|
// 旧金库地址(需要通过Factory.getAllVaults()动态获取新金库)
|
||||||
YT_A: '0x0cA35994F033685E7a57ef9bc5d00dd3cf927330' as const,
|
YT_A: '0x0cA35994F033685E7a57ef9bc5d00dd3cf927330' as const,
|
||||||
YT_B: '0x333805C9EE75f59Aa2Cc79DfDe2499F920c7b408' as const,
|
YT_B: '0x333805C9EE75f59Aa2Cc79DfDe2499F920c7b408' as const,
|
||||||
YT_C: '0x6DF0ED6f0345F601A206974973dE9fC970598587' as const,
|
YT_C: '0x6DF0ED6f0345F601A206974973dE9fC970598587' as const,
|
||||||
YT_D: '0x5d91FD16fa85547b0784c377A47BF7706D7875d3' as const,
|
YT_D: '0x5d91FD16fa85547b0784c377A47BF7706D7875d3' as const,
|
||||||
},
|
},
|
||||||
// LP Pool contracts
|
// LP Pool contracts - 12月18日新部署
|
||||||
YT_REWARD_ROUTER: '0x51eEF57eC57c867AC23945f0ce21aA5A9a2C246c' as const,
|
YT_REWARD_ROUTER: '0x953758c02ec49F1f67fE2a8E3F67C434FeC5aB9d' as const,
|
||||||
YT_LP_TOKEN: '0x1b96F219E8aeE557DD8bD905a6c72cc64eA5BD7B' as const,
|
YT_LP_TOKEN: '0xf5206D958f692556603806A8f65bB106E23d1776' as const,
|
||||||
YT_POOL_MANAGER: '0x14246886a1E1202cb6b5a2db793eF3359d536302' as const,
|
YT_POOL_MANAGER: '0xe3068a25D6eEda551Cd12CC291813A4fe0e4AbB6' as const,
|
||||||
YT_VAULT: '0x19982e5145ca5401A1084c0BF916c0E0bB343Af9' as const,
|
YT_VAULT: '0xbC2e4f06601B92B3F430962a8f0a7E8c378ce54e' as const,
|
||||||
USDY: '0x631Bd6834C50f6d2B07035c9253b4a19132E888c' as const,
|
USDY: '0x54551451E14D3d3418e4Aa9F31e9E8573fd37053' as const,
|
||||||
YT_PRICE_FEED: '0x0f2d930EE73972132E3a36b7eD6F709Af6E5B879' as const,
|
YT_PRICE_FEED: '0x9364D3aF669886883C26EC0ff32000719491452A' as const,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FACTORY_ABI = [
|
export const FACTORY_ABI = [
|
||||||
@@ -265,6 +258,89 @@ export const FACTORY_ABI = [
|
|||||||
outputs: [],
|
outputs: [],
|
||||||
stateMutability: 'nonpayable',
|
stateMutability: 'nonpayable',
|
||||||
type: 'function'
|
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
|
] as const
|
||||||
|
|
||||||
@@ -452,13 +528,122 @@ export const VAULT_ABI = [
|
|||||||
stateMutability: 'nonpayable',
|
stateMutability: 'nonpayable',
|
||||||
type: 'function'
|
type: 'function'
|
||||||
},
|
},
|
||||||
|
// 用户提交退出请求(排队机制),返回 requestId
|
||||||
{
|
{
|
||||||
inputs: [{ internalType: 'uint256', name: '_ytAmount', type: 'uint256' }],
|
inputs: [{ internalType: 'uint256', name: '_ytAmount', type: 'uint256' }],
|
||||||
name: 'withdrawYT',
|
name: 'withdrawYT',
|
||||||
outputs: [{ internalType: 'uint256', name: 'wusdAmount', type: 'uint256' }],
|
outputs: [{ internalType: 'uint256', name: 'requestId', type: 'uint256' }],
|
||||||
stateMutability: 'nonpayable',
|
stateMutability: 'nonpayable',
|
||||||
type: 'function'
|
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: [],
|
inputs: [],
|
||||||
name: 'wusdAddress',
|
name: 'wusdAddress',
|
||||||
@@ -527,6 +712,14 @@ export const VAULT_ABI = [
|
|||||||
outputs: [],
|
outputs: [],
|
||||||
stateMutability: 'nonpayable',
|
stateMutability: 'nonpayable',
|
||||||
type: 'function'
|
type: 'function'
|
||||||
|
},
|
||||||
|
// 读取暂停状态
|
||||||
|
{
|
||||||
|
inputs: [],
|
||||||
|
name: 'paused',
|
||||||
|
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||||
|
stateMutability: 'view',
|
||||||
|
type: 'function'
|
||||||
}
|
}
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
@@ -1134,10 +1327,10 @@ export const YT_VAULT_ABI = [
|
|||||||
stateMutability: 'view',
|
stateMutability: 'view',
|
||||||
type: 'function'
|
type: 'function'
|
||||||
},
|
},
|
||||||
// swap 开关
|
// swap 开关 (合约实际使用 isSwapEnabled)
|
||||||
{
|
{
|
||||||
inputs: [],
|
inputs: [],
|
||||||
name: 'swapEnabled',
|
name: 'isSwapEnabled',
|
||||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||||
stateMutability: 'view',
|
stateMutability: 'view',
|
||||||
type: 'function'
|
type: 'function'
|
||||||
@@ -1167,3 +1360,94 @@ export const YT_VAULT_ABI = [
|
|||||||
type: 'function'
|
type: 'function'
|
||||||
}
|
}
|
||||||
] as const
|
] 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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
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 {
|
export interface TransactionRecord {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -39,7 +39,13 @@
|
|||||||
"burn": "Burn",
|
"burn": "Burn",
|
||||||
"minting": "Minting...",
|
"minting": "Minting...",
|
||||||
"confirming": "Confirming...",
|
"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": {
|
"vault": {
|
||||||
"title": "Vault Trading",
|
"title": "Vault Trading",
|
||||||
@@ -87,7 +93,28 @@
|
|||||||
"toAddress": "To Address",
|
"toAddress": "To Address",
|
||||||
"defaultSelf": "default: self",
|
"defaultSelf": "default: self",
|
||||||
"approvedAmount": "Approved Amount",
|
"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": {
|
"factory": {
|
||||||
"title": "Factory Management",
|
"title": "Factory Management",
|
||||||
@@ -134,7 +161,18 @@
|
|||||||
"upgrade": "Upgrade",
|
"upgrade": "Upgrade",
|
||||||
"batchUpgradeVault": "Batch Upgrade Vaults",
|
"batchUpgradeVault": "Batch Upgrade Vaults",
|
||||||
"batchUpgrade": "Batch Upgrade",
|
"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": {
|
"language": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
|
|||||||
@@ -39,7 +39,13 @@
|
|||||||
"burn": "销毁",
|
"burn": "销毁",
|
||||||
"minting": "铸造中...",
|
"minting": "铸造中...",
|
||||||
"confirming": "确认中...",
|
"confirming": "确认中...",
|
||||||
"mintSuccess": "铸造成功!"
|
"mintSuccess": "铸造成功!",
|
||||||
|
"owner": "合约所有者",
|
||||||
|
"totalSupply": "总供应量",
|
||||||
|
"transfer": "转账",
|
||||||
|
"toAddress": "接收地址",
|
||||||
|
"transferAmount": "转账数量",
|
||||||
|
"invalidAddress": "无效的地址格式"
|
||||||
},
|
},
|
||||||
"vault": {
|
"vault": {
|
||||||
"title": "金库交易",
|
"title": "金库交易",
|
||||||
@@ -87,7 +93,28 @@
|
|||||||
"toAddress": "接收地址",
|
"toAddress": "接收地址",
|
||||||
"defaultSelf": "默认为自己",
|
"defaultSelf": "默认为自己",
|
||||||
"approvedAmount": "已授权额度",
|
"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": {
|
"factory": {
|
||||||
"title": "工厂管理",
|
"title": "工厂管理",
|
||||||
@@ -134,7 +161,18 @@
|
|||||||
"upgrade": "升级",
|
"upgrade": "升级",
|
||||||
"batchUpgradeVault": "批量升级金库",
|
"batchUpgradeVault": "批量升级金库",
|
||||||
"batchUpgrade": "批量升级",
|
"batchUpgrade": "批量升级",
|
||||||
"selectInBatchSection": "在批量操作区选择"
|
"selectInBatchSection": "在批量操作区选择",
|
||||||
|
"batchPauseUnpause": "批量暂停/恢复金库",
|
||||||
|
"pauseSelected": "暂停所选金库",
|
||||||
|
"unpauseSelected": "恢复所选金库",
|
||||||
|
"pauseWarning": "警告: 暂停金库将阻止所有买入/卖出操作",
|
||||||
|
"pauseVault": "暂停",
|
||||||
|
"unpauseVault": "恢复",
|
||||||
|
"paused": "已暂停",
|
||||||
|
"active": "运行中",
|
||||||
|
"batchCreateVaults": "批量创建金库",
|
||||||
|
"addVaultRow": "添加金库",
|
||||||
|
"createBatch": "批量创建"
|
||||||
},
|
},
|
||||||
"language": {
|
"language": {
|
||||||
"en": "英文",
|
"en": "英文",
|
||||||
|
|||||||
Reference in New Issue
Block a user