feat: 完善工厂和金库管理功能

Factory 功能完善:
- 添加 setDefaultHardCap 设置默认硬顶
- 添加 vaultImplementation 显示
- 添加批量操作: updateVaultPricesBatch, setHardCapBatch, setVaultNextRedemptionTimeBatch
- 添加高级功能: setVaultImplementation, upgradeVault, upgradeVaultBatch
- Owner 配置和批量操作区域 UI

Vault 功能完善:
- 添加 Manager 面板 (depositManagedAssets/withdrawForManagement)
- 修复 Manager 存入托管资产需要授权的问题
- 添加 managedAssets 读取和显示

UI 优化:
- 统一颜色方案
- 边界测试卡片式布局
- 可折叠区域统一样式

WUSD 功能:
- 添加销毁功能
- 修复 gas 配置问题

翻译更新:
- 添加所有新功能的中英文翻译

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-17 18:01:14 +00:00
parent 3da0bf24d0
commit 79aceccec3
9 changed files with 2252 additions and 766 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Markdown files
*.md
# Node modules
node_modules/
# Build output
dist/
dist-ssr/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor
.vscode/
.idea/
*.sw?
.DS_Store
# Local env files
*.local
# Claude plans
.claude/

View File

@@ -1,3 +1,33 @@
/* 统一颜色变量 */
:root {
/* 主色 */
--color-primary: #1976d2;
--color-primary-dark: #1565c0;
--color-primary-light: #e3f2fd;
/* 文字 */
--color-text: #333;
--color-text-secondary: #666;
--color-text-muted: #999;
/* 背景 */
--color-bg: #f5f5f5;
--color-bg-card: #fff;
--color-bg-section: #f8f9fa;
/* 边框 */
--color-border: #e0e0e0;
--color-border-light: #eee;
/* 状态色 - 仅用于状态指示 */
--color-success: #4caf50;
--color-success-bg: #e8f5e9;
--color-warning: #ff9800;
--color-warning-bg: #fff3e0;
--color-error: #f44336;
--color-error-bg: #ffebee;
}
* {
box-sizing: border-box;
margin: 0;
@@ -6,8 +36,8 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: #f5f5f5;
color: #333;
background-color: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
}
@@ -453,42 +483,62 @@ body {
.test-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.test-card {
display: flex;
align-items: stretch;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 14px;
border-radius: 6px;
overflow: hidden;
}
.test-header {
.test-card-left {
flex: 1;
padding: 8px 10px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
flex-direction: column;
gap: 2px;
}
.test-name {
.test-card-left .test-name {
font-size: 12px;
font-weight: 600;
font-size: 14px;
color: #1a1a1a;
color: #333;
}
.test-error {
.test-card-left .test-error {
font-size: 10px;
color: #999;
}
.test-card-left .test-desc {
font-size: 10px;
color: #666;
margin: 0;
}
.test-card-right {
display: flex;
align-items: center;
padding: 0 10px;
background: #f8f9fa;
border-left: 1px solid #e0e0e0;
}
.test-card-right .btn {
font-size: 11px;
color: #f57c00;
background: #fff3e0;
padding: 2px 8px;
border-radius: 4px;
padding: 4px 12px;
}
.test-desc {
font-size: 12px;
color: #666;
margin-bottom: 12px;
font-size: 11px;
color: #999;
margin: 0;
line-height: 1.3;
}
.test-actions {

View File

@@ -22,6 +22,17 @@ export function FactoryPanel() {
ytPrice: '',
})
const [showPermissionTest, setShowPermissionTest] = useState(false)
const [showOwnerConfig, setShowOwnerConfig] = useState(false)
const [newDefaultHardCap, setNewDefaultHardCap] = useState('')
const [showBatchOps, setShowBatchOps] = useState(false)
const [batchPriceForm, setBatchPriceForm] = useState({ wusdPrice: '1', ytPrice: '1' })
const [batchHardCapForm, setBatchHardCapForm] = useState('')
const [selectedVaultsForBatch, setSelectedVaultsForBatch] = useState<string[]>([])
const [showAdvanced, setShowAdvanced] = useState(false)
const [newImplementation, setNewImplementation] = useState('')
const [upgradeVaultAddr, setUpgradeVaultAddr] = useState('')
const [upgradeImplAddr, setUpgradeImplAddr] = useState('')
const [batchRedemptionTime, setBatchRedemptionTime] = useState('')
const { data: allVaults, refetch: refetchVaults } = useReadContract({
address: CONTRACTS.FACTORY,
@@ -41,12 +52,18 @@ export function FactoryPanel() {
functionName: 'owner',
})
const { data: defaultHardCap } = useReadContract({
const { data: defaultHardCap, refetch: refetchDefaultHardCap } = useReadContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'defaultHardCap',
})
const { data: vaultImplementation } = useReadContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'vaultImplementation',
})
const { writeContract, data: hash, isPending, reset, error: writeError, status: writeStatus } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
@@ -56,6 +73,7 @@ export function FactoryPanel() {
useEffect(() => {
if (isSuccess) {
refetchVaults()
refetchDefaultHardCap()
reset()
}
}, [isSuccess])
@@ -108,6 +126,125 @@ export function FactoryPanel() {
})
}
const handleSetDefaultHardCap = () => {
if (!newDefaultHardCap) return
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setDefaultHardCap',
args: [parseUnits(newDefaultHardCap, 18)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 批量更新价格
const handleBatchUpdatePrices = () => {
if (selectedVaultsForBatch.length === 0) return
const wusdPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.wusdPrice, 18))
const ytPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.ytPrice, 18))
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'updateVaultPricesBatch',
args: [selectedVaultsForBatch as `0x${string}`[], wusdPrices, ytPrices],
gas: GAS_CONFIG.VERY_COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 批量设置硬顶
const handleBatchSetHardCap = () => {
if (selectedVaultsForBatch.length === 0 || !batchHardCapForm) return
const hardCaps = selectedVaultsForBatch.map(() => parseUnits(batchHardCapForm, 18))
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setHardCapBatch',
args: [selectedVaultsForBatch as `0x${string}`[], hardCaps],
gas: GAS_CONFIG.VERY_COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 切换选中金库
const toggleVaultSelection = (vaultAddr: string) => {
setSelectedVaultsForBatch(prev =>
prev.includes(vaultAddr)
? prev.filter(v => v !== vaultAddr)
: [...prev, vaultAddr]
)
}
// 全选/取消全选
const toggleSelectAll = () => {
if (allVaults && selectedVaultsForBatch.length === allVaults.length) {
setSelectedVaultsForBatch([])
} else if (allVaults) {
setSelectedVaultsForBatch([...allVaults])
}
}
// 批量设置赎回时间
const handleBatchSetRedemptionTime = () => {
if (selectedVaultsForBatch.length === 0 || !batchRedemptionTime) return
const timestamp = BigInt(Math.floor(new Date(batchRedemptionTime).getTime() / 1000))
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setVaultNextRedemptionTimeBatch',
args: [selectedVaultsForBatch as `0x${string}`[], timestamp],
gas: GAS_CONFIG.VERY_COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置金库实现地址
const handleSetVaultImplementation = () => {
if (!newImplementation) return
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setVaultImplementation',
args: [newImplementation as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 升级单个金库
const handleUpgradeVault = () => {
if (!upgradeVaultAddr || !upgradeImplAddr) return
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'upgradeVault',
args: [upgradeVaultAddr as `0x${string}`, upgradeImplAddr as `0x${string}`],
gas: GAS_CONFIG.COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 批量升级金库
const handleBatchUpgradeVault = () => {
if (selectedVaultsForBatch.length === 0 || !upgradeImplAddr) return
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'upgradeVaultBatch',
args: [selectedVaultsForBatch as `0x${string}`[], upgradeImplAddr as `0x${string}`],
gas: GAS_CONFIG.VERY_COMPLEX,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 权限测试函数
const runPermissionTest = (testType: string) => {
const testVault = allVaults && allVaults.length > 0 ? allVaults[0] : CONTRACTS.VAULTS.YT_A
@@ -168,6 +305,10 @@ export function FactoryPanel() {
<span>{t('factory.owner')}:</span>
<code>{owner || '-'}</code>
</div>
<div className="info-row">
<span>{t('factory.vaultImpl')}:</span>
<code style={{ fontSize: '10px' }}>{vaultImplementation || '-'}</code>
</div>
<div className="info-row">
<span>{t('factory.defaultHardCap')}:</span>
<strong>{defaultHardCap ? formatUnits(defaultHardCap, 18) : '0'}</strong>
@@ -332,14 +473,290 @@ export function FactoryPanel() {
</>
)}
{/* Owner 配置区域 */}
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowOwnerConfig(!showOwnerConfig)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('factory.ownerConfig')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showOwnerConfig ? '▼' : '▶'}</span>
</div>
{showOwnerConfig && (
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
{/* 设置默认硬顶 */}
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}>
{t('factory.setDefaultHardCap')}
</label>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="number"
value={newDefaultHardCap}
onChange={(e) => setNewDefaultHardCap(e.target.value)}
placeholder={defaultHardCap ? formatUnits(defaultHardCap, 18) : '1000000'}
className="input"
style={{ flex: 1 }}
/>
<button
onClick={handleSetDefaultHardCap}
disabled={isProcessing || !newDefaultHardCap}
className="btn btn-primary"
>
{isProcessing ? '...' : t('common.confirm')}
</button>
</div>
</div>
</div>
)}
</div>
{/* 批量操作区域 */}
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowBatchOps(!showBatchOps)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('factory.batchOperations')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showBatchOps ? '▼' : '▶'}</span>
</div>
{showBatchOps && (
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
{/* 金库选择 */}
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<label style={{ fontSize: '13px', fontWeight: 500 }}>{t('factory.selectVaultsForBatch')}</label>
<button onClick={toggleSelectAll} className="btn btn-secondary btn-sm">
{allVaults && selectedVaultsForBatch.length === allVaults.length ? t('factory.deselectAll') : t('factory.selectAll')}
</button>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
{allVaults?.map((vault, index) => (
<label key={index} style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '4px 8px',
background: selectedVaultsForBatch.includes(vault) ? '#e3f2fd' : '#f8f9fa',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
border: selectedVaultsForBatch.includes(vault) ? '1px solid #2196f3' : '1px solid #e0e0e0'
}}>
<input
type="checkbox"
checked={selectedVaultsForBatch.includes(vault)}
onChange={() => toggleVaultSelection(vault)}
style={{ margin: 0 }}
/>
Vault {index + 1}
</label>
))}
</div>
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
{t('factory.selectedCount')}: {selectedVaultsForBatch.length}
</div>
</div>
{/* 批量更新价格 */}
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.batchUpdatePrices')}</h5>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: '8px', alignItems: 'end' }}>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('vault.wusdPrice')}</label>
<input
type="number"
value={batchPriceForm.wusdPrice}
onChange={(e) => setBatchPriceForm({ ...batchPriceForm, wusdPrice: e.target.value })}
className="input"
style={{ fontSize: '13px' }}
/>
</div>
<div>
<label style={{ fontSize: '11px', color: '#666' }}>{t('vault.ytPrice')}</label>
<input
type="number"
value={batchPriceForm.ytPrice}
onChange={(e) => setBatchPriceForm({ ...batchPriceForm, ytPrice: e.target.value })}
className="input"
style={{ fontSize: '13px' }}
/>
</div>
<button
onClick={handleBatchUpdatePrices}
disabled={isProcessing || selectedVaultsForBatch.length === 0}
className="btn btn-primary btn-sm"
>
{isProcessing ? '...' : t('factory.update')}
</button>
</div>
</div>
{/* 批量设置硬顶 */}
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.batchSetHardCap')}</h5>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="number"
value={batchHardCapForm}
onChange={(e) => setBatchHardCapForm(e.target.value)}
placeholder="100000"
className="input"
style={{ flex: 1, fontSize: '13px' }}
/>
<button
onClick={handleBatchSetHardCap}
disabled={isProcessing || selectedVaultsForBatch.length === 0 || !batchHardCapForm}
className="btn btn-primary btn-sm"
>
{isProcessing ? '...' : t('vault.setHardCap')}
</button>
</div>
</div>
{/* 批量设置赎回时间 */}
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.batchSetRedemptionTime')}</h5>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="datetime-local"
value={batchRedemptionTime}
onChange={(e) => setBatchRedemptionTime(e.target.value)}
className="input"
style={{ flex: 1, fontSize: '13px' }}
/>
<button
onClick={handleBatchSetRedemptionTime}
disabled={isProcessing || selectedVaultsForBatch.length === 0 || !batchRedemptionTime}
className="btn btn-primary btn-sm"
>
{isProcessing ? '...' : t('common.confirm')}
</button>
</div>
</div>
</div>
)}
</div>
{/* 高级功能区域 */}
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowAdvanced(!showAdvanced)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('factory.advancedFunctions')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showAdvanced ? '▼' : '▶'}</span>
</div>
{showAdvanced && (
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
{/* 警告提示 */}
<div style={{ marginBottom: '16px', padding: '8px', background: '#fff3e0', borderRadius: '4px', fontSize: '12px', color: '#e65100' }}>
{t('factory.advancedWarning')}
</div>
{/* 设置金库实现 */}
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.setVaultImplementation')}</h5>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={newImplementation}
onChange={(e) => setNewImplementation(e.target.value)}
placeholder="0x..."
className="input"
style={{ flex: 1, fontSize: '12px' }}
/>
<button
onClick={handleSetVaultImplementation}
disabled={isProcessing || !newImplementation}
className="btn btn-secondary btn-sm"
>
{isProcessing ? '...' : t('common.confirm')}
</button>
</div>
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
{t('factory.currentImpl')}: <code style={{ fontSize: '10px' }}>{vaultImplementation || '-'}</code>
</div>
</div>
{/* 升级单个金库 */}
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.upgradeVault')}</h5>
<div style={{ marginBottom: '8px' }}>
<select
value={upgradeVaultAddr}
onChange={(e) => setUpgradeVaultAddr(e.target.value)}
className="input"
style={{ fontSize: '12px', marginBottom: '8px' }}
>
<option value="">{t('factory.selectVault')}</option>
{allVaults?.map((vault, index) => (
<option key={index} value={vault}>
Vault {index + 1}: {vault.slice(0, 10)}...
</option>
))}
</select>
<input
type="text"
value={upgradeImplAddr}
onChange={(e) => setUpgradeImplAddr(e.target.value)}
placeholder={t('factory.newImplAddress')}
className="input"
style={{ fontSize: '12px' }}
/>
</div>
<button
onClick={handleUpgradeVault}
disabled={isProcessing || !upgradeVaultAddr || !upgradeImplAddr}
className="btn btn-secondary btn-sm"
>
{isProcessing ? '...' : t('factory.upgrade')}
</button>
</div>
{/* 批量升级金库 */}
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.batchUpgradeVault')}</h5>
<div style={{ fontSize: '11px', color: '#666', marginBottom: '8px' }}>
{t('factory.selectedCount')}: {selectedVaultsForBatch.length} ({t('factory.selectInBatchSection')})
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={upgradeImplAddr}
onChange={(e) => setUpgradeImplAddr(e.target.value)}
placeholder={t('factory.newImplAddress')}
className="input"
style={{ flex: 1, fontSize: '12px' }}
/>
<button
onClick={handleBatchUpgradeVault}
disabled={isProcessing || selectedVaultsForBatch.length === 0 || !upgradeImplAddr}
className="btn btn-secondary btn-sm"
>
{isProcessing ? '...' : t('factory.batchUpgrade')}
</button>
</div>
</div>
</div>
)}
</div>
{/* 权限测试区域 */}
<div className="section">
<div className="section-header" onClick={() => setShowPermissionTest(!showPermissionTest)} style={{ cursor: 'pointer' }}>
<h3>{t('test.permissionTests')} {showPermissionTest ? '[-]' : '[+]'}</h3>
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowPermissionTest(!showPermissionTest)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('test.permissionTests')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showPermissionTest ? '▼' : '▶'}</span>
</div>
{showPermissionTest && (
<>
<div style={{ marginTop: '10px' }}>
<div className="test-hint">
{t('test.permissionHint')}
{isOwner && <span style={{ color: '#2e7d32', marginLeft: '8px' }}>(You are Owner)</span>}
@@ -347,28 +764,27 @@ export function FactoryPanel() {
<div className="test-grid">
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.updatePriceNotOwner')}</span>
<span className="test-error">OwnableUnauthorizedAccount</span>
<span className="test-error">Unauthorized</span>
<p className="test-desc">{t('test.updatePriceNotOwnerDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runPermissionTest('update_price_not_owner')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.updatePriceNotOwnerDesc')}</p>
<button onClick={() => runPermissionTest('update_price_not_owner')} disabled={isProcessing} className="btn btn-danger btn-sm">
{t('test.run')}
</button>
</div>
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.setManagerNotOwner')}</span>
<span className="test-error">OwnableUnauthorizedAccount</span>
<span className="test-error">Unauthorized</span>
<p className="test-desc">{t('test.setManagerNotOwnerDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runPermissionTest('set_manager_not_owner')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.setManagerNotOwnerDesc')}</p>
<button onClick={() => runPermissionTest('set_manager_not_owner')} disabled={isProcessing} className="btn btn-danger btn-sm">
{t('test.run')}
</button>
</div>
</div>
</>
</div>
)}
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,19 @@ export function VaultPanel() {
const [testResult, setTestResult] = useState<{ type: 'success' | 'error' | 'pending', msg: string } | null>(null)
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
// 管理员配置状态
const [showAdminConfig, setShowAdminConfig] = useState(false)
const [priceUpdateForm, setPriceUpdateForm] = useState({ wusdPrice: '1', ytPrice: '1' })
const [hardCapForm, setHardCapForm] = useState('')
const [redemptionTimeForm, setRedemptionTimeForm] = useState('')
const [managerForm, setManagerForm] = useState('')
// Manager 资产管理状态
const [showManagerPanel, setShowManagerPanel] = useState(false)
const [depositManagedAmount, setDepositManagedAmount] = useState('')
const [withdrawManagedAmount, setWithdrawManagedAmount] = useState('')
const [withdrawToAddress, setWithdrawToAddress] = useState('')
// 从合约动态读取所有金库地址
const { data: allVaultsData } = useReadContract({
address: CONTRACTS.FACTORY,
@@ -139,6 +152,21 @@ export function VaultPanel() {
functionName: 'manager',
})
// 读取 managedAssets (Manager 管理的资产)
const { data: managedAssets, refetch: refetchManagedAssets } = useReadContract({
address: selectedVault.address as `0x${string}`,
abi: VAULT_ABI,
functionName: 'managedAssets',
})
// 读取 Manager 对 Vault 的 WUSD 授权额度 (用于 depositManagedAssets)
const { data: managerWusdAllowance, refetch: refetchManagerAllowance } = useReadContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'allowance',
args: address ? [address, selectedVault.address as `0x${string}`] : undefined,
})
const { data: factoryOwner } = useReadContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
@@ -192,8 +220,12 @@ export function VaultPanel() {
refetchYtBalance()
refetchWusdBalance()
refetchAllowance()
refetchManagedAssets()
refetchManagerAllowance()
setBuyAmount('')
setSellAmount('')
setDepositManagedAmount('')
setWithdrawManagedAmount('')
reset()
}
}, [isSuccess])
@@ -333,6 +365,125 @@ export function VaultPanel() {
})
}
// ===== 管理员功能 =====
// 更新金库价格 (Factory.updateVaultPrices)
const handleUpdatePrices = () => {
if (!address) return
const wusdPrice = parseUnits(priceUpdateForm.wusdPrice, 18)
const ytPrice = parseUnits(priceUpdateForm.ytPrice, 18)
recordTx('test', `${priceUpdateForm.wusdPrice}/${priceUpdateForm.ytPrice}`, 'UpdatePrices')
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'updateVaultPrices',
args: [selectedVault.address as `0x${string}`, wusdPrice, ytPrice],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置硬顶 (Factory.setHardCap)
const handleSetHardCap = () => {
if (!address || !hardCapForm) return
const hardCap = parseUnits(hardCapForm, 18)
recordTx('test', hardCapForm, 'SetHardCap')
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setHardCap',
args: [selectedVault.address as `0x${string}`, hardCap],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置赎回时间 (Factory.setVaultNextRedemptionTime)
const handleSetRedemptionTime = () => {
if (!address || !redemptionTimeForm) return
const timestamp = BigInt(Math.floor(new Date(redemptionTimeForm).getTime() / 1000))
recordTx('test', redemptionTimeForm, 'SetRedemptionTime')
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setVaultNextRedemptionTime',
args: [selectedVault.address as `0x${string}`, timestamp],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 设置管理员 (Factory.setVaultManager)
const handleSetManager = () => {
if (!address || !managerForm) return
recordTx('test', managerForm.slice(0, 10) + '...', 'SetManager')
writeContract({
address: CONTRACTS.FACTORY,
abi: FACTORY_ABI,
functionName: 'setVaultManager',
args: [selectedVault.address as `0x${string}`, managerForm as `0x${string}`],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// ===== Manager 资产管理功能 =====
// Manager 授权 WUSD 给 Vault (用于 depositManagedAssets)
const handleManagerApproveWusd = () => {
if (!address || !depositManagedAmount) return
recordTx('approve', depositManagedAmount, 'WUSD')
writeContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'approve',
args: [selectedVault.address as `0x${string}`, parseUnits(depositManagedAmount, 18)],
gas: GAS_CONFIG.SIMPLE,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 存入托管资产 (Manager 将资产从外部存回金库)
const handleDepositManagedAssets = () => {
if (!address || !depositManagedAmount) return
recordTx('test', depositManagedAmount, 'DepositManaged')
writeContract({
address: selectedVault.address as `0x${string}`,
abi: VAULT_ABI,
functionName: 'depositManagedAssets',
args: [parseUnits(depositManagedAmount, 18)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 检查 Manager 是否需要授权
const managerNeedsApproval = depositManagedAmount
? (managerWusdAllowance === undefined || parseUnits(depositManagedAmount, 18) > managerWusdAllowance)
: false
// 提取托管资产 (Manager 从金库提取资产进行管理)
const handleWithdrawForManagement = () => {
if (!address || !withdrawManagedAmount) return
const toAddress = withdrawToAddress || address
recordTx('test', withdrawManagedAmount, 'WithdrawManaged')
writeContract({
address: selectedVault.address as `0x${string}`,
abi: VAULT_ABI,
functionName: 'withdrawForManagement',
args: [toAddress as `0x${string}`, parseUnits(withdrawManagedAmount, 18)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
// 边界测试是否正在运行
const [isTestRunning, setIsTestRunning] = useState(false)
const testTypeRef = useRef<string | null>(null)
@@ -459,8 +610,9 @@ export function VaultPanel() {
}
}, [isSuccess, isTestRunning])
const needsApproval = buyAmount && wusdAllowance !== undefined
? parseUnits(buyAmount, 18) > wusdAllowance
// 修复: 当 wusdAllowance 未加载时,假设需要授权 (安全起见)
const needsApproval = buyAmount
? (wusdAllowance === undefined || parseUnits(buyAmount, 18) > wusdAllowance)
: false
// 格式化剩余时间
@@ -611,6 +763,11 @@ export function VaultPanel() {
{t('vault.youWillReceive')}: <strong>{formatUnits(previewBuyAmount, 18)} YT</strong>
</div>
)}
{/* Debug: 显示授权状态 */}
<div className="debug-info" style={{ fontSize: '12px', color: '#999', marginBottom: '8px' }}>
: {wusdAllowance !== undefined ? formatUnits(wusdAllowance, 18) : 'loading...'} WUSD
{buyAmount && ` | 需要: ${buyAmount} | 需授权: ${needsApproval ? '是' : '否'}`}
</div>
{needsApproval ? (
<button
onClick={handleApprove}
@@ -670,14 +827,230 @@ export function VaultPanel() {
</div>
)}
{/* 管理员配置区域 */}
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowAdminConfig(!showAdminConfig)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('vault.adminConfig')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showAdminConfig ? '▼' : '▶'}</span>
</div>
{showAdminConfig && (
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
<div className="admin-info" style={{ marginBottom: '16px', padding: '8px', background: '#fff3e0', borderRadius: '4px', fontSize: '12px' }}>
<p style={{ margin: 0 }}>Factory Owner: <code style={{ fontSize: '10px' }}>{factoryOwner || 'Loading...'}</code></p>
<p style={{ margin: '4px 0 0 0' }}>Vault Manager: <code style={{ fontSize: '10px' }}>{vaultManager || 'Loading...'}</code></p>
<p style={{ margin: '4px 0 0 0', color: address && factoryOwner && address.toLowerCase() === (factoryOwner as string).toLowerCase() ? '#2e7d32' : '#e65100' }}>
{address && factoryOwner && address.toLowerCase() === (factoryOwner as string).toLowerCase() ? 'You are Factory Owner' : 'You are not Factory Owner'}
</p>
</div>
{/* 更新价格 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.updatePrices')}</h4>
<div className="form-grid" style={{ marginBottom: '16px' }}>
<div className="form-group">
<label>{t('vault.wusdPrice')}</label>
<input
type="number"
value={priceUpdateForm.wusdPrice}
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, wusdPrice: e.target.value })}
placeholder="1"
className="input"
step="0.01"
/>
</div>
<div className="form-group">
<label>{t('vault.ytPrice')}</label>
<input
type="number"
value={priceUpdateForm.ytPrice}
onChange={(e) => setPriceUpdateForm({ ...priceUpdateForm, ytPrice: e.target.value })}
placeholder="1"
className="input"
step="0.01"
/>
</div>
<div className="form-group" style={{ display: 'flex', alignItems: 'flex-end' }}>
<button onClick={handleUpdatePrices} disabled={isPending || isConfirming} className="btn btn-primary">
{isPending || isConfirming ? '...' : t('vault.updatePrices')}
</button>
</div>
</div>
{/* 设置硬顶 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.setHardCap')}</h4>
<div className="form-grid" style={{ marginBottom: '16px' }}>
<div className="form-group">
<label>{t('vault.newHardCap')}</label>
<input
type="number"
value={hardCapForm}
onChange={(e) => setHardCapForm(e.target.value)}
placeholder="100000"
className="input"
/>
</div>
<div className="form-group" style={{ display: 'flex', alignItems: 'flex-end' }}>
<button onClick={handleSetHardCap} disabled={isPending || isConfirming || !hardCapForm} className="btn btn-secondary">
{isPending || isConfirming ? '...' : t('vault.setHardCap')}
</button>
</div>
</div>
{/* 设置赎回时间 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.setRedemptionTime')}</h4>
<div className="form-grid" style={{ marginBottom: '16px' }}>
<div className="form-group">
<label>{t('vault.newRedemptionTime')}</label>
<input
type="datetime-local"
value={redemptionTimeForm}
onChange={(e) => setRedemptionTimeForm(e.target.value)}
className="input"
/>
</div>
<div className="form-group" style={{ display: 'flex', alignItems: 'flex-end' }}>
<button onClick={handleSetRedemptionTime} disabled={isPending || isConfirming || !redemptionTimeForm} className="btn btn-secondary">
{isPending || isConfirming ? '...' : t('vault.setRedemptionTime')}
</button>
</div>
</div>
{/* 设置管理员 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.setManager')}</h4>
<div className="form-grid">
<div className="form-group">
<label>{t('vault.newManager')}</label>
<input
type="text"
value={managerForm}
onChange={(e) => setManagerForm(e.target.value)}
placeholder="0x..."
className="input"
/>
</div>
<div className="form-group" style={{ display: 'flex', alignItems: 'flex-end' }}>
<button onClick={handleSetManager} disabled={isPending || isConfirming || !managerForm} className="btn btn-secondary">
{isPending || isConfirming ? '...' : t('vault.setManager')}
</button>
</div>
</div>
</div>
)}
</div>
{/* Manager 资产管理区域 */}
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowManagerPanel(!showManagerPanel)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('vault.managerPanel')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showManagerPanel ? '▼' : '▶'}</span>
</div>
{showManagerPanel && (
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
{/* 当前托管资产信息 */}
<div style={{ marginBottom: '16px', padding: '8px', background: '#f8f9fa', borderRadius: '4px' }}>
<div className="info-row">
<span>{t('vault.managedAssets')}:</span>
<strong>{managedAssets ? formatUnits(managedAssets, 18) : '0'} WUSD</strong>
</div>
<div className="info-row">
<span>{t('vault.idleAssets')}:</span>
<strong>{vaultInfo ? formatUnits(vaultInfo[1], 18) : '0'} WUSD</strong>
</div>
<div className="info-row" style={{ fontSize: '11px', color: address && vaultManager && address.toLowerCase() === vaultManager.toLowerCase() ? '#2e7d32' : '#e65100' }}>
{address && vaultManager && address.toLowerCase() === vaultManager.toLowerCase()
? t('vault.youAreManager')
: t('vault.youAreNotManager')}
</div>
</div>
{/* 存入托管资产 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.depositManagedAssets')}</h4>
<div style={{ marginBottom: '16px' }}>
<input
type="number"
value={depositManagedAmount}
onChange={(e) => setDepositManagedAmount(e.target.value)}
placeholder={t('common.amount')}
className="input"
style={{ marginBottom: '8px' }}
/>
{/* 授权状态提示 */}
<div style={{ fontSize: '11px', color: '#666', marginBottom: '8px' }}>
{t('vault.approvedAmount')}: {managerWusdAllowance !== undefined ? formatUnits(managerWusdAllowance, 18) : '...'} WUSD
{depositManagedAmount && ` | ${t('vault.needApprove')}: ${managerNeedsApproval ? t('test.yes') : t('test.no')}`}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
{managerNeedsApproval ? (
<button
onClick={handleManagerApproveWusd}
disabled={isPending || isConfirming || !depositManagedAmount}
className="btn btn-secondary"
style={{ flex: 1 }}
>
{isPending || isConfirming ? '...' : t('vault.approveWusd')}
</button>
) : (
<button
onClick={handleDepositManagedAssets}
disabled={isPending || isConfirming || !depositManagedAmount}
className="btn btn-primary"
style={{ flex: 1 }}
>
{isPending || isConfirming ? '...' : t('vault.deposit')}
</button>
)}
</div>
</div>
{/* 提取托管资产 */}
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>{t('vault.withdrawForManagement')}</h4>
<div style={{ marginBottom: '8px' }}>
<input
type="number"
value={withdrawManagedAmount}
onChange={(e) => setWithdrawManagedAmount(e.target.value)}
placeholder={t('common.amount')}
className="input"
style={{ marginBottom: '8px' }}
/>
<input
type="text"
value={withdrawToAddress}
onChange={(e) => setWithdrawToAddress(e.target.value)}
placeholder={t('vault.toAddress') + ' (' + t('vault.defaultSelf') + ')'}
className="input"
/>
</div>
<button
onClick={handleWithdrawForManagement}
disabled={isPending || isConfirming || !withdrawManagedAmount}
className="btn btn-secondary"
>
{isPending || isConfirming ? '...' : t('vault.withdraw')}
</button>
</div>
)}
</div>
{/* 边界测试区域 */}
<div className="section">
<div className="section-header" onClick={() => setShowBoundaryTest(!showBoundaryTest)} style={{ cursor: 'pointer' }}>
<h3>{t('test.boundaryTests')} {showBoundaryTest ? '[-]' : '[+]'}</h3>
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowBoundaryTest(!showBoundaryTest)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('test.boundaryTests')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showBoundaryTest ? '▼' : '▶'}</span>
</div>
{showBoundaryTest && (
<>
<div style={{ marginTop: '10px' }}>
<div className="test-hint">{t('test.boundaryHint')}</div>
{/* 赎回状态 */}
@@ -692,76 +1065,75 @@ export function VaultPanel() {
<div className="test-grid">
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.buyZero')}</span>
<span className="test-error">InvalidAmount</span>
<p className="test-desc">{t('test.buyZeroDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('buy_zero')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.buyZeroDesc')}</p>
<button onClick={() => runBoundaryTest('buy_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
</button>
</div>
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.sellZero')}</span>
<span className="test-error">InvalidAmount</span>
<p className="test-desc">{t('test.sellZeroDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('sell_zero')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.sellZeroDesc')}</p>
<button onClick={() => runBoundaryTest('sell_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
</button>
</div>
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.buyExceedBalance')}</span>
<span className="test-error">InsufficientWUSD</span>
<p className="test-desc">{t('test.buyExceedBalanceDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('buy_exceed_balance')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.buyExceedBalanceDesc')}</p>
<button onClick={() => runBoundaryTest('buy_exceed_balance')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
</button>
</div>
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.sellExceedBalance')}</span>
<span className="test-error">InsufficientYTA</span>
<span className="test-error">InsufficientYT</span>
<p className="test-desc">{t('test.sellExceedBalanceDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('sell_exceed_balance')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.sellExceedBalanceDesc')}</p>
<button onClick={() => runBoundaryTest('sell_exceed_balance')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
</button>
</div>
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.buyExceedHardcap')}</span>
<span className="test-error">HardCapExceeded</span>
<p className="test-desc">{t('test.buyExceedHardcapDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('buy_exceed_hardcap')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.buyExceedHardcapDesc')}</p>
<button onClick={() => runBoundaryTest('buy_exceed_hardcap')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
</button>
</div>
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.sellInLock')}</span>
<span className="test-error">StillInLockPeriod</span>
<p className="test-desc">{t('test.sellInLockDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('sell_in_lock')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
</div>
<div className="test-card">
<div className="test-card-left">
<span className="test-name">{t('test.maxApprove')}</span>
<span className="test-error">MaxUint256</span>
<p className="test-desc">{t('test.maxApproveDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('max_approve')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.sellInLockDesc')}</p>
<button onClick={() => runBoundaryTest('sell_in_lock')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
</button>
</div>
</div>
<div className="quick-actions" style={{ marginTop: '16px' }}>
<button onClick={() => runBoundaryTest('max_approve')} disabled={isProcessing} className="btn btn-secondary">
{t('test.maxApprove')}
</button>
</div>
{testResult && (
@@ -769,7 +1141,7 @@ export function VaultPanel() {
{testResult.msg}
</div>
)}
</>
</div>
)}
</div>

View File

@@ -14,6 +14,7 @@ export function WUSDPanel() {
const { transactions, addTransaction, updateTransaction, clearHistory } = useTransactions()
const { showToast } = useToast()
const [mintAmount, setMintAmount] = useState('')
const [burnAmount, setBurnAmount] = useState('')
const [showBoundaryTest, setShowBoundaryTest] = useState(false)
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
@@ -161,7 +162,21 @@ export function WUSDPanel() {
abi: WUSD_ABI,
functionName: 'mint',
args: [address, parseUnits(mintAmount, decimals)],
gas: GAS_CONFIG.SIMPLE,
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
}
const handleBurn = async () => {
if (!address || !burnAmount || !decimals) return
recordTx('burn', burnAmount, 'WUSD')
writeContract({
address: CONTRACTS.WUSD,
abi: WUSD_ABI,
functionName: 'burn',
args: [address, parseUnits(burnAmount, decimals)],
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
@@ -178,7 +193,7 @@ export function WUSDPanel() {
abi: WUSD_ABI,
functionName: 'mint',
args: [address, BigInt(0)],
gas: GAS_CONFIG.SIMPLE,
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
@@ -190,7 +205,7 @@ export function WUSDPanel() {
abi: WUSD_ABI,
functionName: 'burn',
args: [address, exceedAmount],
gas: GAS_CONFIG.SIMPLE,
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
@@ -202,7 +217,7 @@ export function WUSDPanel() {
abi: WUSD_ABI,
functionName: 'mint',
args: [address, parseUnits('10000', decimals)],
gas: GAS_CONFIG.SIMPLE,
gas: GAS_CONFIG.STANDARD,
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
})
@@ -235,65 +250,101 @@ export function WUSDPanel() {
</strong>
</div>
<div className="form-group">
<label>{t('wusd.mintAmount')}</label>
<input
type="number"
value={mintAmount}
onChange={(e) => setMintAmount(e.target.value)}
placeholder={t('wusd.enterAmount')}
className="input"
/>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
{/* 铸造 */}
<div>
<div className="form-group">
<label>{t('wusd.mintAmount')}</label>
<input
type="number"
value={mintAmount}
onChange={(e) => setMintAmount(e.target.value)}
placeholder={t('wusd.enterAmount')}
className="input"
/>
</div>
<button
onClick={handleMint}
disabled={isProcessing || !mintAmount}
className="btn btn-primary"
style={{ width: '100%' }}
>
{isProcessing ? t('common.processing') : t('wusd.mint')}
</button>
</div>
{/* 销毁 */}
<div>
<div className="form-group">
<label>{t('wusd.burnAmount')}</label>
<input
type="number"
value={burnAmount}
onChange={(e) => setBurnAmount(e.target.value)}
placeholder={t('wusd.enterAmount')}
className="input"
/>
</div>
<button
onClick={handleBurn}
disabled={isProcessing || !burnAmount}
className="btn btn-secondary"
style={{ width: '100%' }}
>
{isProcessing ? t('common.processing') : t('wusd.burn')}
</button>
</div>
</div>
<button
onClick={handleMint}
disabled={isProcessing || !mintAmount}
className="btn btn-primary"
>
{isProcessing ? (isPending ? t('wusd.confirming') : t('wusd.minting')) : t('wusd.mint')}
</button>
{/* 边界测试区域 */}
<div className="section">
<div className="section-header" onClick={() => setShowBoundaryTest(!showBoundaryTest)} style={{ cursor: 'pointer' }}>
<h3>{t('test.boundaryTests')} {showBoundaryTest ? '[-]' : '[+]'}</h3>
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setShowBoundaryTest(!showBoundaryTest)}
>
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('test.boundaryTests')}</h4>
<span style={{ color: '#999', fontSize: '16px' }}>{showBoundaryTest ? '▼' : '▶'}</span>
</div>
{showBoundaryTest && (
<>
<div style={{ marginTop: '10px' }}>
<div className="test-hint">{t('test.boundaryHint')}</div>
<div className="test-grid">
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.mintZero')}</span>
<span className="test-error">MaySucceed</span>
<p className="test-desc">{t('test.mintZeroDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('mint_zero')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.mintZeroDesc')}</p>
<button onClick={() => runBoundaryTest('mint_zero')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
</button>
</div>
<div className="test-card">
<div className="test-header">
<div className="test-card-left">
<span className="test-name">{t('test.burnExceed')}</span>
<span className="test-error">ERC20InsufficientBalance</span>
<span className="test-error">InsufficientBalance</span>
<p className="test-desc">{t('test.burnExceedDesc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('burn_exceed')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
</div>
<div className="test-card">
<div className="test-card-left">
<span className="test-name">{t('test.mint10000')}</span>
<span className="test-error">Mint</span>
<p className="test-desc">{t('test.mint10000Desc')}</p>
</div>
<div className="test-card-right">
<button onClick={() => runBoundaryTest('mint_10000')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
</div>
<p className="test-desc">{t('test.burnExceedDesc')}</p>
<button onClick={() => runBoundaryTest('burn_exceed')} disabled={isProcessing} className="btn btn-warning btn-sm">
{t('test.run')}
</button>
</div>
</div>
<div className="quick-actions" style={{ marginTop: '16px' }}>
<button onClick={() => runBoundaryTest('mint_10000')} disabled={isProcessing} className="btn btn-primary">
{t('test.mint10000')}
</button>
</div>
</>
</div>
)}
</div>

View File

@@ -49,6 +49,7 @@ export const CONTRACTS = {
YT_A: '0x0cA35994F033685E7a57ef9bc5d00dd3cf927330' as const,
YT_B: '0x333805C9EE75f59Aa2Cc79DfDe2499F920c7b408' as const,
YT_C: '0x6DF0ED6f0345F601A206974973dE9fC970598587' as const,
YT_D: '0x5d91FD16fa85547b0784c377A47BF7706D7875d3' as const,
},
// LP Pool contracts
YT_REWARD_ROUTER: '0x51eEF57eC57c867AC23945f0ce21aA5A9a2C246c' as const,
@@ -189,6 +190,81 @@ export const FACTORY_ABI = [
outputs: [{ internalType: 'address', name: '', type: 'address' }],
stateMutability: 'view',
type: 'function'
},
// 批量获取金库(分页)
{
inputs: [
{ internalType: 'uint256', name: '_start', type: 'uint256' },
{ internalType: 'uint256', name: '_end', type: 'uint256' }
],
name: 'getVaults',
outputs: [{ internalType: 'address[]', name: 'vaults', type: 'address[]' }],
stateMutability: 'view',
type: 'function'
},
// 批量设置硬顶
{
inputs: [
{ internalType: 'address[]', name: '_vaults', type: 'address[]' },
{ internalType: 'uint256[]', name: '_hardCaps', type: 'uint256[]' }
],
name: 'setHardCapBatch',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
// 批量更新价格
{
inputs: [
{ internalType: 'address[]', name: '_vaults', type: 'address[]' },
{ internalType: 'uint256[]', name: '_wusdPrices', type: 'uint256[]' },
{ internalType: 'uint256[]', name: '_ytPrices', type: 'uint256[]' }
],
name: 'updateVaultPricesBatch',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
// 批量设置赎回时间
{
inputs: [
{ internalType: 'address[]', name: '_vaults', type: 'address[]' },
{ internalType: 'uint256', name: '_nextRedemptionTime', type: 'uint256' }
],
name: 'setVaultNextRedemptionTimeBatch',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
// 设置金库实现地址
{
inputs: [{ internalType: 'address', name: '_newImplementation', type: 'address' }],
name: 'setVaultImplementation',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
// 升级单个金库
{
inputs: [
{ internalType: 'address', name: '_vault', type: 'address' },
{ internalType: 'address', name: '_newImplementation', type: 'address' }
],
name: 'upgradeVault',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
// 批量升级金库
{
inputs: [
{ internalType: 'address[]', name: '_vaults', type: 'address[]' },
{ internalType: 'address', name: '_newImplementation', type: 'address' }
],
name: 'upgradeVaultBatch',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
}
] as const
@@ -974,5 +1050,120 @@ export const YT_VAULT_ABI = [
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
// 获取交换手续费率 - 用于滑点计算
{
inputs: [
{ internalType: 'address', name: '_tokenIn', type: 'address' },
{ internalType: 'address', name: '_tokenOut', type: 'address' },
{ internalType: 'uint256', name: '_usdyAmount', type: 'uint256' }
],
name: 'getSwapFeeBasisPoints',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
},
// 获取赎回手续费率 - 用于移除流动性滑点计算
{
inputs: [
{ internalType: 'address', name: '_token', type: 'address' },
{ internalType: 'uint256', name: '_usdyAmount', type: 'uint256' }
],
name: 'getRedemptionFeeBasisPoints',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
},
// 获取动态手续费率
{
inputs: [
{ internalType: 'address', name: '_token', type: 'address' },
{ internalType: 'uint256', name: '_usdyDelta', type: 'uint256' },
{ internalType: 'uint256', name: '_feeBasisPoints', type: 'uint256' },
{ internalType: 'uint256', name: '_taxBasisPoints', type: 'uint256' },
{ internalType: 'bool', name: '_increment', type: 'bool' }
],
name: 'getFeeBasisPoints',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
},
// 获取池子总价值
{
inputs: [{ internalType: 'bool', name: '_maximise', type: 'bool' }],
name: 'getPoolValue',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
},
// 获取所有池子代币
{
inputs: [],
name: 'getAllPoolTokens',
outputs: [{ internalType: 'address[]', name: '', type: 'address[]' }],
stateMutability: 'view',
type: 'function'
},
// 常量
{
inputs: [],
name: 'BASIS_POINTS_DIVISOR',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'PRICE_PRECISION',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [],
name: 'USDY_DECIMALS',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
},
// 紧急模式
{
inputs: [],
name: 'emergencyMode',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'view',
type: 'function'
},
// swap 开关
{
inputs: [],
name: 'swapEnabled',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'view',
type: 'function'
},
// 设置紧急模式
{
inputs: [{ internalType: 'bool', name: '_emergencyMode', type: 'bool' }],
name: 'setEmergencyMode',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
// 设置 swap 开关
{
inputs: [{ internalType: 'bool', name: '_isSwapEnabled', type: 'bool' }],
name: 'setSwapEnabled',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
},
// 稳定币标记
{
inputs: [{ internalType: 'address', name: '', type: 'address' }],
name: 'stableTokens',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'view',
type: 'function'
}
] as const

View File

@@ -33,8 +33,10 @@
"wusd": {
"title": "WUSD Token",
"mintAmount": "Mint Amount",
"enterAmount": "Enter amount to mint",
"mint": "Mint WUSD",
"burnAmount": "Burn Amount",
"enterAmount": "Enter amount",
"mint": "Mint",
"burn": "Burn",
"minting": "Minting...",
"confirming": "Confirming...",
"mintSuccess": "Mint successful!"
@@ -65,12 +67,33 @@
"redeemAvailable": "Available",
"redeemNotAvailable": "Not Available",
"redeemLocked": "Redemption Locked",
"timeRemaining": "Time Remaining"
"timeRemaining": "Time Remaining",
"adminConfig": "Admin Config",
"updatePrices": "Update Prices",
"setHardCap": "Set Hard Cap",
"setRedemptionTime": "Set Redemption Time",
"setManager": "Set Manager",
"newHardCap": "New Hard Cap",
"newManager": "New Manager Address",
"newRedemptionTime": "New Redemption Time",
"managerPanel": "Manager Panel",
"managedAssets": "Managed Assets",
"youAreManager": "You are the Manager of this vault",
"youAreNotManager": "You are not the Manager of this vault",
"depositManagedAssets": "Deposit Managed Assets",
"withdrawForManagement": "Withdraw For Management",
"deposit": "Deposit",
"withdraw": "Withdraw",
"toAddress": "To Address",
"defaultSelf": "default: self",
"approvedAmount": "Approved Amount",
"needApprove": "Need Approve"
},
"factory": {
"title": "Factory Management",
"factoryContract": "Factory Contract",
"owner": "Owner",
"vaultImpl": "Vault Implementation",
"defaultHardCap": "Default Hard Cap",
"totalVaults": "Total Vaults",
"yourRole": "Your Role",
@@ -91,7 +114,27 @@
"selectVault": "Select Vault",
"newWusdPrice": "New WUSD Price",
"newYtPrice": "New YT Price",
"update": "Update Prices"
"update": "Update Prices",
"ownerConfig": "Owner Config",
"setDefaultHardCap": "Set Default Hard Cap",
"batchOperations": "Batch Operations",
"selectVaultsForBatch": "Select Vaults",
"selectAll": "Select All",
"deselectAll": "Deselect All",
"selectedCount": "Selected",
"batchUpdatePrices": "Batch Update Prices",
"batchSetHardCap": "Batch Set Hard Cap",
"batchSetRedemptionTime": "Batch Set Redemption Time",
"advancedFunctions": "Advanced Functions",
"advancedWarning": "Warning: These are advanced management functions. Please ensure you understand the implications before proceeding. Incorrect operations may cause contract issues.",
"setVaultImplementation": "Set Vault Implementation",
"currentImpl": "Current Implementation",
"upgradeVault": "Upgrade Vault",
"newImplAddress": "New Implementation Address",
"upgrade": "Upgrade",
"batchUpgradeVault": "Batch Upgrade Vaults",
"batchUpgrade": "Batch Upgrade",
"selectInBatchSection": "select in batch section"
},
"language": {
"en": "English",
@@ -136,7 +179,9 @@
"run": "Run",
"running": "Running...",
"mint10000": "Mint 10000 WUSD",
"mint10000Desc": "Quick mint 10000 test WUSD",
"maxApprove": "Max Approve WUSD",
"maxApproveDesc": "Approve max uint256 amount",
"buyZero": "Buy Amount 0",
"buyZeroDesc": "Test depositYT(0)",
"sellZero": "Sell Amount 0",
@@ -207,6 +252,18 @@
"testSwapExceedDesc": "Swap amount exceeding token balance",
"yourTokenBalances": "Your Token Balances",
"whitelisted": "Whitelisted",
"notWhitelisted": "Not Whitelisted"
"notWhitelisted": "Not Whitelisted",
"emergencyMode": "Emergency Mode",
"emergencyModeOn": "ON (Warning)",
"emergencyModeOff": "Normal",
"swapEnabled": "Swap Function",
"swapEnabledOn": "Enabled",
"swapEnabledOff": "Disabled",
"totalTokenWeights": "Total Weights",
"accountValue": "Your Account Value",
"usdyAmount": "USDY Amount",
"isStableToken": "Stable Token",
"setStableToken": "Set Stable Token",
"stableTokenHint": "Mark token as stable, affects price calculation"
}
}

View File

@@ -33,8 +33,10 @@
"wusd": {
"title": "WUSD 代币",
"mintAmount": "铸造数量",
"enterAmount": "输入铸造数量",
"mint": "铸造 WUSD",
"burnAmount": "销毁数量",
"enterAmount": "输入数量",
"mint": "铸造",
"burn": "销毁",
"minting": "铸造中...",
"confirming": "确认中...",
"mintSuccess": "铸造成功!"
@@ -65,12 +67,33 @@
"redeemAvailable": "可以赎回",
"redeemNotAvailable": "暂不可赎回",
"redeemLocked": "赎回锁定中",
"timeRemaining": "剩余时间"
"timeRemaining": "剩余时间",
"adminConfig": "管理员配置",
"updatePrices": "更新价格",
"setHardCap": "设置硬顶",
"setRedemptionTime": "设置赎回时间",
"setManager": "设置管理员",
"newHardCap": "新硬顶",
"newManager": "新管理员地址",
"newRedemptionTime": "新赎回时间",
"managerPanel": "Manager 面板",
"managedAssets": "托管资产",
"youAreManager": "你是当前金库的 Manager",
"youAreNotManager": "你不是当前金库的 Manager",
"depositManagedAssets": "存入托管资产",
"withdrawForManagement": "提取托管资产",
"deposit": "存入",
"withdraw": "提取",
"toAddress": "接收地址",
"defaultSelf": "默认为自己",
"approvedAmount": "已授权额度",
"needApprove": "需要授权"
},
"factory": {
"title": "工厂管理",
"factoryContract": "工厂合约",
"owner": "所有者",
"vaultImpl": "金库实现",
"defaultHardCap": "默认硬顶",
"totalVaults": "金库总数",
"yourRole": "你的角色",
@@ -91,7 +114,27 @@
"selectVault": "选择金库",
"newWusdPrice": "新 WUSD 价格",
"newYtPrice": "新 YT 价格",
"update": "更新价格"
"update": "更新价格",
"ownerConfig": "Owner 配置",
"setDefaultHardCap": "设置默认硬顶",
"batchOperations": "批量操作",
"selectVaultsForBatch": "选择金库",
"selectAll": "全选",
"deselectAll": "取消全选",
"selectedCount": "已选择",
"batchUpdatePrices": "批量更新价格",
"batchSetHardCap": "批量设置硬顶",
"batchSetRedemptionTime": "批量设置赎回时间",
"advancedFunctions": "高级功能",
"advancedWarning": "警告: 以下是高级管理功能,操作前请确保了解其影响。错误的操作可能导致合约异常。",
"setVaultImplementation": "设置金库实现",
"currentImpl": "当前实现",
"upgradeVault": "升级金库",
"newImplAddress": "新实现地址",
"upgrade": "升级",
"batchUpgradeVault": "批量升级金库",
"batchUpgrade": "批量升级",
"selectInBatchSection": "在批量操作区选择"
},
"language": {
"en": "英文",
@@ -136,7 +179,9 @@
"run": "执行",
"running": "执行中...",
"mint10000": "铸造 10000 WUSD",
"mint10000Desc": "快速铸造10000个测试WUSD",
"maxApprove": "最大授权 WUSD",
"maxApproveDesc": "授权最大uint256额度",
"buyZero": "买入金额为0",
"buyZeroDesc": "测试 depositYT(0)",
"sellZero": "卖出金额为0",
@@ -207,6 +252,18 @@
"testSwapExceedDesc": "互换金额超过代币余额",
"yourTokenBalances": "你的代币余额",
"whitelisted": "已白名单",
"notWhitelisted": "未白名单"
"notWhitelisted": "未白名单",
"emergencyMode": "紧急模式",
"emergencyModeOn": "已开启 (警告)",
"emergencyModeOff": "正常",
"swapEnabled": "交换功能",
"swapEnabledOn": "已开启",
"swapEnabledOff": "已关闭",
"totalTokenWeights": "总权重",
"accountValue": "你的账户价值",
"usdyAmount": "USDY 数量",
"isStableToken": "稳定币",
"setStableToken": "设置稳定币",
"stableTokenHint": "将代币标记为稳定币,影响价格计算方式"
}
}