feat: 添加多链支持和 Lending 借贷系统
- 新增 ARB Sepolia + BNB Testnet 多链支持 - 添加 LendingPanel 借贷系统组件 - 添加 LendingAdminPanel 管理面板 - 添加 USDCPanel USDC 操作组件 - 添加 HoldersPanel 持有人信息组件 - 添加 AutoTestPanel 自动化测试组件 - 重构 LP 组件为模块化结构 (LP/) - 添加多个调试和测试脚本 - 修复 USDC 精度动态配置 - 优化合约配置支持多链切换 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,6 +1,15 @@
|
||||
# Markdown files
|
||||
*.md
|
||||
|
||||
# Documentation directories
|
||||
docs/
|
||||
document/
|
||||
|
||||
# Temp files
|
||||
*.txt
|
||||
*.jpg
|
||||
*.backup*
|
||||
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
|
||||
44
frontend/package-lock.json
generated
44
frontend/package-lock.json
generated
@@ -29,6 +29,7 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4"
|
||||
@@ -5863,6 +5864,19 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -6906,6 +6920,16 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.53.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.4.tgz",
|
||||
@@ -7192,6 +7216,26 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"sim:buy": "tsx scripts/batch-buy-simulation.ts",
|
||||
"sim:sell": "tsx scripts/batch-sell-simulation.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
@@ -31,6 +33,7 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.4"
|
||||
|
||||
377
frontend/scripts/batch-buy-simulation.ts
Normal file
377
frontend/scripts/batch-buy-simulation.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* 批量用户买入模拟脚本
|
||||
*
|
||||
* 功能:
|
||||
* 1. 从 HD 钱包派生多个测试账户
|
||||
* 2. 从主账户给测试账户转 WUSD
|
||||
* 3. 批量执行 depositYT 买入交易
|
||||
*
|
||||
* 使用方法:
|
||||
* npx tsx scripts/batch-buy-simulation.ts
|
||||
*/
|
||||
|
||||
import {
|
||||
createPublicClient,
|
||||
createWalletClient,
|
||||
http,
|
||||
parseUnits,
|
||||
formatUnits,
|
||||
type Address,
|
||||
type Hex
|
||||
} from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
import { privateKeyToAccount, mnemonicToAccount } from 'viem/accounts'
|
||||
|
||||
// ============ 配置 ============
|
||||
|
||||
// 合约地址
|
||||
const CONTRACTS = {
|
||||
WUSD: '0x6d2bf81a631dFE19B2f348aE92cF6Ef41ca2DF98' as Address,
|
||||
FACTORY: '0x982716f32F10BCB5B5944c1473a8992354bF632b' as Address,
|
||||
VAULTS: {
|
||||
YT_A: '0x0cA35994F033685E7a57ef9bc5d00dd3cf927330' as Address,
|
||||
YT_B: '0x333805C9EE75f59Aa2Cc79DfDe2499F920c7b408' as Address,
|
||||
YT_C: '0x6DF0ED6f0345F601A206974973dE9fC970598587' as Address,
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟配置
|
||||
const CONFIG = {
|
||||
// 主账户私钥 (用于分发 WUSD)
|
||||
MAIN_PRIVATE_KEY: '0xa082a7037105ebd606bee80906687e400d89899bbb6ba0273a61528c2f5fab89' as Hex,
|
||||
|
||||
// HD 钱包助记词 (用于生成测试账户)
|
||||
// 测试用,请勿在生产环境使用
|
||||
TEST_MNEMONIC: 'test test test test test test test test test test test junk',
|
||||
|
||||
// 模拟用户数量
|
||||
USER_COUNT: 3,
|
||||
|
||||
// 每个用户的买入金额范围 (WUSD)
|
||||
MIN_BUY_AMOUNT: 10,
|
||||
MAX_BUY_AMOUNT: 100,
|
||||
|
||||
// 目标金库
|
||||
TARGET_VAULT: CONTRACTS.VAULTS.YT_A,
|
||||
|
||||
// 交易间隔 (毫秒)
|
||||
TX_INTERVAL: 2000,
|
||||
|
||||
// RPC URL
|
||||
RPC_URL: 'https://sepolia-rollup.arbitrum.io/rpc',
|
||||
}
|
||||
|
||||
// ============ ABI ============
|
||||
|
||||
const WUSD_ABI = [
|
||||
{
|
||||
inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }],
|
||||
name: 'transfer',
|
||||
outputs: [{ type: 'bool' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }],
|
||||
name: 'approve',
|
||||
outputs: [{ type: 'bool' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'balanceOf',
|
||||
outputs: [{ type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
] as const
|
||||
|
||||
const VAULT_ABI = [
|
||||
{
|
||||
inputs: [{ name: '_wusdAmount', type: 'uint256' }],
|
||||
name: 'depositYT',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ name: '_wusdAmount', type: 'uint256' }],
|
||||
name: 'previewBuy',
|
||||
outputs: [{ type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'balanceOf',
|
||||
outputs: [{ type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'symbol',
|
||||
outputs: [{ type: 'string' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
] as const
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
function randomAmount(min: number, max: number): bigint {
|
||||
const amount = Math.random() * (max - min) + min
|
||||
return parseUnits(amount.toFixed(2), 18)
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// ============ 主逻辑 ============
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 批量用户买入模拟开始\n')
|
||||
|
||||
// 创建客户端
|
||||
const publicClient = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http(CONFIG.RPC_URL),
|
||||
})
|
||||
|
||||
// 主账户
|
||||
const mainAccount = privateKeyToAccount(CONFIG.MAIN_PRIVATE_KEY)
|
||||
const mainWalletClient = createWalletClient({
|
||||
account: mainAccount,
|
||||
chain: arbitrumSepolia,
|
||||
transport: http(CONFIG.RPC_URL),
|
||||
})
|
||||
|
||||
console.log(`📍 主账户: ${mainAccount.address}`)
|
||||
|
||||
// 检查主账户 WUSD 余额
|
||||
const mainBalance = await publicClient.readContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [mainAccount.address],
|
||||
})
|
||||
console.log(`💰 主账户 WUSD 余额: ${formatUnits(mainBalance, 18)}`)
|
||||
|
||||
// 获取金库 symbol
|
||||
const vaultSymbol = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'symbol',
|
||||
})
|
||||
console.log(`🏦 目标金库: ${vaultSymbol} (${CONFIG.TARGET_VAULT})`)
|
||||
|
||||
// 生成测试账户
|
||||
console.log(`\n👥 生成 ${CONFIG.USER_COUNT} 个测试账户...\n`)
|
||||
|
||||
const testAccounts = []
|
||||
for (let i = 0; i < CONFIG.USER_COUNT; i++) {
|
||||
const account = mnemonicToAccount(CONFIG.TEST_MNEMONIC, { addressIndex: i })
|
||||
const buyAmount = randomAmount(CONFIG.MIN_BUY_AMOUNT, CONFIG.MAX_BUY_AMOUNT)
|
||||
testAccounts.push({ account, buyAmount })
|
||||
console.log(` 账户 ${i + 1}: ${account.address} | 计划买入: ${formatUnits(buyAmount, 18)} WUSD`)
|
||||
}
|
||||
|
||||
// 计算总需要的 WUSD
|
||||
const totalNeeded = testAccounts.reduce((sum, a) => sum + a.buyAmount, 0n)
|
||||
console.log(`\n📊 总计需要: ${formatUnits(totalNeeded, 18)} WUSD`)
|
||||
|
||||
if (mainBalance < totalNeeded) {
|
||||
console.error(`❌ 主账户余额不足! 需要 ${formatUnits(totalNeeded, 18)}, 只有 ${formatUnits(mainBalance, 18)}`)
|
||||
return
|
||||
}
|
||||
|
||||
// 第一步: 给测试账户转 ETH (gas 费)
|
||||
console.log('\n⛽ 步骤 1: 分发 ETH (gas 费) 到测试账户...\n')
|
||||
|
||||
const gasAmount = parseUnits('0.05', 18) // 0.05 ETH 足够多次交易 (Arbitrum Sepolia gas 较高)
|
||||
|
||||
for (let i = 0; i < testAccounts.length; i++) {
|
||||
const { account } = testAccounts[i]
|
||||
|
||||
// 检查 ETH 余额
|
||||
const ethBalance = await publicClient.getBalance({ address: account.address })
|
||||
|
||||
if (ethBalance >= gasAmount) {
|
||||
console.log(` ✓ 账户 ${i + 1} 已有足够 ETH: ${formatUnits(ethBalance, 18)} ETH`)
|
||||
continue
|
||||
}
|
||||
|
||||
const transferAmount = gasAmount - ethBalance
|
||||
console.log(` → 转 ETH 给账户 ${i + 1}: ${formatUnits(transferAmount, 18)} ETH...`)
|
||||
|
||||
try {
|
||||
const hash = await mainWalletClient.sendTransaction({
|
||||
to: account.address,
|
||||
value: transferAmount,
|
||||
})
|
||||
await publicClient.waitForTransactionReceipt({ hash })
|
||||
console.log(` ✓ ETH 转账成功`)
|
||||
} catch (error: any) {
|
||||
console.error(` ✗ ETH 转账失败: ${error.message}`)
|
||||
}
|
||||
|
||||
await sleep(500)
|
||||
}
|
||||
|
||||
// 第二步: 给测试账户转 WUSD
|
||||
console.log('\n📤 步骤 2: 分发 WUSD 到测试账户...\n')
|
||||
|
||||
for (let i = 0; i < testAccounts.length; i++) {
|
||||
const { account, buyAmount } = testAccounts[i]
|
||||
|
||||
// 检查是否已有足够余额
|
||||
const currentBalance = await publicClient.readContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [account.address],
|
||||
})
|
||||
|
||||
if (currentBalance >= buyAmount) {
|
||||
console.log(` ✓ 账户 ${i + 1} 已有足够余额: ${formatUnits(currentBalance, 18)} WUSD`)
|
||||
continue
|
||||
}
|
||||
|
||||
const transferAmount = buyAmount - currentBalance
|
||||
console.log(` → 转账给账户 ${i + 1}: ${formatUnits(transferAmount, 18)} WUSD...`)
|
||||
|
||||
try {
|
||||
const hash = await mainWalletClient.writeContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'transfer',
|
||||
args: [account.address, transferAmount],
|
||||
})
|
||||
|
||||
await publicClient.waitForTransactionReceipt({ hash })
|
||||
console.log(` ✓ 转账成功: ${hash}`)
|
||||
} catch (error: any) {
|
||||
console.error(` ✗ 转账失败: ${error.message}`)
|
||||
}
|
||||
|
||||
await sleep(1000)
|
||||
}
|
||||
|
||||
// 第三步: 批量买入
|
||||
console.log('\n🛒 步骤 3: 执行批量买入...\n')
|
||||
|
||||
const results: { address: string; amount: string; ytReceived: string; status: string; hash?: string }[] = []
|
||||
|
||||
for (let i = 0; i < testAccounts.length; i++) {
|
||||
const { account, buyAmount } = testAccounts[i]
|
||||
|
||||
console.log(`\n[${i + 1}/${testAccounts.length}] 账户: ${account.address}`)
|
||||
console.log(` 买入金额: ${formatUnits(buyAmount, 18)} WUSD`)
|
||||
|
||||
const walletClient = createWalletClient({
|
||||
account,
|
||||
chain: arbitrumSepolia,
|
||||
transport: http(CONFIG.RPC_URL),
|
||||
})
|
||||
|
||||
try {
|
||||
// 预览买入
|
||||
const previewYT = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'previewBuy',
|
||||
args: [buyAmount],
|
||||
})
|
||||
console.log(` 预计获得: ${formatUnits(previewYT, 18)} ${vaultSymbol}`)
|
||||
|
||||
// 授权
|
||||
console.log(` → 授权 WUSD...`)
|
||||
const approveHash = await walletClient.writeContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'approve',
|
||||
args: [CONFIG.TARGET_VAULT, buyAmount],
|
||||
})
|
||||
await publicClient.waitForTransactionReceipt({ hash: approveHash })
|
||||
console.log(` ✓ 授权成功`)
|
||||
|
||||
// 买入
|
||||
console.log(` → 执行买入...`)
|
||||
const buyHash = await walletClient.writeContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'depositYT',
|
||||
args: [buyAmount],
|
||||
})
|
||||
await publicClient.waitForTransactionReceipt({ hash: buyHash })
|
||||
|
||||
// 检查 YT 余额
|
||||
const ytBalance = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [account.address],
|
||||
})
|
||||
|
||||
console.log(` ✓ 买入成功! YT 余额: ${formatUnits(ytBalance, 18)} ${vaultSymbol}`)
|
||||
console.log(` 交易哈希: ${buyHash}`)
|
||||
|
||||
results.push({
|
||||
address: account.address,
|
||||
amount: formatUnits(buyAmount, 18),
|
||||
ytReceived: formatUnits(ytBalance, 18),
|
||||
status: 'success',
|
||||
hash: buyHash,
|
||||
})
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(` ✗ 买入失败: ${error.message}`)
|
||||
results.push({
|
||||
address: account.address,
|
||||
amount: formatUnits(buyAmount, 18),
|
||||
ytReceived: '0',
|
||||
status: 'failed',
|
||||
})
|
||||
}
|
||||
|
||||
// 交易间隔
|
||||
if (i < testAccounts.length - 1) {
|
||||
console.log(` ⏳ 等待 ${CONFIG.TX_INTERVAL / 1000} 秒...`)
|
||||
await sleep(CONFIG.TX_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
// 输出汇总
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('📊 模拟结果汇总')
|
||||
console.log('='.repeat(60))
|
||||
|
||||
const successCount = results.filter(r => r.status === 'success').length
|
||||
const failCount = results.filter(r => r.status === 'failed').length
|
||||
const totalBought = results
|
||||
.filter(r => r.status === 'success')
|
||||
.reduce((sum, r) => sum + parseFloat(r.amount), 0)
|
||||
const totalYT = results
|
||||
.filter(r => r.status === 'success')
|
||||
.reduce((sum, r) => sum + parseFloat(r.ytReceived), 0)
|
||||
|
||||
console.log(`\n成功: ${successCount} 笔`)
|
||||
console.log(`失败: ${failCount} 笔`)
|
||||
console.log(`总买入 WUSD: ${totalBought.toFixed(2)}`)
|
||||
console.log(`总获得 ${vaultSymbol}: ${totalYT.toFixed(2)}`)
|
||||
|
||||
console.log('\n详细记录:')
|
||||
console.table(results.map(r => ({
|
||||
地址: r.address.slice(0, 10) + '...',
|
||||
买入WUSD: r.amount,
|
||||
获得YT: r.ytReceived,
|
||||
状态: r.status,
|
||||
})))
|
||||
|
||||
console.log('\n✅ 模拟完成!')
|
||||
}
|
||||
|
||||
// 运行
|
||||
main().catch(console.error)
|
||||
343
frontend/scripts/batch-sell-simulation.ts
Normal file
343
frontend/scripts/batch-sell-simulation.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* 批量用户卖出模拟脚本
|
||||
*
|
||||
* 功能:
|
||||
* 1. 读取测试账户的 YT 余额
|
||||
* 2. 批量执行 withdrawYT 卖出交易(进入排队)
|
||||
* 3. 可选:管理员批量处理队列
|
||||
*
|
||||
* 使用方法:
|
||||
* npx tsx scripts/batch-sell-simulation.ts
|
||||
*/
|
||||
|
||||
import {
|
||||
createPublicClient,
|
||||
createWalletClient,
|
||||
http,
|
||||
parseUnits,
|
||||
formatUnits,
|
||||
type Address,
|
||||
type Hex
|
||||
} from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
import { privateKeyToAccount, mnemonicToAccount } from 'viem/accounts'
|
||||
|
||||
// ============ 配置 ============
|
||||
|
||||
const CONTRACTS = {
|
||||
WUSD: '0x6d2bf81a631dFE19B2f348aE92cF6Ef41ca2DF98' as Address,
|
||||
FACTORY: '0x982716f32F10BCB5B5944c1473a8992354bF632b' as Address,
|
||||
VAULTS: {
|
||||
YT_A: '0x0cA35994F033685E7a57ef9bc5d00dd3cf927330' as Address,
|
||||
YT_B: '0x333805C9EE75f59Aa2Cc79DfDe2499F920c7b408' as Address,
|
||||
YT_C: '0x6DF0ED6f0345F601A206974973dE9fC970598587' as Address,
|
||||
}
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
// 主账户私钥 (Owner,用于处理队列)
|
||||
MAIN_PRIVATE_KEY: '0xa082a7037105ebd606bee80906687e400d89899bbb6ba0273a61528c2f5fab89' as Hex,
|
||||
|
||||
// HD 钱包助记词 (与买入脚本相同)
|
||||
TEST_MNEMONIC: 'test test test test test test test test test test test junk',
|
||||
|
||||
// 模拟用户数量
|
||||
USER_COUNT: 10,
|
||||
|
||||
// 卖出比例 (0.5 = 卖出 50% 的 YT)
|
||||
SELL_RATIO: 0.5,
|
||||
|
||||
// 目标金库
|
||||
TARGET_VAULT: CONTRACTS.VAULTS.YT_A,
|
||||
|
||||
// 交易间隔 (毫秒)
|
||||
TX_INTERVAL: 2000,
|
||||
|
||||
// 是否自动处理队列
|
||||
AUTO_PROCESS_QUEUE: true,
|
||||
|
||||
// 批量处理大小
|
||||
BATCH_PROCESS_SIZE: 10,
|
||||
|
||||
// RPC URL
|
||||
RPC_URL: 'https://sepolia-rollup.arbitrum.io/rpc',
|
||||
}
|
||||
|
||||
// ============ ABI ============
|
||||
|
||||
const VAULT_ABI = [
|
||||
{
|
||||
inputs: [{ name: '_ytAmount', type: 'uint256' }],
|
||||
name: 'withdrawYT',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ name: '_ytAmount', type: 'uint256' }],
|
||||
name: 'previewSell',
|
||||
outputs: [{ type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'balanceOf',
|
||||
outputs: [{ type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'symbol',
|
||||
outputs: [{ type: 'string' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'canRedeemNow',
|
||||
outputs: [{ type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'getQueueProgress',
|
||||
outputs: [
|
||||
{ name: 'currentIndex', type: 'uint256' },
|
||||
{ name: 'totalRequests', type: 'uint256' },
|
||||
{ name: 'pendingCount', type: 'uint256' },
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ name: '_batchSize', type: 'uint256' }],
|
||||
name: 'processBatchWithdrawals',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
] as const
|
||||
|
||||
// ============ 工具函数 ============
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// ============ 主逻辑 ============
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 批量用户卖出模拟开始\n')
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http(CONFIG.RPC_URL),
|
||||
})
|
||||
|
||||
// 主账户 (Owner)
|
||||
const mainAccount = privateKeyToAccount(CONFIG.MAIN_PRIVATE_KEY)
|
||||
const mainWalletClient = createWalletClient({
|
||||
account: mainAccount,
|
||||
chain: arbitrumSepolia,
|
||||
transport: http(CONFIG.RPC_URL),
|
||||
})
|
||||
|
||||
console.log(`📍 Owner 账户: ${mainAccount.address}`)
|
||||
|
||||
// 获取金库信息
|
||||
const vaultSymbol = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'symbol',
|
||||
})
|
||||
console.log(`🏦 目标金库: ${vaultSymbol} (${CONFIG.TARGET_VAULT})`)
|
||||
|
||||
// 检查赎回状态
|
||||
const canRedeem = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'canRedeemNow',
|
||||
})
|
||||
console.log(`📅 赎回状态: ${canRedeem ? '✅ 可赎回' : '❌ 锁定中'}`)
|
||||
|
||||
if (!canRedeem) {
|
||||
console.error('\n⚠️ 当前不在赎回期,无法执行卖出操作')
|
||||
console.log('请先通过管理员设置赎回时间: setVaultNextRedemptionTime')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取队列状态
|
||||
const queueProgress = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'getQueueProgress',
|
||||
})
|
||||
console.log(`📋 队列状态: 已处理 ${queueProgress[0]}, 总请求 ${queueProgress[1]}, 待处理 ${queueProgress[2]}`)
|
||||
|
||||
// 获取测试账户
|
||||
console.log(`\n👥 检查 ${CONFIG.USER_COUNT} 个测试账户的 YT 余额...\n`)
|
||||
|
||||
const testAccounts = []
|
||||
for (let i = 0; i < CONFIG.USER_COUNT; i++) {
|
||||
const account = mnemonicToAccount(CONFIG.TEST_MNEMONIC, { addressIndex: i })
|
||||
|
||||
const ytBalance = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [account.address],
|
||||
})
|
||||
|
||||
if (ytBalance > 0n) {
|
||||
const sellAmount = (ytBalance * BigInt(Math.floor(CONFIG.SELL_RATIO * 100))) / 100n
|
||||
testAccounts.push({ account, ytBalance, sellAmount })
|
||||
console.log(` 账户 ${i + 1}: ${account.address} | YT余额: ${formatUnits(ytBalance, 18)} | 计划卖出: ${formatUnits(sellAmount, 18)}`)
|
||||
} else {
|
||||
console.log(` 账户 ${i + 1}: ${account.address} | YT余额: 0 (跳过)`)
|
||||
}
|
||||
}
|
||||
|
||||
if (testAccounts.length === 0) {
|
||||
console.log('\n⚠️ 没有账户有 YT 余额,请先运行 npm run sim:buy')
|
||||
return
|
||||
}
|
||||
|
||||
// 执行卖出
|
||||
console.log('\n💸 步骤 1: 执行批量卖出 (进入排队)...\n')
|
||||
|
||||
const results: { address: string; sellAmount: string; expectedWusd: string; status: string; hash?: string }[] = []
|
||||
|
||||
for (let i = 0; i < testAccounts.length; i++) {
|
||||
const { account, sellAmount } = testAccounts[i]
|
||||
|
||||
console.log(`\n[${i + 1}/${testAccounts.length}] 账户: ${account.address}`)
|
||||
console.log(` 卖出金额: ${formatUnits(sellAmount, 18)} ${vaultSymbol}`)
|
||||
|
||||
const walletClient = createWalletClient({
|
||||
account,
|
||||
chain: arbitrumSepolia,
|
||||
transport: http(CONFIG.RPC_URL),
|
||||
})
|
||||
|
||||
try {
|
||||
// 预览卖出
|
||||
const previewWusd = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'previewSell',
|
||||
args: [sellAmount],
|
||||
})
|
||||
console.log(` 预计获得: ${formatUnits(previewWusd, 18)} WUSD`)
|
||||
|
||||
// 执行卖出 (进入队列)
|
||||
console.log(` → 提交卖出请求...`)
|
||||
const sellHash = await walletClient.writeContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'withdrawYT',
|
||||
args: [sellAmount],
|
||||
})
|
||||
await publicClient.waitForTransactionReceipt({ hash: sellHash })
|
||||
|
||||
console.log(` ✓ 请求已进入队列`)
|
||||
console.log(` 交易哈希: ${sellHash}`)
|
||||
|
||||
results.push({
|
||||
address: account.address,
|
||||
sellAmount: formatUnits(sellAmount, 18),
|
||||
expectedWusd: formatUnits(previewWusd, 18),
|
||||
status: 'queued',
|
||||
hash: sellHash,
|
||||
})
|
||||
|
||||
} catch (error: any) {
|
||||
console.error(` ✗ 卖出失败: ${error.message}`)
|
||||
results.push({
|
||||
address: account.address,
|
||||
sellAmount: formatUnits(sellAmount, 18),
|
||||
expectedWusd: '0',
|
||||
status: 'failed',
|
||||
})
|
||||
}
|
||||
|
||||
if (i < testAccounts.length - 1) {
|
||||
console.log(` ⏳ 等待 ${CONFIG.TX_INTERVAL / 1000} 秒...`)
|
||||
await sleep(CONFIG.TX_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取更新后的队列状态
|
||||
const newQueueProgress = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'getQueueProgress',
|
||||
})
|
||||
console.log(`\n📋 更新后队列状态: 已处理 ${newQueueProgress[0]}, 总请求 ${newQueueProgress[1]}, 待处理 ${newQueueProgress[2]}`)
|
||||
|
||||
// 处理队列 (如果启用)
|
||||
if (CONFIG.AUTO_PROCESS_QUEUE && newQueueProgress[2] > 0n) {
|
||||
console.log('\n⚙️ 步骤 2: 管理员批量处理队列...\n')
|
||||
|
||||
const pendingCount = Number(newQueueProgress[2])
|
||||
const batches = Math.ceil(pendingCount / CONFIG.BATCH_PROCESS_SIZE)
|
||||
|
||||
for (let batch = 0; batch < batches; batch++) {
|
||||
const batchSize = Math.min(CONFIG.BATCH_PROCESS_SIZE, pendingCount - batch * CONFIG.BATCH_PROCESS_SIZE)
|
||||
console.log(` 处理批次 ${batch + 1}/${batches} (${batchSize} 笔)...`)
|
||||
|
||||
try {
|
||||
const processHash = await mainWalletClient.writeContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'processBatchWithdrawals',
|
||||
args: [BigInt(batchSize)],
|
||||
})
|
||||
await publicClient.waitForTransactionReceipt({ hash: processHash })
|
||||
console.log(` ✓ 批次处理完成: ${processHash}`)
|
||||
} catch (error: any) {
|
||||
console.error(` ✗ 处理失败: ${error.message}`)
|
||||
break
|
||||
}
|
||||
|
||||
await sleep(2000)
|
||||
}
|
||||
|
||||
// 最终队列状态
|
||||
const finalQueue = await publicClient.readContract({
|
||||
address: CONFIG.TARGET_VAULT,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'getQueueProgress',
|
||||
})
|
||||
console.log(`\n📋 最终队列状态: 已处理 ${finalQueue[0]}, 总请求 ${finalQueue[1]}, 待处理 ${finalQueue[2]}`)
|
||||
}
|
||||
|
||||
// 汇总
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('📊 模拟结果汇总')
|
||||
console.log('='.repeat(60))
|
||||
|
||||
const queuedCount = results.filter(r => r.status === 'queued').length
|
||||
const failCount = results.filter(r => r.status === 'failed').length
|
||||
const totalSold = results
|
||||
.filter(r => r.status === 'queued')
|
||||
.reduce((sum, r) => sum + parseFloat(r.sellAmount), 0)
|
||||
|
||||
console.log(`\n成功排队: ${queuedCount} 笔`)
|
||||
console.log(`失败: ${failCount} 笔`)
|
||||
console.log(`总卖出 ${vaultSymbol}: ${totalSold.toFixed(2)}`)
|
||||
|
||||
console.log('\n详细记录:')
|
||||
console.table(results.map(r => ({
|
||||
地址: r.address.slice(0, 10) + '...',
|
||||
卖出YT: r.sellAmount,
|
||||
预期WUSD: r.expectedWusd,
|
||||
状态: r.status,
|
||||
})))
|
||||
|
||||
console.log('\n✅ 模拟完成!')
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
169
frontend/scripts/check-account-status.js
Normal file
169
frontend/scripts/check-account-status.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { createPublicClient, http, parseUnits, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
const USDC = getAddress('0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d')
|
||||
const USER = getAddress('0xa013422A5918CD099c63c8CC35283EACa99a705d')
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'account', type: 'address' }
|
||||
],
|
||||
name: 'borrowBalanceOf',
|
||||
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'
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'account', type: 'address' },
|
||||
{ internalType: 'address', name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'collateralBalanceOf',
|
||||
outputs: [{ internalType: 'uint128', name: '', type: 'uint128' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'baseToken',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'baseMinForRewards',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'baseBorrowMin',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
const ERC20_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'account', type: 'address' }
|
||||
],
|
||||
name: 'balanceOf',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'owner', type: 'address' },
|
||||
{ internalType: 'address', name: 'spender', type: 'address' }
|
||||
],
|
||||
name: 'allowance',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查用户账户状态...\n')
|
||||
console.log('用户地址:', USER)
|
||||
|
||||
try {
|
||||
// 检查 YT-A 余额和授权
|
||||
const ytBalance = await client.readContract({
|
||||
address: YT_A,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
const ytAllowance = await client.readContract({
|
||||
address: YT_A,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'allowance',
|
||||
args: [USER, LENDING_PROXY]
|
||||
})
|
||||
|
||||
console.log('\n=== YT-A 代币状态 ===')
|
||||
console.log('钱包余额:', Number(ytBalance) / 1e18, 'YT-A')
|
||||
console.log('授权额度:', Number(ytAllowance) / 1e18, 'YT-A')
|
||||
console.log('想要存入:', 10, 'YT-A')
|
||||
console.log('余额足够:', Number(ytBalance) >= 10e18 ? '✓' : '✗')
|
||||
console.log('授权足够:', Number(ytAllowance) >= 10e18 ? '✓' : '✗')
|
||||
|
||||
// 检查 USDC 余额
|
||||
const usdcBalance = await client.readContract({
|
||||
address: USDC,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
console.log('\n=== USDC 代币状态 ===')
|
||||
console.log('USDC 余额:', Number(usdcBalance) / 1e6, 'USDC')
|
||||
|
||||
// 检查 Lending 账户状态
|
||||
const baseBalance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
const borrowBalance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'borrowBalanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
const collateralBalance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'collateralBalanceOf',
|
||||
args: [USER, YT_A]
|
||||
})
|
||||
|
||||
console.log('\n=== Lending 账户状态 ===')
|
||||
console.log('供应余额 (USDC):', Number(baseBalance) / 1e6, 'USDC')
|
||||
console.log('借款余额 (USDC):', Number(borrowBalance) / 1e6, 'USDC')
|
||||
console.log('抵押品余额 (YT-A):', Number(collateralBalance) / 1e18, 'YT-A')
|
||||
|
||||
// 检查合约参数
|
||||
const baseBorrowMin = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'baseBorrowMin'
|
||||
})
|
||||
|
||||
console.log('\n=== 合约参数 ===')
|
||||
console.log('最小借款额:', Number(baseBorrowMin) / 1e6, 'USDC')
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ 检查失败:', error.message)
|
||||
console.error('详细错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
30
frontend/scripts/check-all-balances.ts
Normal file
30
frontend/scripts/check-all-balances.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createPublicClient, http, formatEther } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
import { mnemonicToAccount } from 'viem/accounts'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc'),
|
||||
})
|
||||
|
||||
const MAIN_ADDRESS = '0xa013422A5918CD099C63c8CC35283EACa99a705d'
|
||||
const TEST_MNEMONIC = 'test test test test test test test test test test test junk'
|
||||
|
||||
async function main() {
|
||||
console.log('📊 所有账户 ETH 余额:\n')
|
||||
|
||||
// 主账户
|
||||
const mainBalance = await client.getBalance({ address: MAIN_ADDRESS })
|
||||
console.log(`主账户: ${MAIN_ADDRESS}`)
|
||||
console.log(` ETH: ${formatEther(mainBalance)}\n`)
|
||||
|
||||
// 测试账户
|
||||
console.log('测试账户:')
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const account = mnemonicToAccount(TEST_MNEMONIC, { addressIndex: i })
|
||||
const balance = await client.getBalance({ address: account.address })
|
||||
console.log(` ${i + 1}. ${account.address}: ${formatEther(balance)} ETH`)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
23
frontend/scripts/check-balance.ts
Normal file
23
frontend/scripts/check-balance.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createPublicClient, http, formatEther } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc'),
|
||||
})
|
||||
|
||||
async function main() {
|
||||
const balance = await client.getBalance({ address: '0xa013422A5918CD099C63c8CC35283EACa99a705d' })
|
||||
console.log('主账户 ETH 余额:', formatEther(balance), 'ETH')
|
||||
|
||||
// 检查是否足够 (10 个账户 × 0.01 ETH = 0.1 ETH)
|
||||
const needed = 0.1
|
||||
if (parseFloat(formatEther(balance)) < needed) {
|
||||
console.log(`\n⚠️ ETH 不足! 需要至少 ${needed} ETH 来分发 gas 费`)
|
||||
console.log('请先获取测试网 ETH: https://www.alchemy.com/faucets/arbitrum-sepolia')
|
||||
} else {
|
||||
console.log('\n✅ ETH 足够运行模拟脚本')
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
171
frontend/scripts/check-borrow-history.js
Normal file
171
frontend/scripts/check-borrow-history.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USDC = getAddress('0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
|
||||
const TRANSFER_EVENT = {
|
||||
type: 'event',
|
||||
name: 'Transfer',
|
||||
inputs: [
|
||||
{ indexed: true, name: 'from', type: 'address' },
|
||||
{ indexed: true, name: 'to', type: 'address' },
|
||||
{ indexed: false, name: 'value', type: 'uint256' }
|
||||
]
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n=== 检查借款历史(USDC 从 Lending 转出给用户)===\n')
|
||||
console.log('用户:', USER)
|
||||
console.log('Lending 合约:', LENDING_PROXY)
|
||||
console.log('USDC:', USDC)
|
||||
console.log()
|
||||
|
||||
const latestBlock = await client.getBlockNumber()
|
||||
console.log('最新区块:', latestBlock)
|
||||
console.log('查询范围: 最近 10000 个区块\n')
|
||||
|
||||
try {
|
||||
// 查找 USDC 从 Lending 转给用户的记录(这才是真正的借款/提取)
|
||||
const logs = await client.getLogs({
|
||||
address: USDC,
|
||||
event: TRANSFER_EVENT,
|
||||
args: {
|
||||
from: LENDING_PROXY,
|
||||
to: USER
|
||||
},
|
||||
fromBlock: latestBlock - 10000n,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
|
||||
if (logs.length > 0) {
|
||||
console.log(`✓ 找到 ${logs.length} 笔 USDC 转出记录:\n`)
|
||||
let totalWithdrawn = 0n
|
||||
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
const log = logs[i]
|
||||
const { value } = log.args
|
||||
totalWithdrawn += value
|
||||
|
||||
console.log(`${i + 1}. 区块 ${log.blockNumber}`)
|
||||
console.log(` 交易: ${log.transactionHash}`)
|
||||
console.log(` 数量: ${Number(value) / 1e6} USDC`)
|
||||
|
||||
// 获取交易详情以确认是 withdraw 还是其他操作
|
||||
try {
|
||||
const tx = await client.getTransaction({ hash: log.transactionHash })
|
||||
const selector = tx.input.slice(0, 10)
|
||||
|
||||
// withdraw(uint256) = 0x2e1a7d4d
|
||||
// withdrawFrom(...) = ...
|
||||
if (selector === '0x2e1a7d4d') {
|
||||
console.log(` 函数: withdraw (借款/提取)`)
|
||||
} else {
|
||||
console.log(` 函数选择器: ${selector}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` (无法获取交易详情)`)
|
||||
}
|
||||
|
||||
console.log()
|
||||
}
|
||||
|
||||
console.log(`总计: ${Number(totalWithdrawn) / 1e6} USDC`)
|
||||
} else {
|
||||
console.log('✗ 未找到任何 USDC 转出记录')
|
||||
console.log()
|
||||
console.log('这意味着:')
|
||||
console.log(' - 用户从未真正从 Lending 提取或借款 USDC')
|
||||
console.log(' - 如果用户看到"借款成功"的消息,那些都是前端误报')
|
||||
}
|
||||
|
||||
console.log('\n=== 对比:USDC 转入 Lending(存款)===\n')
|
||||
|
||||
const supplyLogs = await client.getLogs({
|
||||
address: USDC,
|
||||
event: TRANSFER_EVENT,
|
||||
args: {
|
||||
from: USER,
|
||||
to: LENDING_PROXY
|
||||
},
|
||||
fromBlock: latestBlock - 10000n,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
|
||||
if (supplyLogs.length > 0) {
|
||||
console.log(`找到 ${supplyLogs.length} 笔 USDC 存入记录:\n`)
|
||||
let totalSupplied = 0n
|
||||
|
||||
supplyLogs.forEach((log, i) => {
|
||||
const { value } = log.args
|
||||
totalSupplied += value
|
||||
console.log(`${i + 1}. 区块 ${log.blockNumber}`)
|
||||
console.log(` 交易: ${log.transactionHash}`)
|
||||
console.log(` 数量: ${Number(value) / 1e6} USDC`)
|
||||
console.log()
|
||||
})
|
||||
|
||||
console.log(`总计存入: ${Number(totalSupplied) / 1e6} USDC`)
|
||||
} else {
|
||||
console.log('未找到 USDC 存入记录')
|
||||
}
|
||||
|
||||
// 检查当前余额
|
||||
console.log('\n=== 当前账户状态 ===\n')
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'getBalance',
|
||||
outputs: [{ name: '', type: 'int256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'borrowBalanceOf',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
const balance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getBalance',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
const borrowBalance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'borrowBalanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
console.log('USDC 余额(存款):', balance > 0 ? `${Number(balance) / 1e6} USDC` : '0 USDC')
|
||||
console.log('借款余额:', Number(borrowBalance) / 1e6, 'USDC')
|
||||
console.log()
|
||||
|
||||
if (balance > 0) {
|
||||
console.log('⚠️ 用户当前有 USDC 存款在 Lending 中')
|
||||
console.log(` 存款金额: ${Number(balance) / 1e6} USDC`)
|
||||
console.log()
|
||||
console.log('根据 Compound V3 设计:')
|
||||
console.log(' - withdraw() 会先从存款中扣除')
|
||||
console.log(` - 只有 withdraw 金额 > ${Number(balance) / 1e6} USDC 时,才会产生真正的借款`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
55
frontend/scripts/check-collateral-balance.js
Normal file
55
frontend/scripts/check-collateral-balance.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: '_user', type: 'address' },
|
||||
{ internalType: 'address', name: '_collateralAsset', type: 'address' }
|
||||
],
|
||||
name: 'getUserCollateral',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查抵押品余额...\n')
|
||||
console.log('用户:', USER)
|
||||
console.log('抵押品:', YT_A)
|
||||
|
||||
try {
|
||||
const collateralBalance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getUserCollateral',
|
||||
args: [USER, YT_A]
|
||||
})
|
||||
|
||||
console.log('\n✓ 抵押品余额:', Number(collateralBalance) / 1e18, 'YT-A')
|
||||
|
||||
if (collateralBalance > 0n) {
|
||||
console.log('\n成功!代币已存入合约作为抵押品。')
|
||||
} else {
|
||||
console.log('\n警告:抵押品余额为 0,但钱包代币已被扣除。')
|
||||
console.log('可能原因:')
|
||||
console.log('1. 交易失败但代币被锁在某处')
|
||||
console.log('2. 函数调用错误,需要检查交易哈希')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ 读取失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
119
frontend/scripts/check-compound-v3-storage.js
Normal file
119
frontend/scripts/check-compound-v3-storage.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { createPublicClient, http, getAddress, keccak256, toHex, pad, concat } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
|
||||
// Compound V3 使用的公开状态变量
|
||||
const COMET_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ name: 'account', type: 'address' },
|
||||
{ name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'userCollateral',
|
||||
outputs: [
|
||||
{
|
||||
components: [
|
||||
{ name: 'balance', type: 'uint128' },
|
||||
{ name: '_reserved', type: 'uint128' }
|
||||
],
|
||||
name: '',
|
||||
type: 'tuple'
|
||||
}
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'balanceOf',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'borrowBalanceOf',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ name: 'account', type: 'address' },
|
||||
{ name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'collateralBalanceOf',
|
||||
outputs: [{ name: '', type: 'uint128' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查 Compound V3 存储结构\n')
|
||||
console.log('合约:', LENDING_PROXY)
|
||||
console.log('用户:', USER)
|
||||
console.log('抵押品:', YT_A)
|
||||
console.log()
|
||||
|
||||
// 测试 Compound V3 原生函数
|
||||
for (const func of COMET_ABI) {
|
||||
console.log(`--- ${func.name} ---`)
|
||||
try {
|
||||
let result
|
||||
if (func.name === 'userCollateral') {
|
||||
result = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [func],
|
||||
functionName: func.name,
|
||||
args: [USER, YT_A]
|
||||
})
|
||||
console.log('✓ 成功!')
|
||||
console.log(' balance:', result.balance.toString(), '(', Number(result.balance) / 1e18, 'YT-A )')
|
||||
console.log(' _reserved:', result._reserved.toString())
|
||||
} else if (func.name === 'collateralBalanceOf') {
|
||||
result = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [func],
|
||||
functionName: func.name,
|
||||
args: [USER, YT_A]
|
||||
})
|
||||
console.log('✓ 成功:', result.toString(), '(', Number(result) / 1e18, 'YT-A )')
|
||||
} else if (func.name === 'balanceOf') {
|
||||
result = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [func],
|
||||
functionName: func.name,
|
||||
args: [USER]
|
||||
})
|
||||
console.log('✓ 成功:', result.toString(), '(', Number(result) / 1e6, 'USDC )')
|
||||
} else {
|
||||
result = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [func],
|
||||
functionName: func.name,
|
||||
args: [USER]
|
||||
})
|
||||
console.log('✓ 成功:', result.toString())
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('✗ 失败:', error.message.split('\n')[0])
|
||||
}
|
||||
console.log()
|
||||
}
|
||||
|
||||
console.log('=== 结论 ===')
|
||||
console.log('如果上面的 Compound V3 原生函数能工作,')
|
||||
console.log('说明合约使用了 Compound V3 的数据结构,')
|
||||
console.log('前端应该调用这些函数而不是自定义的 getUserCollateral 等。\n')
|
||||
}
|
||||
|
||||
main()
|
||||
78
frontend/scripts/check-configurator-setup.js
Normal file
78
frontend/scripts/check-configurator-setup.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { createPublicClient, http, parseAbi } from 'viem';
|
||||
import { arbitrumSepolia } from 'viem/chains';
|
||||
|
||||
const CONFIGURATOR_ADDRESS = '0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc';
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D';
|
||||
|
||||
// 扩展的 ABI,包含可能的其他函数
|
||||
const CONFIGURATOR_ABI = parseAbi([
|
||||
'function owner() view returns (address)',
|
||||
'function lending() view returns (address)',
|
||||
'function lendingContract() view returns (address)',
|
||||
'function getLending() view returns (address)',
|
||||
'function setLending(address) external',
|
||||
'function initialize(address) external',
|
||||
'function collateralConfigs(address) view returns (bool, uint256, uint256, uint256)'
|
||||
]);
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc')
|
||||
});
|
||||
|
||||
async function checkSetup() {
|
||||
console.log('🔍 检查 Configurator 设置\n');
|
||||
|
||||
// 尝试读取lending地址(可能有不同的函数名)
|
||||
const possibleGetters = ['lending', 'lendingContract', 'getLending'];
|
||||
|
||||
for (const getter of possibleGetters) {
|
||||
try {
|
||||
console.log(`尝试调用 ${getter}()...`);
|
||||
const lendingAddr = await publicClient.readContract({
|
||||
address: CONFIGURATOR_ADDRESS,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: getter
|
||||
});
|
||||
console.log(`✅ 找到! ${getter}() = ${lendingAddr}`);
|
||||
console.log(` 期望地址: ${LENDING_PROXY}`);
|
||||
console.log(` 匹配: ${lendingAddr.toLowerCase() === LENDING_PROXY.toLowerCase() ? '✅' : '❌'}\n`);
|
||||
|
||||
if (lendingAddr.toLowerCase() !== LENDING_PROXY.toLowerCase()) {
|
||||
console.log('⚠️ 警告: Lending 地址不匹配!');
|
||||
console.log('💡 可能需要调用 setLending() 来设置正确的地址\n');
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.log(` ❌ ${getter}() 不存在或调用失败\n`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('💡 建议:');
|
||||
console.log(' 1. Configurator 可能需要先通过 initialize() 或 setLending() 设置 Lending 合约地址');
|
||||
console.log(' 2. 检查合约源码或部署脚本中的初始化步骤');
|
||||
console.log(' 3. 可能需要 Lending owner 先在 Configurator 中注册\n');
|
||||
|
||||
// 检查是否有其他状态变量
|
||||
console.log('🔍 尝试读取其他可能的状态...\n');
|
||||
|
||||
// 尝试直接读取存储槽
|
||||
try {
|
||||
// Slot 0 通常是第一个状态变量
|
||||
const slot0 = await publicClient.getStorageAt({
|
||||
address: CONFIGURATOR_ADDRESS,
|
||||
slot: '0x0'
|
||||
});
|
||||
console.log('Storage Slot 0:', slot0);
|
||||
|
||||
if (slot0 && slot0 !== '0x' + '0'.repeat(64)) {
|
||||
const addr = '0x' + slot0.slice(-40);
|
||||
console.log('可能的地址值:', addr);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('无法读取存储槽');
|
||||
}
|
||||
}
|
||||
|
||||
checkSetup();
|
||||
28
frontend/scripts/check-function-selector.js
Normal file
28
frontend/scripts/check-function-selector.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { keccak256, toHex } from 'viem'
|
||||
|
||||
// 计算函数选择器
|
||||
function getFunctionSelector(signature) {
|
||||
const hash = keccak256(toHex(signature))
|
||||
return hash.slice(0, 10) // 前4字节
|
||||
}
|
||||
|
||||
console.log('\n计算函数选择器...\n')
|
||||
|
||||
const signatures = [
|
||||
'deposit(address,uint256)',
|
||||
'supplyCollateral(address,uint256)',
|
||||
'supply(address,uint256)',
|
||||
'supplyTo(address,address,uint256)'
|
||||
]
|
||||
|
||||
signatures.forEach(sig => {
|
||||
const selector = getFunctionSelector(sig)
|
||||
console.log(`${sig}`)
|
||||
console.log(` 选择器: ${selector}`)
|
||||
if (selector === '0x47e7ef24') {
|
||||
console.log(' ✓ 匹配!')
|
||||
}
|
||||
console.log()
|
||||
})
|
||||
|
||||
console.log('错误消息中的选择器: 0x47e7ef24')
|
||||
122
frontend/scripts/check-interest-rates.js
Normal file
122
frontend/scripts/check-interest-rates.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [],
|
||||
name: 'getBorrowRate',
|
||||
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'getSupplyRate',
|
||||
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'getUtilization',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查利率和使用率...\n')
|
||||
|
||||
try {
|
||||
// 1. 获取借款利率
|
||||
const borrowRate = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getBorrowRate'
|
||||
})
|
||||
console.log('=== getBorrowRate ===')
|
||||
console.log('原始值:', borrowRate.toString())
|
||||
console.log('类型: uint64')
|
||||
console.log()
|
||||
|
||||
// Compound V3 利率通常是每秒利率,精度 1e18
|
||||
// APY = (1 + ratePerSecond)^(365*24*60*60) - 1
|
||||
// 简化近似:APY ≈ ratePerSecond * secondsPerYear * 100%
|
||||
const secondsPerYear = 365 * 24 * 60 * 60
|
||||
|
||||
// 如果是 1e18 精度的每秒利率
|
||||
if (borrowRate > 0n) {
|
||||
const apyE18 = Number(borrowRate) * secondsPerYear / 1e18
|
||||
console.log('假设精度 1e18 (每秒利率):')
|
||||
console.log(' 每秒利率:', Number(borrowRate) / 1e18)
|
||||
console.log(' 年化利率 (APY):', apyE18.toFixed(2), '%')
|
||||
console.log()
|
||||
|
||||
// 如果是 1e16 精度(百分比形式)
|
||||
const apyE16 = Number(borrowRate) * secondsPerYear / 1e16
|
||||
console.log('假设精度 1e16:')
|
||||
console.log(' 年化利率 (APY):', apyE16.toFixed(2), '%')
|
||||
console.log()
|
||||
|
||||
// 如果是 1e4 精度(basis points)
|
||||
const apyE4 = Number(borrowRate) / 1e4
|
||||
console.log('假设精度 1e4 (basis points):')
|
||||
console.log(' 年化利率 (APY):', apyE4.toFixed(2), '%')
|
||||
console.log()
|
||||
}
|
||||
|
||||
// 2. 获取存款利率
|
||||
const supplyRate = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getSupplyRate'
|
||||
})
|
||||
console.log('=== getSupplyRate ===')
|
||||
console.log('原始值:', supplyRate.toString())
|
||||
console.log()
|
||||
|
||||
if (supplyRate > 0n) {
|
||||
const apyE18 = Number(supplyRate) * secondsPerYear / 1e18
|
||||
console.log('假设精度 1e18 (每秒利率):')
|
||||
console.log(' 年化利率 (APY):', apyE18.toFixed(2), '%')
|
||||
console.log()
|
||||
}
|
||||
|
||||
// 3. 获取使用率
|
||||
const utilization = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getUtilization'
|
||||
})
|
||||
console.log('=== getUtilization ===')
|
||||
console.log('原始值:', utilization.toString())
|
||||
console.log()
|
||||
|
||||
if (utilization > 0n) {
|
||||
// Compound V3 使用率通常是 1e18 精度
|
||||
const utilizationPercent = Number(utilization) / 1e18 * 100
|
||||
console.log('假设精度 1e18:')
|
||||
console.log(' 使用率:', utilizationPercent.toFixed(2), '%')
|
||||
console.log()
|
||||
|
||||
// 如果是 1e4 精度
|
||||
const utilizationE4 = Number(utilization) / 1e4
|
||||
console.log('假设精度 1e4:')
|
||||
console.log(' 使用率:', utilizationE4.toFixed(2), '%')
|
||||
console.log()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
153
frontend/scripts/check-lending-config.js
Normal file
153
frontend/scripts/check-lending-config.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import { createPublicClient, http } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const CONFIGURATOR = '0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc'
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D'
|
||||
const YT_A = '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05'
|
||||
const USDC = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d'
|
||||
|
||||
const CONFIGURATOR_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'lendingProxy', type: 'address' }
|
||||
],
|
||||
name: 'getConfiguration',
|
||||
outputs: [
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'address', name: 'baseToken', type: 'address' },
|
||||
{ internalType: 'address', name: 'lendingPriceSource', type: 'address' },
|
||||
{ internalType: 'uint64', name: 'supplyKink', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'supplyPerYearInterestRateSlopeLow', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'supplyPerYearInterestRateSlopeHigh', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'supplyPerYearInterestRateBase', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowKink', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowPerYearInterestRateSlopeLow', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowPerYearInterestRateSlopeHigh', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowPerYearInterestRateBase', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'storeFrontPriceFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'trackingIndexScale', type: 'uint64' },
|
||||
{ internalType: 'uint104', name: 'baseBorrowMin', type: 'uint104' },
|
||||
{ internalType: 'uint104', name: 'targetReserves', type: 'uint104' },
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'address', name: 'asset', type: 'address' },
|
||||
{ internalType: 'uint8', name: 'decimals', type: 'uint8' },
|
||||
{ internalType: 'uint64', name: 'borrowCollateralFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'liquidateCollateralFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'liquidationFactor', type: 'uint64' },
|
||||
{ internalType: 'uint128', name: 'supplyCap', type: 'uint128' }
|
||||
],
|
||||
internalType: 'struct LendingConfiguration.AssetConfig[]',
|
||||
name: 'assetConfigs',
|
||||
type: 'tuple[]'
|
||||
}
|
||||
],
|
||||
internalType: 'struct LendingConfiguration.Configuration',
|
||||
name: '',
|
||||
type: 'tuple'
|
||||
}
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [],
|
||||
name: 'paused',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查 Lending 合约配置状态...\n')
|
||||
|
||||
try {
|
||||
// 读取配置
|
||||
const config = await client.readContract({
|
||||
address: CONFIGURATOR,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'getConfiguration',
|
||||
args: [LENDING_PROXY]
|
||||
})
|
||||
|
||||
// 检查暂停状态
|
||||
const isPaused = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'paused'
|
||||
})
|
||||
|
||||
console.log('=== 基础配置 ===')
|
||||
console.log('baseToken:', config.baseToken)
|
||||
console.log('是否为 USDC:', config.baseToken.toLowerCase() === USDC.toLowerCase() ? '✓ 是' : '✗ 否')
|
||||
console.log('lendingPriceSource:', config.lendingPriceSource)
|
||||
console.log('是否为零地址:', config.lendingPriceSource === '0x0000000000000000000000000000000000000000' ? '✗ 是(错误)' : '✓ 否')
|
||||
|
||||
console.log('\n=== 利率参数 ===')
|
||||
console.log('supplyKink:', config.supplyKink.toString())
|
||||
console.log('borrowKink:', config.borrowKink.toString())
|
||||
console.log('baseBorrowMin:', config.baseBorrowMin.toString())
|
||||
console.log('targetReserves:', config.targetReserves.toString())
|
||||
|
||||
console.log('\n=== 系统状态 ===')
|
||||
console.log('是否暂停:', isPaused ? '✗ 是(无法操作)' : '✓ 否')
|
||||
|
||||
console.log('\n=== 抵押品配置 ===')
|
||||
console.log('配置的抵押品数量:', config.assetConfigs.length)
|
||||
|
||||
const ytAConfig = config.assetConfigs.find(
|
||||
cfg => cfg.asset.toLowerCase() === YT_A.toLowerCase()
|
||||
)
|
||||
|
||||
if (ytAConfig) {
|
||||
console.log('\nYT-A 配置:')
|
||||
console.log(' 地址:', ytAConfig.asset)
|
||||
console.log(' 精度:', ytAConfig.decimals)
|
||||
console.log(' 借款抵押率:', Number(ytAConfig.borrowCollateralFactor) / 1e16, '%')
|
||||
console.log(' 清算抵押率:', Number(ytAConfig.liquidateCollateralFactor) / 1e16, '%')
|
||||
console.log(' 清算奖励:', Number(ytAConfig.liquidationFactor) / 1e16, '%')
|
||||
console.log(' 供应上限:', Number(ytAConfig.supplyCap) / 1e18, 'tokens')
|
||||
} else {
|
||||
console.log('\n✗ YT-A 未配置!')
|
||||
}
|
||||
|
||||
// 诊断
|
||||
console.log('\n=== 诊断结果 ===')
|
||||
const issues = []
|
||||
|
||||
if (config.baseToken === '0x0000000000000000000000000000000000000000') {
|
||||
issues.push('✗ baseToken 未设置(零地址)')
|
||||
}
|
||||
if (config.lendingPriceSource === '0x0000000000000000000000000000000000000000') {
|
||||
issues.push('✗ lendingPriceSource 未设置(零地址)')
|
||||
}
|
||||
if (isPaused) {
|
||||
issues.push('✗ 系统已暂停')
|
||||
}
|
||||
if (!ytAConfig) {
|
||||
issues.push('✗ YT-A 未配置')
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.log('发现问题:')
|
||||
issues.forEach(issue => console.log(' ' + issue))
|
||||
} else {
|
||||
console.log('✓ 所有配置正常')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ 读取配置失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
237
frontend/scripts/check-lending-setup.js
Normal file
237
frontend/scripts/check-lending-setup.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import { createPublicClient, http, parseAbi } from 'viem';
|
||||
import { arbitrumSepolia } from 'viem/chains';
|
||||
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D';
|
||||
const CONFIGURATOR_ADDRESS = '0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc';
|
||||
|
||||
const LENDING_ABI = parseAbi([
|
||||
'function owner() view returns (address)',
|
||||
'function configurator() view returns (address)',
|
||||
'function getConfigurator() view returns (address)',
|
||||
'function setConfigurator(address) external',
|
||||
'function paused() view returns (bool)',
|
||||
'function getTotalSupply() view returns (uint256)',
|
||||
'function getTotalBorrow() view returns (uint256)',
|
||||
'function getUtilization() view returns (uint256)',
|
||||
'function getBorrowRate() view returns (uint256)',
|
||||
'function getSupplyRate(uint256) view returns (uint256)',
|
||||
'function initialize(address,address,address) external'
|
||||
]);
|
||||
|
||||
const CONFIGURATOR_ABI = [
|
||||
{
|
||||
"inputs": [
|
||||
{ "internalType": "address", "name": "lendingProxy", "type": "address" }
|
||||
],
|
||||
"name": "getConfiguration",
|
||||
"outputs": [
|
||||
{
|
||||
"components": [
|
||||
{ "internalType": "address", "name": "baseToken", "type": "address" },
|
||||
{ "internalType": "address", "name": "lendingPriceSource", "type": "address" },
|
||||
{ "internalType": "uint64", "name": "supplyKink", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "supplyPerYearInterestRateSlopeLow", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "supplyPerYearInterestRateSlopeHigh", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "supplyPerYearInterestRateBase", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "borrowKink", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "borrowPerYearInterestRateSlopeLow", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "borrowPerYearInterestRateSlopeHigh", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "borrowPerYearInterestRateBase", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "storeFrontPriceFactor", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "trackingIndexScale", "type": "uint64" },
|
||||
{ "internalType": "uint104", "name": "baseBorrowMin", "type": "uint104" },
|
||||
{ "internalType": "uint104", "name": "targetReserves", "type": "uint104" },
|
||||
{
|
||||
"components": [
|
||||
{ "internalType": "address", "name": "asset", "type": "address" },
|
||||
{ "internalType": "uint8", "name": "decimals", "type": "uint8" },
|
||||
{ "internalType": "uint64", "name": "borrowCollateralFactor", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "liquidateCollateralFactor", "type": "uint64" },
|
||||
{ "internalType": "uint64", "name": "liquidationFactor", "type": "uint64" },
|
||||
{ "internalType": "uint128", "name": "supplyCap", "type": "uint128" }
|
||||
],
|
||||
"internalType": "struct LendingConfiguration.AssetConfig[]",
|
||||
"name": "assetConfigs",
|
||||
"type": "tuple[]"
|
||||
}
|
||||
],
|
||||
"internalType": "struct LendingConfiguration.Configuration",
|
||||
"name": "",
|
||||
"type": "tuple"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
];
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc')
|
||||
});
|
||||
|
||||
async function checkLending() {
|
||||
console.log('🔍 检查 Lending 合约设置\n');
|
||||
|
||||
try {
|
||||
// 检查 owner
|
||||
const owner = await publicClient.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'owner'
|
||||
});
|
||||
console.log('1️⃣ Lending Owner:', owner);
|
||||
} catch (error) {
|
||||
console.log('1️⃣ ❌ 读取 owner 失败');
|
||||
}
|
||||
|
||||
// 尝试读取configurator地址
|
||||
const getters = ['configurator', 'getConfigurator'];
|
||||
let foundConfigurator = null;
|
||||
|
||||
for (const getter of getters) {
|
||||
try {
|
||||
console.log(`\n2️⃣ 尝试调用 ${getter}()...`);
|
||||
const configuratorAddr = await publicClient.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: getter
|
||||
});
|
||||
console.log(` ✅ ${getter}() = ${configuratorAddr}`);
|
||||
console.log(` 期望地址: ${CONFIGURATOR_ADDRESS}`);
|
||||
console.log(` 匹配: ${configuratorAddr.toLowerCase() === CONFIGURATOR_ADDRESS.toLowerCase() ? '✅' : '❌'}`);
|
||||
|
||||
foundConfigurator = configuratorAddr;
|
||||
|
||||
if (configuratorAddr.toLowerCase() !== CONFIGURATOR_ADDRESS.toLowerCase()) {
|
||||
console.log('\n⚠️ 警告: Configurator 地址不匹配!');
|
||||
console.log('💡 Lending 合约指向了不同的 Configurator');
|
||||
console.log(` Lending 中的: ${configuratorAddr}`);
|
||||
console.log(` 前端使用的: ${CONFIGURATOR_ADDRESS}`);
|
||||
}
|
||||
|
||||
break;
|
||||
} catch (error) {
|
||||
console.log(` ❌ ${getter}() 不存在或调用失败`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundConfigurator) {
|
||||
console.log('\n⚠️ 未找到 Configurator 地址!');
|
||||
console.log('💡 Lending 合约可能需要先通过 setConfigurator() 设置 Configurator 地址\n');
|
||||
}
|
||||
|
||||
// 检查是否暂停
|
||||
try {
|
||||
const isPaused = await publicClient.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'paused'
|
||||
});
|
||||
console.log('\n3️⃣ 系统暂停状态:', isPaused ? '已暂停' : '运行中');
|
||||
} catch (error) {
|
||||
console.log('\n3️⃣ ❌ 读取暂停状态失败');
|
||||
}
|
||||
|
||||
// 检查流动性
|
||||
try {
|
||||
const liquidity = await publicClient.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getTotalSupply'
|
||||
});
|
||||
console.log('4️⃣ 总供应量:', (Number(liquidity) / 1e6).toFixed(2), 'USDC');
|
||||
} catch (error) {
|
||||
console.log('4️⃣ ❌ 读取总供应量失败');
|
||||
}
|
||||
|
||||
// 检查系统数据查询函数
|
||||
console.log('\n📊 系统数据查询:');
|
||||
|
||||
try {
|
||||
const totalBorrow = await publicClient.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getTotalBorrow'
|
||||
});
|
||||
console.log(' 总借款:', (Number(totalBorrow) / 1e6).toFixed(2), 'USDC');
|
||||
} catch (error) {
|
||||
console.log(' ❌ getTotalBorrow() 调用失败');
|
||||
}
|
||||
|
||||
try {
|
||||
const utilization = await publicClient.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getUtilization'
|
||||
});
|
||||
console.log(' 资金利用率:', (Number(utilization) / 1e18 * 100).toFixed(2), '%');
|
||||
} catch (error) {
|
||||
console.log(' ❌ getUtilization() 调用失败');
|
||||
}
|
||||
|
||||
try {
|
||||
const borrowRate = await publicClient.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getBorrowRate'
|
||||
});
|
||||
const borrowAPR = (Number(borrowRate) / 1e18 * 100).toFixed(2);
|
||||
console.log(' 借款年利率:', borrowAPR, '%');
|
||||
} catch (error) {
|
||||
console.log(' ❌ getBorrowRate() 调用失败');
|
||||
}
|
||||
|
||||
try {
|
||||
const utilization = await publicClient.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getUtilization'
|
||||
});
|
||||
const supplyRate = await publicClient.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getSupplyRate',
|
||||
args: [utilization]
|
||||
});
|
||||
const supplyAPR = (Number(supplyRate) / 1e18 * 100).toFixed(2);
|
||||
console.log(' 存款年利率:', supplyAPR, '%');
|
||||
} catch (error) {
|
||||
console.log(' ❌ getSupplyRate() 调用失败');
|
||||
}
|
||||
|
||||
// 检查 Configurator 配置
|
||||
console.log('\n⚙️ Configurator 配置:');
|
||||
|
||||
try {
|
||||
const config = await publicClient.readContract({
|
||||
address: CONFIGURATOR_ADDRESS,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'getConfiguration',
|
||||
args: [LENDING_PROXY]
|
||||
});
|
||||
|
||||
console.log(' 基础资产:', config.baseToken);
|
||||
console.log(' 价格源:', config.lendingPriceSource);
|
||||
console.log(' 抵押品资产数量:', config.assetConfigs.length);
|
||||
|
||||
config.assetConfigs.forEach((asset, index) => {
|
||||
console.log(`\n 抵押品 ${index + 1}:`);
|
||||
console.log(` 地址: ${asset.asset}`);
|
||||
console.log(` 精度: ${asset.decimals}`);
|
||||
console.log(` 借款抵押率: ${(Number(asset.borrowCollateralFactor) / 1e18 * 100).toFixed(2)}%`);
|
||||
console.log(` 清算抵押率: ${(Number(asset.liquidateCollateralFactor) / 1e18 * 100).toFixed(2)}%`);
|
||||
console.log(` 清算奖励: ${(Number(asset.liquidationFactor) / 1e18 * 100).toFixed(2)}%`);
|
||||
console.log(` 供应上限: ${(Number(asset.supplyCap) / (10 ** asset.decimals)).toFixed(2)}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(' ❌ 读取 Configurator 配置失败:', error.message);
|
||||
}
|
||||
|
||||
console.log('\n💡 总结:');
|
||||
console.log(' - 检查 Lending 合约是否正确设置了 Configurator 地址');
|
||||
console.log(' - 检查 Configurator 合约是否正确设置了 Lending 地址');
|
||||
console.log(' - 这两个合约需要相互关联才能正常工作');
|
||||
}
|
||||
|
||||
checkLending();
|
||||
46
frontend/scripts/check-owner.js
Normal file
46
frontend/scripts/check-owner.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createPublicClient, http } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const CONFIGURATOR = '0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc'
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D'
|
||||
|
||||
const OWNER_ABI = [
|
||||
{
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('🔍 检查合约 owner...\n')
|
||||
|
||||
try {
|
||||
const configuratorOwner = await client.readContract({
|
||||
address: CONFIGURATOR,
|
||||
abi: OWNER_ABI,
|
||||
functionName: 'owner'
|
||||
})
|
||||
|
||||
const lendingOwner = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: OWNER_ABI,
|
||||
functionName: 'owner'
|
||||
})
|
||||
|
||||
console.log('Configurator Owner:', configuratorOwner)
|
||||
console.log('Lending Proxy Owner:', lendingOwner)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 读取 owner 失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
110
frontend/scripts/check-price-decimals.js
Normal file
110
frontend/scripts/check-price-decimals.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PRICE_FEED = getAddress('0xE82c7cB9CfA42D6eb7e443956b78f8290249c316')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查价格精度\n')
|
||||
|
||||
try {
|
||||
// 检查 decimals
|
||||
const decimals = await client.readContract({
|
||||
address: LENDING_PRICE_FEED,
|
||||
abi: [{
|
||||
inputs: [],
|
||||
name: 'decimals',
|
||||
outputs: [{ name: '', type: 'uint8' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'decimals'
|
||||
})
|
||||
console.log('✓ decimals:', decimals)
|
||||
|
||||
const price = await client.readContract({
|
||||
address: LENDING_PRICE_FEED,
|
||||
abi: [{
|
||||
inputs: [{ name: 'asset', type: 'address' }],
|
||||
name: 'getPrice',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'getPrice',
|
||||
args: [YT_A]
|
||||
})
|
||||
|
||||
console.log('\nYT-A 价格:')
|
||||
console.log(' 原始值:', price.toString())
|
||||
console.log(' ÷ 1e8:', Number(price) / 1e8)
|
||||
console.log(' ÷ 1e18:', Number(price) / 1e18)
|
||||
console.log(' ÷ 1e', decimals, ':', Number(price) / (10 ** Number(decimals)))
|
||||
|
||||
// 如果价格是 1e22,可能是:
|
||||
// - 错误地设置为 1e30 的固定价格(Compound V3 使用 1e30 作为基准)
|
||||
// - 或者配置了错误的精度
|
||||
|
||||
if (price > 1e20) {
|
||||
console.log('\n⚠️ 警告:价格异常高!')
|
||||
console.log('可能原因:')
|
||||
console.log('1. 价格设置错误(使用了 1e30 而不是合适的精度)')
|
||||
console.log('2. 精度配置错误')
|
||||
console.log('\n建议:')
|
||||
console.log('如果 YT-A 应该价值 $1,价格应该设置为:')
|
||||
console.log(` - 如果精度是 8: ${1e8}`)
|
||||
console.log(` - 如果精度是 18: ${1e18}`)
|
||||
console.log(` - 如果使用 Compound V3 格式: ${1e18} (价格) * ${1e18} (精度) / (资产精度)`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ 查询失败:', error.message)
|
||||
console.log('\n尝试读取价格(无 decimals):')
|
||||
|
||||
try {
|
||||
const price = await client.readContract({
|
||||
address: LENDING_PRICE_FEED,
|
||||
abi: [{
|
||||
inputs: [{ name: 'asset', type: 'address' }],
|
||||
name: 'getPrice',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'getPrice',
|
||||
args: [YT_A]
|
||||
})
|
||||
console.log('价格:', price.toString())
|
||||
console.log('\n如果这个值是 1e30 数量级,说明使用了 Compound V3 的价格格式')
|
||||
console.log('Compound V3 价格 = (USD价格 * 1e', await getBaseScale(), ') / (10^资产精度)')
|
||||
} catch (e2) {
|
||||
console.error('仍然失败:', e2.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getBaseScale() {
|
||||
try {
|
||||
const scale = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [],
|
||||
name: 'baseScale',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'baseScale'
|
||||
})
|
||||
return scale
|
||||
} catch {
|
||||
return '?'
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
75
frontend/scripts/check-price-oracle.js
Normal file
75
frontend/scripts/check-price-oracle.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PRICE_FEED = getAddress('0xE82c7cB9CfA42D6eb7e443956b78f8290249c316')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
const USDC = getAddress('0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d')
|
||||
|
||||
const PRICE_FEED_ABI = [
|
||||
{
|
||||
inputs: [{ name: 'asset', type: 'address' }],
|
||||
name: 'getPrice',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'asset', type: 'address' }],
|
||||
name: 'getAssetPrice',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function testPrice(asset, name) {
|
||||
console.log(`\n=== ${name} (${asset}) ===`)
|
||||
|
||||
// 尝试 getPrice
|
||||
try {
|
||||
const price1 = await client.readContract({
|
||||
address: LENDING_PRICE_FEED,
|
||||
abi: PRICE_FEED_ABI,
|
||||
functionName: 'getPrice',
|
||||
args: [asset]
|
||||
})
|
||||
console.log('✓ getPrice:', price1.toString(), '(', Number(price1) / 1e8, 'USD )')
|
||||
} catch (error) {
|
||||
console.log('✗ getPrice 失败:', error.message.split('\n')[0])
|
||||
}
|
||||
|
||||
// 尝试 getAssetPrice
|
||||
try {
|
||||
const price2 = await client.readContract({
|
||||
address: LENDING_PRICE_FEED,
|
||||
abi: PRICE_FEED_ABI,
|
||||
functionName: 'getAssetPrice',
|
||||
args: [asset]
|
||||
})
|
||||
console.log('✓ getAssetPrice:', price2.toString(), '(', Number(price2) / 1e8, 'USD )')
|
||||
} catch (error) {
|
||||
console.log('✗ getAssetPrice 失败:', error.message.split('\n')[0])
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('检查价格预言机\n')
|
||||
console.log('Price Feed 地址:', LENDING_PRICE_FEED)
|
||||
|
||||
await testPrice(YT_A, 'YT-A')
|
||||
await testPrice(USDC, 'USDC')
|
||||
|
||||
console.log('\n=== 诊断 ===')
|
||||
console.log('如果价格查询失败,这就是为什么 getUserAccountData 会 revert')
|
||||
console.log('需要:')
|
||||
console.log('1. 检查价格预言机合约是否正确部署')
|
||||
console.log('2. 检查是否为 YT-A 设置了价格')
|
||||
console.log('3. 可能需要手动调用 setPrice() 来设置价格')
|
||||
}
|
||||
|
||||
main()
|
||||
55
frontend/scripts/check-price.js
Normal file
55
frontend/scripts/check-price.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { createPublicClient, http } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const PRICE_FEED = '0xE82c7cB9CfA42D6eb7e443956b78f8290249c316'
|
||||
const YT_A = '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05'
|
||||
|
||||
const PRICE_FEED_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'getPrice',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查价格源...\n')
|
||||
|
||||
try {
|
||||
const price = await client.readContract({
|
||||
address: PRICE_FEED,
|
||||
abi: PRICE_FEED_ABI,
|
||||
functionName: 'getPrice',
|
||||
args: [YT_A]
|
||||
})
|
||||
|
||||
console.log('YT-A 地址:', YT_A)
|
||||
console.log('价格 (原始值):', price.toString())
|
||||
console.log('价格 (格式化):', Number(price) / 1e8, 'USD')
|
||||
|
||||
if (price === 0n) {
|
||||
console.log('\n✗ 错误: 价格为 0!')
|
||||
console.log(' 这会导致存入失败,因为合约无法计算抵押品价值')
|
||||
} else {
|
||||
console.log('\n✓ 价格正常')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ 读取价格失败:', error.message)
|
||||
console.log('\n可能的原因:')
|
||||
console.log(' 1. 价格源合约没有设置 YT-A 的价格')
|
||||
console.log(' 2. getPrice 函数不存在或签名不匹配')
|
||||
console.log(' 3. YT-A 地址在价格源中未注册')
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
76
frontend/scripts/check-recent-events.js
Normal file
76
frontend/scripts/check-recent-events.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
|
||||
const SUPPLY_COLLATERAL_EVENT = {
|
||||
type: 'event',
|
||||
name: 'SupplyCollateral',
|
||||
inputs: [
|
||||
{ indexed: true, name: 'from', type: 'address' },
|
||||
{ indexed: true, name: 'dst', type: 'address' },
|
||||
{ indexed: true, name: 'asset', type: 'address' },
|
||||
{ indexed: false, name: 'amount', type: 'uint256' }
|
||||
]
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查最近的 SupplyCollateral 事件...\n')
|
||||
|
||||
const latestBlock = await client.getBlockNumber()
|
||||
console.log('最新区块:', latestBlock)
|
||||
console.log('查询范围: 最近 1000 个区块\n')
|
||||
|
||||
try {
|
||||
const logs = await client.getLogs({
|
||||
address: LENDING_PROXY,
|
||||
event: SUPPLY_COLLATERAL_EVENT,
|
||||
fromBlock: latestBlock - 1000n,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
|
||||
if (logs.length === 0) {
|
||||
console.log('未找到任何 SupplyCollateral 事件')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('找到', logs.length, '个事件:\n')
|
||||
|
||||
logs.forEach((log, i) => {
|
||||
const { from, dst, asset, amount } = log.args
|
||||
const isCurrentUser = from.toLowerCase() === USER.toLowerCase()
|
||||
|
||||
console.log((i + 1) + '. 区块', log.blockNumber)
|
||||
console.log(' 交易:', log.transactionHash)
|
||||
console.log(' From:', from, isCurrentUser ? '<- 你的地址' : '')
|
||||
console.log(' To:', dst)
|
||||
console.log(' Asset:', asset)
|
||||
console.log(' Amount:', Number(amount) / 1e18)
|
||||
console.log()
|
||||
})
|
||||
|
||||
const userLogs = logs.filter(log =>
|
||||
log.args.from.toLowerCase() === USER.toLowerCase()
|
||||
)
|
||||
|
||||
if (userLogs.length > 0) {
|
||||
console.log('\n找到你的', userLogs.length, '笔存入记录!')
|
||||
console.log('最近一笔:')
|
||||
const latest = userLogs[userLogs.length - 1]
|
||||
console.log(' 交易哈希:', latest.transactionHash)
|
||||
console.log(' 数量:', Number(latest.args.amount) / 1e18, 'YT-A')
|
||||
console.log(' 区块:', latest.blockNumber)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
108
frontend/scripts/check-token-transfers.js
Normal file
108
frontend/scripts/check-token-transfers.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
|
||||
const TRANSFER_EVENT = {
|
||||
type: 'event',
|
||||
name: 'Transfer',
|
||||
inputs: [
|
||||
{ indexed: true, name: 'from', type: 'address' },
|
||||
{ indexed: true, name: 'to', type: 'address' },
|
||||
{ indexed: false, name: 'value', type: 'uint256' }
|
||||
]
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查 YT-A 代币转账... \n')
|
||||
|
||||
const latestBlock = await client.getBlockNumber()
|
||||
console.log('最新区块:', latestBlock)
|
||||
console.log('查询范围: 最近 1000 个区块\n')
|
||||
|
||||
try {
|
||||
// 检查转入 Lending 合约的 Transfer
|
||||
console.log('--- 1. 查找转入 Lending 合约的转账 ---')
|
||||
const logsTo = await client.getLogs({
|
||||
address: YT_A,
|
||||
event: TRANSFER_EVENT,
|
||||
args: {
|
||||
to: LENDING_PROXY
|
||||
},
|
||||
fromBlock: latestBlock - 1000n,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
|
||||
if (logsTo.length === 0) {
|
||||
console.log('✗ 未找到任何转入 Lending 合约的转账\n')
|
||||
} else {
|
||||
console.log('✓ 找到', logsTo.length, '笔转入记录:\n')
|
||||
logsTo.forEach((log, i) => {
|
||||
const { from, to, value } = log.args
|
||||
const isFromUser = from.toLowerCase() === USER.toLowerCase()
|
||||
console.log((i + 1) + '. 区块', log.blockNumber)
|
||||
console.log(' 交易:', log.transactionHash)
|
||||
console.log(' From:', from, isFromUser ? '<- 你的地址' : '')
|
||||
console.log(' To:', to)
|
||||
console.log(' Amount:', Number(value) / 1e18, 'YT-A')
|
||||
console.log()
|
||||
})
|
||||
}
|
||||
|
||||
// 检查从用户发出的 Transfer
|
||||
console.log('--- 2. 查找从你的地址发出的转账 ---')
|
||||
const logsFrom = await client.getLogs({
|
||||
address: YT_A,
|
||||
event: TRANSFER_EVENT,
|
||||
args: {
|
||||
from: USER
|
||||
},
|
||||
fromBlock: latestBlock - 1000n,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
|
||||
if (logsFrom.length === 0) {
|
||||
console.log('✗ 未找到任何从你地址发出的转账\n')
|
||||
} else {
|
||||
console.log('✓ 找到', logsFrom.length, '笔转出记录:\n')
|
||||
logsFrom.forEach((log, i) => {
|
||||
const { from, to, value } = log.args
|
||||
const isToLending = to.toLowerCase() === LENDING_PROXY.toLowerCase()
|
||||
console.log((i + 1) + '. 区块', log.blockNumber)
|
||||
console.log(' 交易:', log.transactionHash)
|
||||
console.log(' From:', from)
|
||||
console.log(' To:', to, isToLending ? '<- Lending 合约' : '')
|
||||
console.log(' Amount:', Number(value) / 1e18, 'YT-A')
|
||||
console.log()
|
||||
})
|
||||
}
|
||||
|
||||
// 检查用户的 YT-A 余额
|
||||
console.log('--- 3. 检查当前 YT-A 余额 ---')
|
||||
const balance = await client.readContract({
|
||||
address: YT_A,
|
||||
abi: [{
|
||||
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
||||
name: 'balanceOf',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'balanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
console.log('你的 YT-A 余额:', Number(balance) / 1e18, '\n')
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
75
frontend/scripts/check-transaction-details.js
Normal file
75
frontend/scripts/check-transaction-details.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const TX_HASH = '0xf38e4201b397b2e267402e2b355c811fb0d99ce2c9f67b3b0fff26028a7a4df4'
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查交易详情...\n')
|
||||
console.log('交易哈希:', TX_HASH, '\n')
|
||||
|
||||
try {
|
||||
// 获取交易详情
|
||||
const tx = await client.getTransaction({ hash: TX_HASH })
|
||||
|
||||
console.log('=== 交易基本信息 ===')
|
||||
console.log('From:', tx.from)
|
||||
console.log('To:', tx.to)
|
||||
console.log('区块:', tx.blockNumber)
|
||||
console.log('Gas Used:', tx.gas.toString())
|
||||
console.log('\n函数调用 (input):', tx.input.slice(0, 200) + '...')
|
||||
console.log('函数选择器:', tx.input.slice(0, 10))
|
||||
|
||||
// 获取交易回执
|
||||
const receipt = await client.getTransactionReceipt({ hash: TX_HASH })
|
||||
|
||||
console.log('\n=== 交易回执 ===')
|
||||
console.log('状态:', receipt.status === 'success' ? '✓ 成功' : '✗ 失败')
|
||||
console.log('Gas 实际使用:', receipt.gasUsed.toString())
|
||||
console.log('事件数量:', receipt.logs.length)
|
||||
|
||||
console.log('\n=== 事件日志 ===')
|
||||
receipt.logs.forEach((log, i) => {
|
||||
console.log(`\n事件 ${i + 1}:`)
|
||||
console.log(' 合约:', log.address)
|
||||
console.log(' Topics[0]:', log.topics[0])
|
||||
|
||||
// 识别 Transfer 事件 (keccak256("Transfer(address,address,uint256)"))
|
||||
if (log.topics[0] === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') {
|
||||
console.log(' 类型: Transfer 事件')
|
||||
if (log.topics[1]) console.log(' From:', '0x' + log.topics[1].slice(26))
|
||||
if (log.topics[2]) console.log(' To:', '0x' + log.topics[2].slice(26))
|
||||
}
|
||||
// 识别 SupplyCollateral 事件
|
||||
else if (log.topics[0] === '0x...') { // 需要知道实际的事件签名
|
||||
console.log(' 类型: SupplyCollateral 事件')
|
||||
}
|
||||
else {
|
||||
console.log(' 类型: 未知事件')
|
||||
}
|
||||
|
||||
console.log(' Data:', log.data.slice(0, 66) + (log.data.length > 66 ? '...' : ''))
|
||||
})
|
||||
|
||||
// 解析函数选择器
|
||||
const selector = tx.input.slice(0, 10)
|
||||
console.log('\n=== 函数识别 ===')
|
||||
const functionMap = {
|
||||
'0x47e7ef24': 'deposit(address,uint256)',
|
||||
'0xe8eda9df': 'supplyCollateral(address,uint256)',
|
||||
'0x23b872dd': 'transferFrom(address,address,uint256)',
|
||||
'0xf213159c': 'supply(uint256)',
|
||||
'0x2e1a7d4d': 'withdraw(uint256)'
|
||||
}
|
||||
console.log('调用函数:', functionMap[selector] || '未知: ' + selector)
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
158
frontend/scripts/check-usdc-supply-history.js
Normal file
158
frontend/scripts/check-usdc-supply-history.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USDC = getAddress('0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
|
||||
const TRANSFER_EVENT = {
|
||||
type: 'event',
|
||||
name: 'Transfer',
|
||||
inputs: [
|
||||
{ indexed: true, name: 'from', type: 'address' },
|
||||
{ indexed: true, name: 'to', type: 'address' },
|
||||
{ indexed: false, name: 'value', type: 'uint256' }
|
||||
]
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查 USDC 供应/提取历史...\n')
|
||||
console.log('用户:', USER)
|
||||
console.log('Lending 合约:', LENDING_PROXY)
|
||||
console.log()
|
||||
|
||||
const latestBlock = await client.getBlockNumber()
|
||||
console.log('最新区块:', latestBlock)
|
||||
console.log('查询范围: 最近 10000 个区块\n')
|
||||
|
||||
try {
|
||||
// 1. 检查用户转入 Lending 的 USDC(supply 操作)
|
||||
console.log('=== 1. USDC 转入 Lending 合约(Supply)===')
|
||||
const logsToLending = await client.getLogs({
|
||||
address: USDC,
|
||||
event: TRANSFER_EVENT,
|
||||
args: {
|
||||
from: USER,
|
||||
to: LENDING_PROXY
|
||||
},
|
||||
fromBlock: latestBlock - 10000n,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
|
||||
if (logsToLending.length > 0) {
|
||||
console.log(`找到 ${logsToLending.length} 笔转入记录:\n`)
|
||||
let totalSupplied = 0n
|
||||
logsToLending.forEach((log, i) => {
|
||||
const { value } = log.args
|
||||
totalSupplied += value
|
||||
console.log(`${i + 1}. 区块 ${log.blockNumber}`)
|
||||
console.log(` 交易: ${log.transactionHash}`)
|
||||
console.log(` 数量: ${Number(value) / 1e6} USDC`)
|
||||
console.log()
|
||||
})
|
||||
console.log(`总存入: ${Number(totalSupplied) / 1e6} USDC\n`)
|
||||
} else {
|
||||
console.log('未找到转入记录\n')
|
||||
}
|
||||
|
||||
// 2. 检查 Lending 转给用户的 USDC(withdraw 操作)
|
||||
console.log('=== 2. USDC 从 Lending 转出(Withdraw)===')
|
||||
const logsFromLending = await client.getLogs({
|
||||
address: USDC,
|
||||
event: TRANSFER_EVENT,
|
||||
args: {
|
||||
from: LENDING_PROXY,
|
||||
to: USER
|
||||
},
|
||||
fromBlock: latestBlock - 10000n,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
|
||||
if (logsFromLending.length > 0) {
|
||||
console.log(`找到 ${logsFromLending.length} 笔转出记录:\n`)
|
||||
let totalWithdrawn = 0n
|
||||
logsFromLending.forEach((log, i) => {
|
||||
const { value } = log.args
|
||||
totalWithdrawn += value
|
||||
console.log(`${i + 1}. 区块 ${log.blockNumber}`)
|
||||
console.log(` 交易: ${log.transactionHash}`)
|
||||
console.log(` 数量: ${Number(value) / 1e6} USDC`)
|
||||
console.log()
|
||||
})
|
||||
console.log(`总提取: ${Number(totalWithdrawn) / 1e6} USDC\n`)
|
||||
} else {
|
||||
console.log('未找到转出记录\n')
|
||||
}
|
||||
|
||||
// 3. 获取当前余额
|
||||
console.log('=== 3. 当前状态 ===')
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'getBalance',
|
||||
outputs: [{ name: '', type: 'int256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'borrowBalanceOf',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
const balance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getBalance',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
const borrowBalance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'borrowBalanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
console.log('USDC 余额(在 Lending 中):', Number(balance) / 1e6, 'USDC')
|
||||
console.log(' 原始值:', balance.toString())
|
||||
console.log(' 正数 = 存款,负数 = 借款')
|
||||
console.log()
|
||||
console.log('借款余额:', Number(borrowBalance) / 1e6, 'USDC')
|
||||
console.log()
|
||||
|
||||
// 4. 分析
|
||||
console.log('=== 分析 ===')
|
||||
if (balance > 0) {
|
||||
console.log('⚠️ 你当前有存款!')
|
||||
console.log(` 存款金额: ${Number(balance) / 1e6} USDC`)
|
||||
console.log()
|
||||
console.log('这意味着:')
|
||||
console.log(` - 如果你借款 ≤ ${Number(balance) / 1e6} USDC,只是提取存款`)
|
||||
console.log(` - 只有借款 > ${Number(balance) / 1e6} USDC,才会产生债务`)
|
||||
console.log()
|
||||
console.log('建议:')
|
||||
console.log(' 1. 如果要真正借款,请借款金额大于当前存款')
|
||||
console.log(` 例如:借款 ${Number(balance) / 1e6 + 100} USDC`)
|
||||
console.log(' 2. 或者先提取所有存款,再借款')
|
||||
} else if (balance < 0) {
|
||||
console.log('✓ 你有借款!')
|
||||
console.log(` 借款金额: ${-Number(balance) / 1e6} USDC`)
|
||||
} else {
|
||||
console.log('✓ 你没有存款也没有借款')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
100
frontend/scripts/check-user-account-data.js
Normal file
100
frontend/scripts/check-user-account-data.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: '_user', type: 'address' }],
|
||||
name: 'getUserAccountData',
|
||||
outputs: [
|
||||
{ internalType: 'uint256', name: 'totalCollateralValue', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: 'totalBorrowValue', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: 'availableToBorrow', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: 'healthFactor', type: 'uint256' }
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: '_user', type: 'address' },
|
||||
{ internalType: 'address', name: '_collateralAsset', type: 'address' }
|
||||
],
|
||||
name: 'getUserCollateral',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n检查用户账户数据...\n')
|
||||
console.log('用户地址:', USER)
|
||||
console.log('Lending 合约:', LENDING_PROXY)
|
||||
|
||||
try {
|
||||
// 1. 检查抵押品余额
|
||||
console.log('\n--- 1. 检查 getUserCollateral (YT-A) ---')
|
||||
const collateral = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getUserCollateral',
|
||||
args: [USER, YT_A]
|
||||
})
|
||||
console.log('✓ getUserCollateral:', Number(collateral) / 1e18, 'YT-A')
|
||||
console.log(' 原始值:', collateral.toString())
|
||||
|
||||
// 2. 检查账户数据
|
||||
console.log('\n--- 2. 检查 getUserAccountData ---')
|
||||
const accountData = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getUserAccountData',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
console.log('✓ getUserAccountData 返回:')
|
||||
console.log(' totalCollateralValue:', Number(accountData[0]) / 1e6, 'USD')
|
||||
console.log(' totalBorrowValue:', Number(accountData[1]) / 1e6, 'USD')
|
||||
console.log(' availableToBorrow:', Number(accountData[2]) / 1e6, 'USD')
|
||||
console.log(' healthFactor:', Number(accountData[3]) / 1e4, '%')
|
||||
|
||||
console.log('\n 原始值:')
|
||||
console.log(' [0]:', accountData[0].toString())
|
||||
console.log(' [1]:', accountData[1].toString())
|
||||
console.log(' [2]:', accountData[2].toString())
|
||||
console.log(' [3]:', accountData[3].toString())
|
||||
|
||||
// 3. 诊断
|
||||
console.log('\n--- 诊断 ---')
|
||||
if (collateral > 0n && accountData[0] === 0n) {
|
||||
console.log('⚠️ 问题发现:')
|
||||
console.log(' - getUserCollateral 返回有值(', Number(collateral) / 1e18, 'YT-A)')
|
||||
console.log(' - 但 getUserAccountData 返回总抵押价值为 0')
|
||||
console.log(' 可能原因:')
|
||||
console.log(' 1. 价格预言机返回 0(YT-A 价格未设置)')
|
||||
console.log(' 2. getUserAccountData 函数逻辑错误')
|
||||
console.log(' 3. 抵押品资产未在配置中激活')
|
||||
} else if (collateral === 0n) {
|
||||
console.log('✓ 抵押品余额为 0,这是正常的(如果之前交易失败)')
|
||||
} else if (accountData[0] > 0n) {
|
||||
console.log('✓ 一切正常,抵押品价值已正确计算')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ 调用失败:', error.message)
|
||||
if (error.message.includes('Contract function')) {
|
||||
console.log('\n可能原因:getUserAccountData 函数不存在或签名不匹配')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
106
frontend/scripts/check-withdraw-transactions.js
Normal file
106
frontend/scripts/check-withdraw-transactions.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { createPublicClient, http, getAddress, keccak256, toBytes } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
|
||||
// 计算函数选择器
|
||||
const withdrawSelector = keccak256(toBytes('withdraw(uint256)')).slice(0, 10)
|
||||
const supplyCollateralSelector = keccak256(toBytes('supplyCollateral(address,uint256)')).slice(0, 10)
|
||||
|
||||
console.log('\n=== 查找用户的交易记录 ===\n')
|
||||
console.log('用户地址:', USER)
|
||||
console.log('Lending 合约:', LENDING_PROXY)
|
||||
console.log('\n函数选择器:')
|
||||
console.log(' withdraw:', withdrawSelector)
|
||||
console.log(' supplyCollateral:', supplyCollateralSelector)
|
||||
console.log()
|
||||
|
||||
async function main() {
|
||||
const latestBlock = await client.getBlockNumber()
|
||||
console.log('最新区块:', latestBlock)
|
||||
console.log('查询范围: 最近 10000 个区块\n')
|
||||
|
||||
// 获取用户发送到 Lending 合约的所有交易
|
||||
const fromBlock = latestBlock - 10000n
|
||||
const toBlock = latestBlock
|
||||
|
||||
console.log('=== 查找所有用户 → Lending 的交易 ===\n')
|
||||
|
||||
let blockNum = fromBlock
|
||||
const transactions = []
|
||||
|
||||
while (blockNum <= toBlock) {
|
||||
const endBlock = blockNum + 1000n > toBlock ? toBlock : blockNum + 1000n
|
||||
|
||||
try {
|
||||
const block = await client.getBlock({
|
||||
blockNumber: blockNum,
|
||||
includeTransactions: true
|
||||
})
|
||||
|
||||
for (const tx of block.transactions) {
|
||||
if (typeof tx === 'object' &&
|
||||
tx.from.toLowerCase() === USER.toLowerCase() &&
|
||||
tx.to?.toLowerCase() === LENDING_PROXY.toLowerCase()) {
|
||||
transactions.push({
|
||||
hash: tx.hash,
|
||||
blockNumber: block.number,
|
||||
input: tx.input
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip blocks without transactions
|
||||
}
|
||||
|
||||
blockNum += 1000n
|
||||
}
|
||||
|
||||
console.log(`找到 ${transactions.length} 笔交易\n`)
|
||||
|
||||
if (transactions.length === 0) {
|
||||
console.log('没有找到任何交易')
|
||||
return
|
||||
}
|
||||
|
||||
// 分析每笔交易
|
||||
for (const tx of transactions) {
|
||||
console.log('---')
|
||||
console.log('交易哈希:', tx.hash)
|
||||
console.log('区块:', tx.blockNumber.toString())
|
||||
|
||||
const selector = tx.input.slice(0, 10)
|
||||
console.log('函数选择器:', selector)
|
||||
|
||||
let functionName = '未知'
|
||||
if (selector === withdrawSelector) {
|
||||
functionName = 'withdraw (借款/提取)'
|
||||
// 解析参数 (uint256)
|
||||
const amountHex = '0x' + tx.input.slice(10)
|
||||
const amount = BigInt(amountHex)
|
||||
console.log('调用函数:', functionName)
|
||||
console.log('金额:', Number(amount) / 1e6, 'USDC')
|
||||
} else if (selector === supplyCollateralSelector) {
|
||||
functionName = 'supplyCollateral (存入抵押品)'
|
||||
// 解析参数 (address, uint256)
|
||||
const assetAddress = '0x' + tx.input.slice(34, 74)
|
||||
const amountHex = '0x' + tx.input.slice(74)
|
||||
const amount = BigInt(amountHex)
|
||||
console.log('调用函数:', functionName)
|
||||
console.log('资产:', getAddress(assetAddress))
|
||||
console.log('金额:', Number(amount) / 1e18)
|
||||
} else {
|
||||
console.log('调用函数:', functionName, '-', selector)
|
||||
}
|
||||
|
||||
console.log()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
169
frontend/scripts/configure-collateral.js
Normal file
169
frontend/scripts/configure-collateral.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { createWalletClient, http, createPublicClient } from 'viem'
|
||||
import { privateKeyToAccount } from 'viem/accounts'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
// 从环境变量或命令行参数获取私钥
|
||||
const PRIVATE_KEY = process.env.PRIVATE_KEY || process.argv[2]
|
||||
|
||||
if (!PRIVATE_KEY || !PRIVATE_KEY.startsWith('0x')) {
|
||||
console.error('❌ 请提供私钥:')
|
||||
console.error(' 方式1: export PRIVATE_KEY=0x...')
|
||||
console.error(' 方式2: node configure-collateral.js 0x...')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const account = privateKeyToAccount(PRIVATE_KEY)
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http()
|
||||
})
|
||||
|
||||
const walletClient = createWalletClient({
|
||||
account,
|
||||
chain: arbitrumSepolia,
|
||||
transport: http()
|
||||
})
|
||||
|
||||
const CONFIGURATOR = '0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc'
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D'
|
||||
|
||||
const COLLATERAL_ASSETS = [
|
||||
{
|
||||
name: 'YT-A',
|
||||
address: '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05',
|
||||
collateralFactor: 7500, // 75%
|
||||
liquidationThreshold: 8500, // 85%
|
||||
liquidationBonus: 1000 // 10%
|
||||
},
|
||||
{
|
||||
name: 'YT-B',
|
||||
address: '0x181ef4011c35C4a2Fda08eBC5Cf509Ef58E553fF',
|
||||
collateralFactor: 7500,
|
||||
liquidationThreshold: 8500,
|
||||
liquidationBonus: 1000
|
||||
},
|
||||
{
|
||||
name: 'YT-C',
|
||||
address: '0xE9A5b9f3a2Eda4358f81d4E2eF4f3280A664e5B0',
|
||||
collateralFactor: 7500,
|
||||
liquidationThreshold: 8500,
|
||||
liquidationBonus: 1000
|
||||
}
|
||||
]
|
||||
|
||||
// Configurator ABI
|
||||
const CONFIGURATOR_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: '_asset', type: 'address' },
|
||||
{ internalType: 'uint256', name: '_collateralFactor', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: '_liquidationThreshold', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: '_liquidationBonus', type: 'uint256' }
|
||||
],
|
||||
name: 'setCollateralConfig',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: '_asset', type: 'address' },
|
||||
{ internalType: 'bool', name: '_isActive', type: 'bool' }
|
||||
],
|
||||
name: 'setCollateralActive',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function configureCollateral() {
|
||||
console.log('\n🔧 配置借贷抵押品\n')
|
||||
console.log('Configurator:', CONFIGURATOR)
|
||||
console.log('操作者:', account.address)
|
||||
console.log('')
|
||||
|
||||
// 检查权限
|
||||
try {
|
||||
const owner = await publicClient.readContract({
|
||||
address: CONFIGURATOR,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'owner'
|
||||
})
|
||||
console.log('Configurator Owner:', owner)
|
||||
|
||||
if (owner.toLowerCase() !== account.address.toLowerCase()) {
|
||||
console.error('\n❌ 错误: 当前账户不是 Configurator 的 owner')
|
||||
console.error(' 需要使用 owner 账户的私钥')
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ 无法检查owner,继续尝试配置...\n')
|
||||
}
|
||||
|
||||
// 配置每个抵押品
|
||||
for (const asset of COLLATERAL_ASSETS) {
|
||||
console.log(`\n📝 配置 ${asset.name} (${asset.address})`)
|
||||
console.log(` - 抵押率: ${asset.collateralFactor / 100}%`)
|
||||
console.log(` - 清算阈值: ${asset.liquidationThreshold / 100}%`)
|
||||
console.log(` - 清算奖励: ${asset.liquidationBonus / 100}%`)
|
||||
|
||||
try {
|
||||
// 1. 设置抵押品参数
|
||||
console.log(' → 设置参数...')
|
||||
const hash1 = await walletClient.writeContract({
|
||||
address: CONFIGURATOR,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'setCollateralConfig',
|
||||
args: [
|
||||
asset.address,
|
||||
asset.collateralFactor,
|
||||
asset.liquidationThreshold,
|
||||
asset.liquidationBonus
|
||||
]
|
||||
})
|
||||
console.log(' ✅ 参数设置交易:', hash1)
|
||||
|
||||
// 等待确认
|
||||
await publicClient.waitForTransactionReceipt({ hash: hash1 })
|
||||
console.log(' ✅ 交易已确认')
|
||||
|
||||
// 2. 激活抵押品
|
||||
console.log(' → 激活抵押品...')
|
||||
const hash2 = await walletClient.writeContract({
|
||||
address: CONFIGURATOR,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'setCollateralActive',
|
||||
args: [asset.address, true]
|
||||
})
|
||||
console.log(' ✅ 激活交易:', hash2)
|
||||
|
||||
// 等待确认
|
||||
await publicClient.waitForTransactionReceipt({ hash: hash2 })
|
||||
console.log(' ✅ 交易已确认')
|
||||
|
||||
console.log(` ✅ ${asset.name} 配置完成!`)
|
||||
} catch (error) {
|
||||
console.error(` ❌ 配置失败:`, error.message.split('\n')[0])
|
||||
|
||||
// 继续处理下一个
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ 所有抵押品配置完成!\n')
|
||||
console.log('现在可以尝试存入抵押品了。')
|
||||
}
|
||||
|
||||
configureCollateral().catch((error) => {
|
||||
console.error('\n❌ 配置过程出错:', error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
107
frontend/scripts/debug-contract-view-functions.js
Normal file
107
frontend/scripts/debug-contract-view-functions.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import { createPublicClient, http, getAddress, decodeErrorResult } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
|
||||
async function testWithDetails(name, abi, args) {
|
||||
console.log(`\n=== 测试 ${name} ===`)
|
||||
try {
|
||||
const result = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [abi],
|
||||
functionName: name,
|
||||
args: args
|
||||
})
|
||||
console.log('✓ 成功:', result)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.log('✗ 失败')
|
||||
console.log('错误类型:', error.name)
|
||||
console.log('错误消息:', error.message.split('\n')[0])
|
||||
|
||||
// 尝试解码错误
|
||||
if (error.data) {
|
||||
console.log('错误数据:', error.data)
|
||||
}
|
||||
|
||||
// 打印完整堆栈
|
||||
console.log('\n完整错误:')
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('诊断 Lending 合约 View 函数\n')
|
||||
console.log('合约:', LENDING_PROXY)
|
||||
console.log('用户:', USER)
|
||||
console.log('抵押品:', YT_A)
|
||||
|
||||
// 测试 1: borrowBalanceOf (已知能工作)
|
||||
await testWithDetails('borrowBalanceOf', {
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'borrowBalanceOf',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [USER])
|
||||
|
||||
// 测试 2: getUserAccountData
|
||||
await testWithDetails('getUserAccountData', {
|
||||
inputs: [{ name: '_user', type: 'address' }],
|
||||
name: 'getUserAccountData',
|
||||
outputs: [
|
||||
{ name: 'totalCollateralValue', type: 'uint256' },
|
||||
{ name: 'totalBorrowValue', type: 'uint256' },
|
||||
{ name: 'availableToBorrow', type: 'uint256' },
|
||||
{ name: 'healthFactor', type: 'uint256' }
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [USER])
|
||||
|
||||
// 测试 3: getUserCollateral
|
||||
await testWithDetails('getUserCollateral', {
|
||||
inputs: [
|
||||
{ name: '_user', type: 'address' },
|
||||
{ name: '_collateralAsset', type: 'address' }
|
||||
],
|
||||
name: 'getUserCollateral',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [USER, YT_A])
|
||||
|
||||
// 测试 4: getCollateralConfig (检查资产是否配置)
|
||||
await testWithDetails('getCollateralConfig', {
|
||||
inputs: [{ name: '_asset', type: 'address' }],
|
||||
name: 'getCollateralConfig',
|
||||
outputs: [
|
||||
{ name: 'isActive', type: 'bool' },
|
||||
{ name: 'decimals', type: 'uint8' },
|
||||
{ name: 'borrowCollateralFactor', type: 'uint64' },
|
||||
{ name: 'liquidateCollateralFactor', type: 'uint64' },
|
||||
{ name: 'liquidationFactor', type: 'uint64' },
|
||||
{ name: 'supplyCap', type: 'uint128' }
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [YT_A])
|
||||
|
||||
// 测试 5: paused (检查是否暂停)
|
||||
await testWithDetails('paused', {
|
||||
inputs: [],
|
||||
name: 'paused',
|
||||
outputs: [{ name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [])
|
||||
}
|
||||
|
||||
main()
|
||||
60
frontend/scripts/debug-events.js
Normal file
60
frontend/scripts/debug-events.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createPublicClient, http, getAddress, parseAbiItem } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
|
||||
async function main() {
|
||||
const latestBlock = await client.getBlockNumber()
|
||||
console.log('Latest Block:', latestBlock)
|
||||
const fromBlock = latestBlock - 10000n
|
||||
|
||||
console.log(`Checking events from block ${fromBlock} to ${latestBlock}..\n`)
|
||||
|
||||
// Check SupplyCollateral
|
||||
const supplyEvent = parseAbiItem('event SupplyCollateral(address indexed from, address indexed dst, address indexed asset, uint256 amount)')
|
||||
const supplyLogs = await client.getLogs({
|
||||
address: LENDING_PROXY,
|
||||
event: supplyEvent,
|
||||
fromBlock,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
console.log(`SupplyCollateral Events found: ${supplyLogs.length}`)
|
||||
|
||||
// Check Deposit
|
||||
const depositEvent = parseAbiItem('event Deposit(address indexed user, address indexed collateralAsset, uint256 amount)')
|
||||
const depositLogs = await client.getLogs({
|
||||
address: LENDING_PROXY,
|
||||
event: depositEvent,
|
||||
fromBlock,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
console.log(`Deposit Events found: ${depositLogs.length}`)
|
||||
|
||||
// Check WithdrawCollateral
|
||||
const withdrawColEvent = parseAbiItem('event WithdrawCollateral(address indexed src, address indexed to, address indexed asset, uint256 amount)')
|
||||
const withdrawColLogs = await client.getLogs({
|
||||
address: LENDING_PROXY,
|
||||
event: withdrawColEvent,
|
||||
fromBlock,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
console.log(`WithdrawCollateral Events found: ${withdrawColLogs.length}`)
|
||||
|
||||
// Check Withdraw
|
||||
const withdrawEvent = parseAbiItem('event Withdraw(address indexed user, address indexed collateralAsset, uint256 amount)')
|
||||
const withdrawLogs = await client.getLogs({
|
||||
address: LENDING_PROXY,
|
||||
event: withdrawEvent,
|
||||
fromBlock,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
console.log(`Withdraw Events (custom) found: ${withdrawLogs.length}`)
|
||||
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
82
frontend/scripts/debug-lending-functions.js
Normal file
82
frontend/scripts/debug-lending-functions.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { createPublicClient, http, getAddress, parseAbi, encodeFunctionData } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
// A random user address or the deployer to test view functions
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
|
||||
async function main() {
|
||||
console.log('Checking Lending Proxy View Functions...')
|
||||
|
||||
// Check borrowBalanceOf
|
||||
try {
|
||||
const data = encodeFunctionData({
|
||||
abi: parseAbi(['function borrowBalanceOf(address) view returns (uint256)']),
|
||||
functionName: 'borrowBalanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
const result = await client.call({
|
||||
to: LENDING_PROXY,
|
||||
data
|
||||
})
|
||||
console.log(`✅ borrowBalanceOf exists. Result: ${result.data}`)
|
||||
} catch (e) {
|
||||
console.log(`❌ borrowBalanceOf failed: ${e.message.slice(0, 100)}...`)
|
||||
}
|
||||
|
||||
// Check getBorrowBalance
|
||||
try {
|
||||
const data = encodeFunctionData({
|
||||
abi: parseAbi(['function getBorrowBalance(address) view returns (uint256)']),
|
||||
functionName: 'getBorrowBalance',
|
||||
args: [USER]
|
||||
})
|
||||
const result = await client.call({
|
||||
to: LENDING_PROXY,
|
||||
data
|
||||
})
|
||||
console.log(`✅ getBorrowBalance exists. Result: ${result.data}`)
|
||||
} catch (e) {
|
||||
console.log(`❌ getBorrowBalance failed: ${e.message.slice(0, 100)}...`)
|
||||
}
|
||||
|
||||
// Check getTotalSupply
|
||||
try {
|
||||
const data = encodeFunctionData({
|
||||
abi: parseAbi(['function getTotalSupply() view returns (uint256)']),
|
||||
functionName: 'getTotalSupply',
|
||||
args: []
|
||||
})
|
||||
const result = await client.call({
|
||||
to: LENDING_PROXY,
|
||||
data
|
||||
})
|
||||
console.log(`✅ getTotalSupply exists. Result: ${result.data}`)
|
||||
} catch (e) {
|
||||
console.log(`❌ getTotalSupply failed: ${e.message.slice(0, 100)}...`)
|
||||
}
|
||||
|
||||
// Check getTotalLiquidity
|
||||
try {
|
||||
const data = encodeFunctionData({
|
||||
abi: parseAbi(['function getTotalLiquidity() view returns (uint256)']),
|
||||
functionName: 'getTotalLiquidity',
|
||||
args: []
|
||||
})
|
||||
const result = await client.call({
|
||||
to: LENDING_PROXY,
|
||||
data
|
||||
})
|
||||
console.log(`✅ getTotalLiquidity exists. Result: ${result.data}`)
|
||||
} catch (e) {
|
||||
console.log(`❌ getTotalLiquidity failed: ${e.message.slice(0, 100)}...`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
main()
|
||||
33
frontend/scripts/decode-tx-data.js
Normal file
33
frontend/scripts/decode-tx-data.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { decodeAbiParameters, parseAbiParameters } from 'viem';
|
||||
|
||||
// 从错误日志中的 data 字段
|
||||
const txData = '0x5fcbde4700000000000000000000000097204190b35d9895a7a47aa7bac61ac08de3cf050000000000000000000000000000000000000000000000000000000000001d4c000000000000000000000000000000000000000000000000000000000000213400000000000000000000000000000000000000000000000000000000000003e8';
|
||||
|
||||
// 函数选择器 (前4字节)
|
||||
const selector = txData.slice(0, 10);
|
||||
console.log('函数选择器:', selector);
|
||||
|
||||
// 参数数据 (从第10个字符开始)
|
||||
const paramData = '0x' + txData.slice(10);
|
||||
|
||||
try {
|
||||
// 解码参数
|
||||
const decoded = decodeAbiParameters(
|
||||
parseAbiParameters('address, uint256, uint256, uint256'),
|
||||
paramData
|
||||
);
|
||||
|
||||
console.log('\n解码的参数:');
|
||||
console.log(' _asset (YT-A地址):', decoded[0]);
|
||||
console.log(' _collateralFactor:', decoded[1].toString(), '(即', Number(decoded[1]) / 100, '%)');
|
||||
console.log(' _liquidationThreshold:', decoded[2].toString(), '(即', Number(decoded[2]) / 100, '%)');
|
||||
console.log(' _liquidationBonus:', decoded[3].toString(), '(即', Number(decoded[3]) / 100, '%)');
|
||||
|
||||
console.log('\n✅ 参数解码成功,看起来都是正常的值');
|
||||
console.log('💡 问题可能在于:');
|
||||
console.log(' 1. 合约内部的require条件未满足');
|
||||
console.log(' 2. 可能需要先调用其他初始化函数');
|
||||
console.log(' 3. 可能Lending合约需要先设置到Configurator中');
|
||||
} catch (error) {
|
||||
console.error('解码失败:', error);
|
||||
}
|
||||
107
frontend/scripts/diagnose-configurator.js
Normal file
107
frontend/scripts/diagnose-configurator.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import { createPublicClient, http, parseAbi } from 'viem';
|
||||
import { arbitrumSepolia } from 'viem/chains';
|
||||
|
||||
// 合约地址
|
||||
const CONFIGURATOR_ADDRESS = '0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc';
|
||||
const YT_A_ADDRESS = '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05';
|
||||
const USER_ADDRESS = '0xa013422A5918CD099C63c8CC35283EACa99a705d';
|
||||
|
||||
const CONFIGURATOR_ABI = parseAbi([
|
||||
'function owner() view returns (address)',
|
||||
'function collateralConfigs(address) view returns (bool isActive, uint256 collateralFactor, uint256 liquidationThreshold, uint256 liquidationBonus)',
|
||||
'function setCollateralConfig(address _asset, uint256 _collateralFactor, uint256 _liquidationThreshold, uint256 _liquidationBonus)',
|
||||
'function setCollateralActive(address _asset, bool _isActive)'
|
||||
]);
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc')
|
||||
});
|
||||
|
||||
async function diagnose() {
|
||||
console.log('🔍 Configurator 诊断工具\n');
|
||||
|
||||
try {
|
||||
// 1. 检查 owner
|
||||
console.log('1️⃣ 检查合约 Owner:');
|
||||
const owner = await publicClient.readContract({
|
||||
address: CONFIGURATOR_ADDRESS,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'owner'
|
||||
});
|
||||
console.log(` Owner: ${owner}`);
|
||||
console.log(` 你的地址: ${USER_ADDRESS}`);
|
||||
const isOwner = owner.toLowerCase() === USER_ADDRESS.toLowerCase();
|
||||
console.log(` 匹配: ${isOwner ? '✅' : '❌'}\n`);
|
||||
|
||||
if (!isOwner) {
|
||||
console.log(' ⚠️ 你不是 owner!这就是交易失败的原因。\n');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 检查当前配置
|
||||
console.log('2️⃣ 检查 YT-A 当前配置:');
|
||||
try {
|
||||
const config = await publicClient.readContract({
|
||||
address: CONFIGURATOR_ADDRESS,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'collateralConfigs',
|
||||
args: [YT_A_ADDRESS]
|
||||
});
|
||||
console.log(` isActive: ${config[0]}`);
|
||||
console.log(` collateralFactor: ${config[1]}`);
|
||||
console.log(` liquidationThreshold: ${config[2]}`);
|
||||
console.log(` liquidationBonus: ${config[3]}\n`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ 读取失败: ${error.message}\n`);
|
||||
}
|
||||
|
||||
// 3. 尝试模拟调用 setCollateralConfig
|
||||
console.log('3️⃣ 模拟调用 setCollateralConfig(YT-A, 7500, 8500, 1000):');
|
||||
try {
|
||||
const result = await publicClient.simulateContract({
|
||||
address: CONFIGURATOR_ADDRESS,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'setCollateralConfig',
|
||||
args: [YT_A_ADDRESS, 7500n, 8500n, 1000n],
|
||||
account: USER_ADDRESS
|
||||
});
|
||||
console.log(' ✅ 模拟成功!交易应该可以执行\n');
|
||||
console.log(' 模拟结果:', result);
|
||||
} catch (error) {
|
||||
console.log(' ❌ 模拟失败!');
|
||||
console.log(` 错误类型: ${error.name}`);
|
||||
console.log(` 错误信息: ${error.shortMessage || error.message}`);
|
||||
if (error.cause) {
|
||||
console.log(` 底层原因: ${JSON.stringify(error.cause, null, 2)}`);
|
||||
}
|
||||
if (error.details) {
|
||||
console.log(` 详情: ${error.details}`);
|
||||
}
|
||||
if (error.metaMessages) {
|
||||
console.log(` 元信息: ${error.metaMessages.join(', ')}`);
|
||||
}
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
// 4. 检查合约代码
|
||||
console.log('4️⃣ 检查合约代码:');
|
||||
const bytecode = await publicClient.getBytecode({ address: CONFIGURATOR_ADDRESS });
|
||||
console.log(` 代码大小: ${bytecode ? bytecode.length : 0} bytes`);
|
||||
console.log(` 合约已部署: ${bytecode && bytecode.length > 2 ? '✅' : '❌'}\n`);
|
||||
|
||||
// 5. 建议
|
||||
console.log('💡 诊断建议:');
|
||||
console.log(' 1. 检查合约是否有访问控制(如 Ownable, AccessControl)');
|
||||
console.log(' 2. 参数可能有范围限制(如 collateralFactor 必须 <= 10000)');
|
||||
console.log(' 3. 可能需要先调用其他初始化函数');
|
||||
console.log(' 4. 查看合约源码了解具体的 require 条件');
|
||||
console.log('\n📊 在 Arbiscan 查看合约:');
|
||||
console.log(` https://sepolia.arbiscan.io/address/${CONFIGURATOR_ADDRESS}#code`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 诊断过程出错:', error);
|
||||
}
|
||||
}
|
||||
|
||||
diagnose();
|
||||
130
frontend/scripts/find-missing-tokens.js
Normal file
130
frontend/scripts/find-missing-tokens.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
|
||||
const TRANSFER_EVENT = {
|
||||
type: 'event',
|
||||
name: 'Transfer',
|
||||
inputs: [
|
||||
{ indexed: true, name: 'from', type: 'address' },
|
||||
{ indexed: true, name: 'to', type: 'address' },
|
||||
{ indexed: false, name: 'value', type: 'uint256' }
|
||||
]
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n追踪丢失的 YT-A 代币...\n')
|
||||
console.log('你的地址:', USER)
|
||||
console.log('当前余额: 10 YT-A')
|
||||
console.log('之前余额: 400 YT-A')
|
||||
console.log('丢失数量: 390 YT-A\n')
|
||||
|
||||
const latestBlock = await client.getBlockNumber()
|
||||
console.log('最新区块:', latestBlock)
|
||||
console.log('查询范围: 最近 10000 个区块\n')
|
||||
|
||||
try {
|
||||
// 查找所有从用户地址发出的转账
|
||||
console.log('=== 查找所有转出记录 ===\n')
|
||||
const logsOut = await client.getLogs({
|
||||
address: YT_A,
|
||||
event: TRANSFER_EVENT,
|
||||
args: { from: USER },
|
||||
fromBlock: latestBlock - 10000n,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
|
||||
if (logsOut.length === 0) {
|
||||
console.log('✗ 未找到任何转出记录(最近 10000 个区块)\n')
|
||||
} else {
|
||||
console.log(`✓ 找到 ${logsOut.length} 笔转出记录:\n`)
|
||||
|
||||
let totalOut = 0n
|
||||
logsOut.forEach((log, i) => {
|
||||
const { from, to, value } = log.args
|
||||
const isToLending = to.toLowerCase() === LENDING_PROXY.toLowerCase()
|
||||
totalOut += value
|
||||
|
||||
console.log(`${i + 1}. 区块 ${log.blockNumber}`)
|
||||
console.log(` 交易: ${log.transactionHash}`)
|
||||
console.log(` To: ${to} ${isToLending ? '<- Lending 合约' : ''}`)
|
||||
console.log(` 数量: ${Number(value) / 1e18} YT-A`)
|
||||
console.log()
|
||||
})
|
||||
|
||||
console.log(`总转出: ${Number(totalOut) / 1e18} YT-A\n`)
|
||||
}
|
||||
|
||||
// 查找所有转入用户地址的转账
|
||||
console.log('=== 查找所有转入记录 ===\n')
|
||||
const logsIn = await client.getLogs({
|
||||
address: YT_A,
|
||||
event: TRANSFER_EVENT,
|
||||
args: { to: USER },
|
||||
fromBlock: latestBlock - 10000n,
|
||||
toBlock: latestBlock
|
||||
})
|
||||
|
||||
if (logsIn.length === 0) {
|
||||
console.log('✗ 未找到任何转入记录\n')
|
||||
} else {
|
||||
console.log(`✓ 找到 ${logsIn.length} 笔转入记录:\n`)
|
||||
|
||||
let totalIn = 0n
|
||||
logsIn.forEach((log, i) => {
|
||||
const { from, to, value } = log.args
|
||||
totalIn += value
|
||||
|
||||
console.log(`${i + 1}. 区块 ${log.blockNumber}`)
|
||||
console.log(` 交易: ${log.transactionHash}`)
|
||||
console.log(` From: ${from}`)
|
||||
console.log(` 数量: ${Number(value) / 1e18} YT-A`)
|
||||
console.log()
|
||||
})
|
||||
|
||||
console.log(`总转入: ${Number(totalIn) / 1e18} YT-A\n`)
|
||||
}
|
||||
|
||||
// 计算净流出
|
||||
if (logsOut.length > 0 || logsIn.length > 0) {
|
||||
const totalOut = logsOut.reduce((sum, log) => sum + log.args.value, 0n)
|
||||
const totalIn = logsIn.reduce((sum, log) => sum + log.args.value, 0n)
|
||||
const netFlow = Number(totalIn - totalOut) / 1e18
|
||||
|
||||
console.log('=== 汇总 ===')
|
||||
console.log(`总转入: ${Number(totalIn) / 1e18} YT-A`)
|
||||
console.log(`总转出: ${Number(totalOut) / 1e18} YT-A`)
|
||||
console.log(`净变化: ${netFlow > 0 ? '+' : ''}${netFlow} YT-A`)
|
||||
console.log(`当前余额: 10 YT-A\n`)
|
||||
|
||||
// 检查代币是否在 Lending 合约中
|
||||
console.log('=== 检查 Lending 合约余额 ===')
|
||||
const lendingBalance = await client.readContract({
|
||||
address: YT_A,
|
||||
abi: [{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'balanceOf',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'balanceOf',
|
||||
args: [LENDING_PROXY]
|
||||
})
|
||||
console.log(`Lending 合约持有的 YT-A: ${Number(lendingBalance) / 1e18}\n`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
41
frontend/scripts/get-proxy-implementation.js
Normal file
41
frontend/scripts/get-proxy-implementation.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createPublicClient, http } from 'viem';
|
||||
import { arbitrumSepolia } from 'viem/chains';
|
||||
|
||||
const CONFIGURATOR_ADDRESS = '0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc';
|
||||
|
||||
// ERC-1967 标准存储槽
|
||||
const IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc';
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc')
|
||||
});
|
||||
|
||||
async function getImplementation() {
|
||||
console.log('🔍 查找代理合约的实现地址\n');
|
||||
|
||||
try {
|
||||
// 读取 ERC-1967 实现槽
|
||||
const slot = await publicClient.getStorageAt({
|
||||
address: CONFIGURATOR_ADDRESS,
|
||||
slot: IMPLEMENTATION_SLOT
|
||||
});
|
||||
|
||||
if (slot && slot !== '0x' + '0'.repeat(64)) {
|
||||
// 从存储槽中提取地址(去掉前面的0)
|
||||
const implementationAddress = '0x' + slot.slice(-40);
|
||||
console.log('✅ 找到实现合约:');
|
||||
console.log(` 代理合约 (Configurator): ${CONFIGURATOR_ADDRESS}`);
|
||||
console.log(` 实现合约 (Implementation): ${implementationAddress}`);
|
||||
console.log(`\n📊 查看实现合约源码:`);
|
||||
console.log(` https://sepolia.arbiscan.io/address/${implementationAddress}#code`);
|
||||
console.log(`\n💡 提示: 需要使用实现合约的 ABI,但调用代理合约的地址`);
|
||||
} else {
|
||||
console.log('❌ 未找到实现地址');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 错误:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
getImplementation();
|
||||
60
frontend/scripts/init-lending-config.js
Normal file
60
frontend/scripts/init-lending-config.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 初始化 Lending 合约基础配置
|
||||
*
|
||||
* ⚠️ 需要使用 Configurator Owner 钱包执行
|
||||
* Owner: 0xa013422A5918CD099C63c8CC35283EACa99a705d
|
||||
*
|
||||
* 使用方法:
|
||||
* 1. 确保钱包连接到 Arbitrum Sepolia
|
||||
* 2. 在管理员面板中手动调用 Configurator 的配置函数
|
||||
*/
|
||||
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ Lending 合约初始化配置说明 ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
|
||||
❌ 问题: Lending 合约基础配置未初始化
|
||||
- USDC 地址: 0x0000...0000 ❌
|
||||
- 价格源地址: 0x0000...0000 ❌
|
||||
|
||||
✅ 解决方案: 需要调用 Configurator 初始化函数
|
||||
|
||||
📝 合约信息:
|
||||
- Configurator: 0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc
|
||||
- Lending Proxy: 0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D
|
||||
- Owner: 0xa013422A5918CD099C63c8CC35283EACa99a705d
|
||||
|
||||
🔧 需要设置的参数:
|
||||
1. baseToken (USDC): 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d
|
||||
2. lendingPriceSource: 0xE82c7cB9CfA42D6eb7e443956b78f8290249c316
|
||||
3. 利率参数 (borrowKink, supplyKink 等)
|
||||
4. baseBorrowMin, targetReserves 等
|
||||
|
||||
⚠️ 这需要合约开发者或管理员操作!
|
||||
|
||||
💡 可能的原因:
|
||||
1. 合约刚部署,还没有初始化
|
||||
2. 初始化函数调用失败
|
||||
3. 配置被重置了
|
||||
|
||||
📞 建议:
|
||||
联系合约部署者 (0xa013422A5918CD099C63c8CC35283EACa99a705d)
|
||||
使用 Configurator 合约的初始化函数设置基础配置
|
||||
`)
|
||||
|
||||
// 显示需要调用的函数签名
|
||||
console.log(`
|
||||
🔍 需要调用的 Configurator 函数示例:
|
||||
|
||||
function setConfiguration(
|
||||
address lendingProxy,
|
||||
Configuration memory configuration
|
||||
) external onlyOwner
|
||||
|
||||
其中 Configuration 结构包括:
|
||||
- baseToken: 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d (USDC)
|
||||
- lendingPriceSource: 0xE82c7cB9CfA42D6eb7e443956b78f8290249c316
|
||||
- 利率参数(根据文档配置)
|
||||
- assetConfigs: 已经配置了 YT-A ✅
|
||||
`)
|
||||
134
frontend/scripts/simulate-deposit.js
Normal file
134
frontend/scripts/simulate-deposit.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import { createPublicClient, http, encodeFunctionData, formatUnits } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http()
|
||||
})
|
||||
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D'
|
||||
const YT_A = '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05'
|
||||
const USER = '0xa013422a5918cd099c63c8cc35283eaca99a705d'
|
||||
const CONFIGURATOR = '0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc'
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: '_collateralAsset', type: 'address' },
|
||||
{ internalType: 'uint256', name: '_amount', type: 'uint256' }
|
||||
],
|
||||
name: 'deposit',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
const CONFIGURATOR_ABI = [
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: '_asset', type: 'address' }],
|
||||
name: 'getCollateralConfig',
|
||||
outputs: [
|
||||
{ internalType: 'bool', name: 'isActive', type: 'bool' },
|
||||
{ internalType: 'uint256', name: 'collateralFactor', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: 'liquidationThreshold', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: 'liquidationBonus', type: 'uint256' }
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
name: 'collateralConfigs',
|
||||
outputs: [
|
||||
{ internalType: 'bool', name: 'isActive', type: 'bool' },
|
||||
{ internalType: 'uint256', name: 'collateralFactor', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: 'liquidationThreshold', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: 'liquidationBonus', type: 'uint256' }
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function simulateDeposit() {
|
||||
console.log('🔍 模拟存款操作...\n')
|
||||
console.log('合约地址:', LENDING_PROXY)
|
||||
console.log('抵押品地址:', YT_A)
|
||||
console.log('用户地址:', USER)
|
||||
console.log('存款金额: 10 YT-A\n')
|
||||
|
||||
// 1. 检查 Configurator 中的配置
|
||||
console.log('=== 检查 Configurator 配置 ===')
|
||||
try {
|
||||
const config = await client.readContract({
|
||||
address: CONFIGURATOR,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'getCollateralConfig',
|
||||
args: [YT_A]
|
||||
})
|
||||
console.log('✅ 通过 getCollateralConfig 读取:')
|
||||
console.log(' - 是否激活:', config[0])
|
||||
console.log(' - 抵押率:', Number(config[1]) / 100 + '%')
|
||||
console.log(' - 清算阈值:', Number(config[2]) / 100 + '%')
|
||||
console.log(' - 清算奖励:', Number(config[3]) / 100 + '%')
|
||||
} catch (error) {
|
||||
console.log('❌ getCollateralConfig 失败:', error.message.split('\n')[0])
|
||||
|
||||
// 尝试直接读取mapping
|
||||
try {
|
||||
const config = await client.readContract({
|
||||
address: CONFIGURATOR,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'collateralConfigs',
|
||||
args: [YT_A]
|
||||
})
|
||||
console.log('✅ 通过 collateralConfigs mapping 读取:')
|
||||
console.log(' - 是否激活:', config[0])
|
||||
console.log(' - 抵押率:', Number(config[1]) / 100 + '%')
|
||||
console.log(' - 清算阈值:', Number(config[2]) / 100 + '%')
|
||||
console.log(' - 清算奖励:', Number(config[3]) / 100 + '%')
|
||||
|
||||
if (!config[0]) {
|
||||
console.log('\n⚠️ 警告: 抵押品未激活!这就是存款失败的原因。')
|
||||
}
|
||||
} catch (error2) {
|
||||
console.log('❌ collateralConfigs mapping 也失败:', error2.message.split('\n')[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 模拟调用 deposit
|
||||
console.log('\n=== 模拟存款调用 ===')
|
||||
const depositAmount = 10n * 10n ** 18n // 10 YT-A
|
||||
|
||||
try {
|
||||
await client.simulateContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'deposit',
|
||||
args: [YT_A, depositAmount],
|
||||
account: USER
|
||||
})
|
||||
console.log('✅ 模拟存款成功!交易应该可以执行。')
|
||||
} catch (error) {
|
||||
console.log('❌ 模拟存款失败')
|
||||
console.log('\n详细错误信息:')
|
||||
console.log(error.message)
|
||||
|
||||
// 尝试解析错误原因
|
||||
if (error.message.includes('Collateral not active')) {
|
||||
console.log('\n💡 原因: 抵押品未激活')
|
||||
console.log(' 需要管理员通过 Configurator 激活此抵押品')
|
||||
} else if (error.message.includes('insufficient')) {
|
||||
console.log('\n💡 原因: 余额或授权不足')
|
||||
} else if (error.message.includes('paused')) {
|
||||
console.log('\n💡 原因: 合约已暂停')
|
||||
} else {
|
||||
console.log('\n💡 这可能是由于抵押品未在借贷合约中配置')
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== 诊断完成 ===\n')
|
||||
}
|
||||
|
||||
simulateDeposit().catch(console.error)
|
||||
117
frontend/scripts/simulate-supply.js
Normal file
117
frontend/scripts/simulate-supply.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import { createPublicClient, http, parseEther } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D'
|
||||
const YT_A = '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05'
|
||||
const USDC = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d'
|
||||
const USER = '0xa013422A5918CD099C63c8CC35283EACa99a705d'
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'asset', type: 'address' },
|
||||
{ internalType: 'uint256', name: 'amount', type: 'uint256' }
|
||||
],
|
||||
name: 'supplyCollateral',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'getAssetInfo',
|
||||
outputs: [
|
||||
{ internalType: 'uint8', name: 'offset', type: 'uint8' },
|
||||
{ internalType: 'address', name: 'priceFeed', type: 'address' },
|
||||
{ internalType: 'uint64', name: 'scale', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowCollateralFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'liquidateCollateralFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'liquidationFactor', type: 'uint64' },
|
||||
{ internalType: 'uint128', name: 'supplyCap', type: 'uint128' }
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'baseToken',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n模拟 supplyCollateral 调用...\n')
|
||||
|
||||
try {
|
||||
// 先检查 Lending 合约能否读取到配置
|
||||
console.log('=== 检查 Lending 合约状态 ===')
|
||||
|
||||
const baseToken = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'baseToken'
|
||||
})
|
||||
|
||||
console.log('Lending.baseToken:', baseToken)
|
||||
console.log('是否为 USDC:', baseToken.toLowerCase() === USDC.toLowerCase() ? '✓' : '✗')
|
||||
|
||||
// 检查资产信息
|
||||
try {
|
||||
const assetInfo = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getAssetInfo',
|
||||
args: [YT_A]
|
||||
})
|
||||
|
||||
console.log('\nLending.getAssetInfo(YT-A):')
|
||||
console.log(' borrowCollateralFactor:', assetInfo[3].toString())
|
||||
console.log(' liquidateCollateralFactor:', assetInfo[4].toString())
|
||||
console.log(' supplyCap:', assetInfo[6].toString())
|
||||
} catch (e) {
|
||||
console.log('\n✗ getAssetInfo 失败:', e.shortMessage || e.message)
|
||||
}
|
||||
|
||||
// 尝试模拟调用
|
||||
console.log('\n=== 模拟 supplyCollateral 调用 ===')
|
||||
const amount = parseEther('10')
|
||||
|
||||
try {
|
||||
await client.simulateContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'supplyCollateral',
|
||||
args: [YT_A, amount],
|
||||
account: USER
|
||||
})
|
||||
console.log('✓ 模拟成功!理论上应该可以执行')
|
||||
} catch (error) {
|
||||
console.log('✗ 模拟失败:')
|
||||
console.log(' 错误类型:', error.name)
|
||||
console.log(' 错误信息:', error.shortMessage || error.message)
|
||||
|
||||
if (error.cause) {
|
||||
console.log('\n详细错误:')
|
||||
console.log(' ', error.cause.message || error.cause)
|
||||
}
|
||||
|
||||
if (error.data) {
|
||||
console.log('\n错误数据:', error.data)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n意外错误:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
118
frontend/scripts/single-buy-test.ts
Normal file
118
frontend/scripts/single-buy-test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 单账户买入测试脚本
|
||||
* 使用主账户直接执行买入,不需要分发 ETH
|
||||
*/
|
||||
|
||||
import {
|
||||
createPublicClient,
|
||||
createWalletClient,
|
||||
http,
|
||||
parseUnits,
|
||||
formatUnits,
|
||||
type Address,
|
||||
type Hex
|
||||
} from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
import { privateKeyToAccount } from 'viem/accounts'
|
||||
|
||||
const CONTRACTS = {
|
||||
WUSD: '0x6d2bf81a631dFE19B2f348aE92cF6Ef41ca2DF98' as Address,
|
||||
VAULT_YT_A: '0x0cA35994F033685E7a57ef9bc5d00dd3cf927330' as Address,
|
||||
}
|
||||
|
||||
const MAIN_PRIVATE_KEY = '0xa082a7037105ebd606bee80906687e400d89899bbb6ba0273a61528c2f5fab89' as Hex
|
||||
|
||||
const WUSD_ABI = [
|
||||
{ inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'approve', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
|
||||
{ inputs: [{ name: 'account', type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
] as const
|
||||
|
||||
const VAULT_ABI = [
|
||||
{ inputs: [{ name: '_wusdAmount', type: 'uint256' }], name: 'depositYT', outputs: [], stateMutability: 'nonpayable', type: 'function' },
|
||||
{ inputs: [{ name: '_wusdAmount', type: 'uint256' }], name: 'previewBuy', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [{ name: 'account', type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [], name: 'symbol', outputs: [{ type: 'string' }], stateMutability: 'view', type: 'function' },
|
||||
] as const
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 单账户买入测试\n')
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc'),
|
||||
})
|
||||
|
||||
const account = privateKeyToAccount(MAIN_PRIVATE_KEY)
|
||||
const walletClient = createWalletClient({
|
||||
account,
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://sepolia-rollup.arbitrum.io/rpc'),
|
||||
})
|
||||
|
||||
console.log(`📍 账户: ${account.address}`)
|
||||
|
||||
// 检查余额
|
||||
const ethBalance = await publicClient.getBalance({ address: account.address })
|
||||
const wusdBalance = await publicClient.readContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [account.address],
|
||||
})
|
||||
|
||||
console.log(`⛽ ETH 余额: ${formatUnits(ethBalance, 18)} ETH`)
|
||||
console.log(`💰 WUSD 余额: ${formatUnits(wusdBalance, 18)} WUSD`)
|
||||
|
||||
// 获取金库 symbol
|
||||
const vaultSymbol = await publicClient.readContract({
|
||||
address: CONTRACTS.VAULT_YT_A,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'symbol',
|
||||
})
|
||||
console.log(`🏦 目标金库: ${vaultSymbol}`)
|
||||
|
||||
// 买入金额
|
||||
const buyAmount = parseUnits('50', 18) // 50 WUSD
|
||||
console.log(`\n📝 买入金额: ${formatUnits(buyAmount, 18)} WUSD`)
|
||||
|
||||
// 预览
|
||||
const previewYT = await publicClient.readContract({
|
||||
address: CONTRACTS.VAULT_YT_A,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'previewBuy',
|
||||
args: [buyAmount],
|
||||
})
|
||||
console.log(`📊 预计获得: ${formatUnits(previewYT, 18)} ${vaultSymbol}`)
|
||||
|
||||
// 执行买入
|
||||
console.log('\n1️⃣ 授权 WUSD...')
|
||||
const approveHash = await walletClient.writeContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'approve',
|
||||
args: [CONTRACTS.VAULT_YT_A, buyAmount],
|
||||
})
|
||||
await publicClient.waitForTransactionReceipt({ hash: approveHash })
|
||||
console.log(` ✓ 授权成功: ${approveHash}`)
|
||||
|
||||
console.log('\n2️⃣ 执行买入...')
|
||||
const buyHash = await walletClient.writeContract({
|
||||
address: CONTRACTS.VAULT_YT_A,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'depositYT',
|
||||
args: [buyAmount],
|
||||
})
|
||||
await publicClient.waitForTransactionReceipt({ hash: buyHash })
|
||||
console.log(` ✓ 买入成功: ${buyHash}`)
|
||||
|
||||
// 检查结果
|
||||
const ytBalance = await publicClient.readContract({
|
||||
address: CONTRACTS.VAULT_YT_A,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [account.address],
|
||||
})
|
||||
console.log(`\n✅ ${vaultSymbol} 余额: ${formatUnits(ytBalance, 18)}`)
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
182
frontend/scripts/test-calculations.js
Normal file
182
frontend/scripts/test-calculations.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const LENDING_PRICE_FEED = getAddress('0xE82c7cB9CfA42D6eb7e443956b78f8290249c316')
|
||||
const CONFIGURATOR = getAddress('0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'account', type: 'address' },
|
||||
{ internalType: 'address', name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'getCollateral',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
||||
name: 'borrowBalanceOf',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
const PRICE_FEED_ABI = [
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'asset', type: 'address' }],
|
||||
name: 'getPrice',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
const CONFIGURATOR_ABI = [
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'lendingProxy', type: 'address' }],
|
||||
name: 'getConfiguration',
|
||||
outputs: [
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'address', name: 'baseToken', type: 'address' },
|
||||
{ internalType: 'address', name: 'lendingPriceSource', type: 'address' },
|
||||
{ internalType: 'uint64', name: 'supplyKink', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'supplyPerYearInterestRateSlopeLow', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'supplyPerYearInterestRateSlopeHigh', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'supplyPerYearInterestRateBase', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowKink', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowPerYearInterestRateSlopeLow', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowPerYearInterestRateSlopeHigh', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowPerYearInterestRateBase', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'storeFrontPriceFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'trackingIndexScale', type: 'uint64' },
|
||||
{ internalType: 'uint104', name: 'baseBorrowMin', type: 'uint104' },
|
||||
{ internalType: 'uint104', name: 'targetReserves', type: 'uint104' },
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'address', name: 'asset', type: 'address' },
|
||||
{ internalType: 'uint8', name: 'decimals', type: 'uint8' },
|
||||
{ internalType: 'uint64', name: 'borrowCollateralFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'liquidateCollateralFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'liquidationFactor', type: 'uint64' },
|
||||
{ internalType: 'uint128', name: 'supplyCap', type: 'uint128' }
|
||||
],
|
||||
internalType: 'struct LendingConfiguration.AssetConfig[]',
|
||||
name: 'assetConfigs',
|
||||
type: 'tuple[]'
|
||||
}
|
||||
],
|
||||
internalType: 'struct LendingConfiguration.Configuration',
|
||||
name: '',
|
||||
type: 'tuple'
|
||||
}
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n=== 测试抵押价值计算逻辑 ===\n')
|
||||
console.log('用户:', USER)
|
||||
|
||||
// 1. 获取抵押品余额
|
||||
console.log('\n--- 1. 获取抵押品余额 ---')
|
||||
const collateral = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getCollateral',
|
||||
args: [USER, YT_A]
|
||||
})
|
||||
console.log('YT-A 抵押品:', Number(collateral) / 1e18, 'YT-A')
|
||||
console.log('原始值:', collateral.toString())
|
||||
|
||||
// 2. 获取价格
|
||||
console.log('\n--- 2. 获取 YT-A 价格 ---')
|
||||
const price = await client.readContract({
|
||||
address: LENDING_PRICE_FEED,
|
||||
abi: PRICE_FEED_ABI,
|
||||
functionName: 'getPrice',
|
||||
args: [YT_A]
|
||||
})
|
||||
console.log('YT-A 价格(Compound V3 格式):', price.toString())
|
||||
console.log(' = 1e30 规模')
|
||||
|
||||
// 3. 获取配置
|
||||
console.log('\n--- 3. 获取配置 ---')
|
||||
const config = await client.readContract({
|
||||
address: CONFIGURATOR,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'getConfiguration',
|
||||
args: [LENDING_PROXY]
|
||||
})
|
||||
|
||||
const ytAConfig = config.assetConfigs.find(
|
||||
cfg => cfg.asset.toLowerCase() === YT_A.toLowerCase()
|
||||
)
|
||||
|
||||
if (ytAConfig) {
|
||||
console.log('YT-A 配置:')
|
||||
console.log(' 借款抵押率 (borrowCollateralFactor):', ytAConfig.borrowCollateralFactor.toString())
|
||||
console.log(' = ', Number(ytAConfig.borrowCollateralFactor) / 1e18 * 100, '%')
|
||||
console.log(' 清算阈值 (liquidateCollateralFactor):', ytAConfig.liquidateCollateralFactor.toString())
|
||||
console.log(' = ', Number(ytAConfig.liquidateCollateralFactor) / 1e18 * 100, '%')
|
||||
}
|
||||
|
||||
// 4. 获取借款余额
|
||||
console.log('\n--- 4. 获取借款余额 ---')
|
||||
const borrowBalance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'borrowBalanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
console.log('借款余额:', Number(borrowBalance) / 1e6, 'USDC')
|
||||
|
||||
// 5. 计算总抵押价值
|
||||
console.log('\n--- 5. 计算总抵押价值 ---')
|
||||
// 价值(USDC 6位精度)= (balance * price) / 1e42
|
||||
const valueInUSD = (collateral * price) / BigInt(10 ** 42)
|
||||
console.log('总抵押价值:', Number(valueInUSD) / 1e6, 'USDC')
|
||||
console.log('原始值 (6位精度):', valueInUSD.toString())
|
||||
|
||||
// 6. 计算可借额度
|
||||
console.log('\n--- 6. 计算可借额度 ---')
|
||||
if (ytAConfig) {
|
||||
const borrowCollateralFactor = BigInt(ytAConfig.borrowCollateralFactor)
|
||||
const maxBorrow = (valueInUSD * borrowCollateralFactor) / BigInt(10 ** 18)
|
||||
const availableToBorrow = maxBorrow > borrowBalance ? maxBorrow - borrowBalance : 0n
|
||||
console.log('最大可借:', Number(maxBorrow) / 1e6, 'USDC')
|
||||
console.log('已借:', Number(borrowBalance) / 1e6, 'USDC')
|
||||
console.log('剩余可借:', Number(availableToBorrow) / 1e6, 'USDC')
|
||||
}
|
||||
|
||||
// 7. 计算健康因子
|
||||
console.log('\n--- 7. 计算健康因子 ---')
|
||||
if (borrowBalance > 0n && ytAConfig) {
|
||||
const liquidationFactor = BigInt(ytAConfig.liquidateCollateralFactor)
|
||||
const liquidationThreshold = (valueInUSD * liquidationFactor) / BigInt(10 ** 18)
|
||||
// healthFactor 以 10000 为基数
|
||||
const healthFactor = (liquidationThreshold * BigInt(10000)) / borrowBalance
|
||||
console.log('清算阈值价值:', Number(liquidationThreshold) / 1e6, 'USDC')
|
||||
console.log('健康因子 (10000=100%):', Number(healthFactor))
|
||||
console.log(' = ', Number(healthFactor) / 100, '%')
|
||||
} else if (borrowBalance === 0n) {
|
||||
console.log('无借款,健康因子: ∞(无限大,非常安全)')
|
||||
}
|
||||
|
||||
console.log('\n=== 计算完成 ===\n')
|
||||
}
|
||||
|
||||
main()
|
||||
111
frontend/scripts/test-collateral-functions.js
Normal file
111
frontend/scripts/test-collateral-functions.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
|
||||
async function testFunction(name, abi, args) {
|
||||
try {
|
||||
const result = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [abi],
|
||||
functionName: name,
|
||||
args: args
|
||||
})
|
||||
console.log(`✓ ${name} 成功:`, result)
|
||||
return { success: true, result }
|
||||
} catch (error) {
|
||||
console.log(`✗ ${name} 失败:`, error.message.split('\n')[0])
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n测试不同的抵押品查询函数...\n')
|
||||
console.log('合约:', LENDING_PROXY)
|
||||
console.log('用户:', USER)
|
||||
console.log('抵押品:', YT_A, '\n')
|
||||
|
||||
// 测试 1: userCollateral (Compound V3 标准)
|
||||
console.log('--- 测试 1: userCollateral(address, address) ---')
|
||||
await testFunction('userCollateral', {
|
||||
inputs: [
|
||||
{ name: 'account', type: 'address' },
|
||||
{ name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'userCollateral',
|
||||
outputs: [
|
||||
{ name: 'balance', type: 'uint128' },
|
||||
{ name: '_reserved', type: 'uint128' }
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [USER, YT_A])
|
||||
|
||||
// 测试 2: getUserCollateral
|
||||
console.log('\n--- 测试 2: getUserCollateral(address, address) ---')
|
||||
await testFunction('getUserCollateral', {
|
||||
inputs: [
|
||||
{ name: '_user', type: 'address' },
|
||||
{ name: '_collateralAsset', type: 'address' }
|
||||
],
|
||||
name: 'getUserCollateral',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [USER, YT_A])
|
||||
|
||||
// 测试 3: collateralBalanceOf
|
||||
console.log('\n--- 测试 3: collateralBalanceOf(address, address) ---')
|
||||
await testFunction('collateralBalanceOf', {
|
||||
inputs: [
|
||||
{ name: 'account', type: 'address' },
|
||||
{ name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'collateralBalanceOf',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [USER, YT_A])
|
||||
|
||||
// 测试 4: getUserAccountData
|
||||
console.log('\n--- 测试 4: getUserAccountData(address) ---')
|
||||
const result = await testFunction('getUserAccountData', {
|
||||
inputs: [{ name: '_user', type: 'address' }],
|
||||
name: 'getUserAccountData',
|
||||
outputs: [
|
||||
{ name: 'totalCollateralValue', type: 'uint256' },
|
||||
{ name: 'totalBorrowValue', type: 'uint256' },
|
||||
{ name: 'availableToBorrow', type: 'uint256' },
|
||||
{ name: 'healthFactor', type: 'uint256' }
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [USER])
|
||||
|
||||
if (result.success) {
|
||||
console.log('\n详细结果:')
|
||||
console.log(' totalCollateralValue:', result.result[0].toString())
|
||||
console.log(' totalBorrowValue:', result.result[1].toString())
|
||||
console.log(' availableToBorrow:', result.result[2].toString())
|
||||
console.log(' healthFactor:', result.result[3].toString())
|
||||
}
|
||||
|
||||
// 测试 5: borrowBalanceOf (检查借款)
|
||||
console.log('\n--- 测试 5: borrowBalanceOf(address) ---')
|
||||
await testFunction('borrowBalanceOf', {
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'borrowBalanceOf',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [USER])
|
||||
}
|
||||
|
||||
main()
|
||||
94
frontend/scripts/test-config-read.js
Normal file
94
frontend/scripts/test-config-read.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { createPublicClient, http } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const CONFIGURATOR = '0x488409CE9A3Fcd8EbD373dCb7e025cF8AB96fcdc'
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D'
|
||||
const YT_A = '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05'
|
||||
|
||||
const CONFIGURATOR_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'lendingProxy', type: 'address' }
|
||||
],
|
||||
name: 'getConfiguration',
|
||||
outputs: [
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'address', name: 'baseToken', type: 'address' },
|
||||
{ internalType: 'address', name: 'lendingPriceSource', type: 'address' },
|
||||
{ internalType: 'uint64', name: 'supplyKink', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'supplyPerYearInterestRateSlopeLow', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'supplyPerYearInterestRateSlopeHigh', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'supplyPerYearInterestRateBase', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowKink', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowPerYearInterestRateSlopeLow', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowPerYearInterestRateSlopeHigh', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'borrowPerYearInterestRateBase', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'storeFrontPriceFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'trackingIndexScale', type: 'uint64' },
|
||||
{ internalType: 'uint104', name: 'baseBorrowMin', type: 'uint104' },
|
||||
{ internalType: 'uint104', name: 'targetReserves', type: 'uint104' },
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'address', name: 'asset', type: 'address' },
|
||||
{ internalType: 'uint8', name: 'decimals', type: 'uint8' },
|
||||
{ internalType: 'uint64', name: 'borrowCollateralFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'liquidateCollateralFactor', type: 'uint64' },
|
||||
{ internalType: 'uint64', name: 'liquidationFactor', type: 'uint64' },
|
||||
{ internalType: 'uint128', name: 'supplyCap', type: 'uint128' }
|
||||
],
|
||||
internalType: 'struct LendingConfiguration.AssetConfig[]',
|
||||
name: 'assetConfigs',
|
||||
type: 'tuple[]'
|
||||
}
|
||||
],
|
||||
internalType: 'struct LendingConfiguration.Configuration',
|
||||
name: '',
|
||||
type: 'tuple'
|
||||
}
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('🔍 测试从 Configurator 读取配置...\n')
|
||||
|
||||
try {
|
||||
const config = await client.readContract({
|
||||
address: CONFIGURATOR,
|
||||
abi: CONFIGURATOR_ABI,
|
||||
functionName: 'getConfiguration',
|
||||
args: [LENDING_PROXY]
|
||||
})
|
||||
|
||||
console.log('✅ 成功读取配置!\n')
|
||||
console.log('基础代币 (USDC):', config.baseToken)
|
||||
console.log('价格源:', config.lendingPriceSource)
|
||||
console.log('\n抵押品配置数量:', config.assetConfigs.length)
|
||||
|
||||
config.assetConfigs.forEach((asset, index) => {
|
||||
console.log(`\n${index + 1}. 抵押品:`, asset.asset)
|
||||
console.log(' 精度:', asset.decimals)
|
||||
console.log(' 借款抵押率:', Number(asset.borrowCollateralFactor) / 1e16, '%')
|
||||
console.log(' 清算抵押率:', Number(asset.liquidateCollateralFactor) / 1e16, '%')
|
||||
console.log(' 清算奖励:', Number(asset.liquidationFactor) / 1e16, '%')
|
||||
console.log(' 供应上限:', Number(asset.supplyCap) / 1e18)
|
||||
|
||||
if (asset.asset.toLowerCase() === YT_A.toLowerCase()) {
|
||||
console.log(' ✅ 这是 YT-A')
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 读取配置失败:', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
134
frontend/scripts/test-correct-function-names.js
Normal file
134
frontend/scripts/test-correct-function-names.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05')
|
||||
|
||||
async function main() {
|
||||
console.log('\n测试合约文档中的正确函数名\n')
|
||||
console.log('合约:', LENDING_PROXY)
|
||||
console.log('用户:', USER)
|
||||
console.log('抵押品:', YT_A)
|
||||
console.log()
|
||||
|
||||
// 1. 测试 getCollateral(正确的函数名)
|
||||
console.log('=== 1. getCollateral(account, asset) ===')
|
||||
try {
|
||||
const collateral = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'account', type: 'address' },
|
||||
{ internalType: 'address', name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'getCollateral',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'getCollateral',
|
||||
args: [USER, YT_A]
|
||||
})
|
||||
console.log('✓ 成功!')
|
||||
console.log(' 抵押品余额:', Number(collateral) / 1e18, 'YT-A')
|
||||
console.log(' 原始值:', collateral.toString())
|
||||
} catch (error) {
|
||||
console.log('✗ 失败:', error.message.split('\n')[0])
|
||||
}
|
||||
|
||||
// 2. 测试 getBalance(用户USDC余额)
|
||||
console.log('\n=== 2. getBalance(account) ===')
|
||||
try {
|
||||
const balance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
||||
name: 'getBalance',
|
||||
outputs: [{ internalType: 'int256', name: '', type: 'int256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'getBalance',
|
||||
args: [USER]
|
||||
})
|
||||
console.log('✓ 成功!')
|
||||
console.log(' USDC 余额:', Number(balance) / 1e6, 'USDC')
|
||||
console.log(' 原始值:', balance.toString())
|
||||
} catch (error) {
|
||||
console.log('✗ 失败:', error.message.split('\n')[0])
|
||||
}
|
||||
|
||||
// 3. 测试 borrowBalanceOf
|
||||
console.log('\n=== 3. borrowBalanceOf(account) ===')
|
||||
try {
|
||||
const borrowBalance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
||||
name: 'borrowBalanceOf',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'borrowBalanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
console.log('✓ 成功!')
|
||||
console.log(' 借款余额:', Number(borrowBalance) / 1e6, 'USDC')
|
||||
} catch (error) {
|
||||
console.log('✗ 失败:', error.message.split('\n')[0])
|
||||
}
|
||||
|
||||
// 4. 测试系统函数
|
||||
console.log('\n=== 4. getTotalBorrow() ===')
|
||||
try {
|
||||
const totalBorrow = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [],
|
||||
name: 'getTotalBorrow',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'getTotalBorrow'
|
||||
})
|
||||
console.log('✓ 成功!')
|
||||
console.log(' 系统总借款:', Number(totalBorrow) / 1e6, 'USDC')
|
||||
} catch (error) {
|
||||
console.log('✗ 失败:', error.message.split('\n')[0])
|
||||
}
|
||||
|
||||
console.log('\n=== 5. getTotalSupply() ===')
|
||||
try {
|
||||
const totalSupply = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [],
|
||||
name: 'getTotalSupply',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'getTotalSupply'
|
||||
})
|
||||
console.log('✓ 成功!')
|
||||
console.log(' 系统总供应:', Number(totalSupply) / 1e6, 'USDC')
|
||||
} catch (error) {
|
||||
console.log('✗ 失败:', error.message.split('\n')[0])
|
||||
}
|
||||
|
||||
console.log('\n=== 结论 ===')
|
||||
console.log('如果上面的函数都能工作,说明:')
|
||||
console.log('1. 合约使用的是文档中的函数名(getCollateral 等)')
|
||||
console.log('2. 前端 ABI 需要更新为文档中的正确函数名')
|
||||
console.log('3. getUserCollateral 和 getUserAccountData 不存在于合约中\n')
|
||||
}
|
||||
|
||||
main()
|
||||
67
frontend/scripts/test-supply-with-trace.js
Normal file
67
frontend/scripts/test-supply-with-trace.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { createPublicClient, createWalletClient, http, parseEther } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
import { privateKeyToAccount } from 'viem/accounts'
|
||||
|
||||
const publicClient = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D'
|
||||
const YT_A = '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05'
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'asset', type: 'address' },
|
||||
{ internalType: 'uint256', name: 'amount', type: 'uint256' }
|
||||
],
|
||||
name: 'supplyCollateral',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n尝试捕获详细错误...\n')
|
||||
|
||||
try {
|
||||
// 尝试估算 gas
|
||||
const gasEstimate = await publicClient.estimateContractGas({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'supplyCollateral',
|
||||
args: [YT_A, parseEther('10')],
|
||||
account: '0xa013422A5918CD099C63c8CC35283EACa99a705d'
|
||||
})
|
||||
|
||||
console.log('✓ Gas 估算成功:', gasEstimate.toString())
|
||||
|
||||
} catch (error) {
|
||||
console.log('✗ Gas 估算失败\n')
|
||||
console.log('错误类型:', error.name)
|
||||
console.log('错误消息:', error.shortMessage || error.message)
|
||||
|
||||
if (error.cause) {
|
||||
console.log('\n详细原因:')
|
||||
console.log(' Reason:', error.cause.reason)
|
||||
console.log(' Details:', error.cause.details)
|
||||
|
||||
// 尝试解析 revert 原因
|
||||
if (error.cause.data) {
|
||||
console.log(' Raw Data:', error.cause.data)
|
||||
}
|
||||
}
|
||||
|
||||
if (error.details) {
|
||||
console.log('\n更多细节:', error.details)
|
||||
}
|
||||
|
||||
// 打印完整错误用于调试
|
||||
console.log('\n=== 完整错误对象 ===')
|
||||
console.log(JSON.stringify(error, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
104
frontend/scripts/test-total-functions.js
Normal file
104
frontend/scripts/test-total-functions.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { createPublicClient, http, formatUnits } from 'viem';
|
||||
import { arbitrumSepolia } from 'viem/chains';
|
||||
|
||||
const RPC_URL = 'https://sepolia-rollup.arbitrum.io/rpc';
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D';
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http(RPC_URL)
|
||||
});
|
||||
|
||||
async function testBothFunctions() {
|
||||
console.log('\n=== 测试 getTotalSupply vs getTotalLiquidity ===\n');
|
||||
|
||||
// 测试 getTotalSupply
|
||||
console.log('1️⃣ 测试 getTotalSupply():');
|
||||
try {
|
||||
const totalSupply = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [],
|
||||
name: 'getTotalSupply',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'getTotalSupply'
|
||||
});
|
||||
console.log(' ✅ 调用成功!');
|
||||
console.log(' 值:', totalSupply.toString());
|
||||
console.log(' 格式化:', formatUnits(totalSupply, 6), 'USDC');
|
||||
} catch (error) {
|
||||
console.log(' ❌ 调用失败!');
|
||||
console.log(' 错误:', error.message.split('\n')[0]);
|
||||
}
|
||||
|
||||
console.log('\n2️⃣ 测试 getTotalLiquidity():');
|
||||
try {
|
||||
const totalLiquidity = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [],
|
||||
name: 'getTotalLiquidity',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'getTotalLiquidity'
|
||||
});
|
||||
console.log(' ✅ 调用成功!');
|
||||
console.log(' 值:', totalLiquidity.toString());
|
||||
console.log(' 格式化:', formatUnits(totalLiquidity, 6), 'USDC');
|
||||
} catch (error) {
|
||||
console.log(' ❌ 调用失败!');
|
||||
console.log(' 错误:', error.message.split('\n')[0]);
|
||||
}
|
||||
|
||||
// 测试其他可能的函数名
|
||||
console.log('\n3️⃣ 测试 totalSupply() (Compound V3 标准):');
|
||||
try {
|
||||
const totalSupply = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [],
|
||||
name: 'totalSupply',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'totalSupply'
|
||||
});
|
||||
console.log(' ✅ 调用成功!');
|
||||
console.log(' 值:', totalSupply.toString());
|
||||
console.log(' 格式化:', formatUnits(totalSupply, 6), 'USDC');
|
||||
} catch (error) {
|
||||
console.log(' ❌ 调用失败!');
|
||||
console.log(' 错误:', error.message.split('\n')[0]);
|
||||
}
|
||||
|
||||
console.log('\n4️⃣ 测试 totalBorrow():');
|
||||
try {
|
||||
const totalBorrow = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [{
|
||||
inputs: [],
|
||||
name: 'totalBorrow',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}],
|
||||
functionName: 'totalBorrow'
|
||||
});
|
||||
console.log(' ✅ 调用成功!');
|
||||
console.log(' 值:', totalBorrow.toString());
|
||||
console.log(' 格式化:', formatUnits(totalBorrow, 6), 'USDC');
|
||||
} catch (error) {
|
||||
console.log(' ❌ 调用失败!');
|
||||
console.log(' 错误:', error.message.split('\n')[0]);
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
testBothFunctions().catch(console.error);
|
||||
169
frontend/scripts/verify-all-lending-functions.js
Normal file
169
frontend/scripts/verify-all-lending-functions.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { createPublicClient, http, formatUnits } from 'viem';
|
||||
import { arbitrumSepolia } from 'viem/chains';
|
||||
|
||||
const RPC_URL = 'https://sepolia-rollup.arbitrum.io/rpc';
|
||||
const LENDING_PROXY = '0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D';
|
||||
const TEST_USER = '0xa013422A5918CD099C63c8CC35283EACa99a705d'; // 有抵押品的用户
|
||||
const YT_A = '0x97204190B35D9895a7a47aa7BaC61ac08De3cF05';
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http(RPC_URL)
|
||||
});
|
||||
|
||||
const testResults = {
|
||||
success: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
async function testFunction(name, abi, params = []) {
|
||||
try {
|
||||
const result = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: [abi],
|
||||
functionName: name,
|
||||
args: params
|
||||
});
|
||||
testResults.success.push(name);
|
||||
return { success: true, value: result };
|
||||
} catch (error) {
|
||||
testResults.failed.push(name);
|
||||
return { success: false, error: error.message.split('\n')[0] };
|
||||
}
|
||||
}
|
||||
|
||||
async function runAllTests() {
|
||||
console.log('\n=== 验证所有前端 LENDING ABI 函数 ===\n');
|
||||
|
||||
// 1. getCollateral - 用户查询
|
||||
console.log('1️⃣ getCollateral(account, asset):');
|
||||
const r1 = await testFunction('getCollateral', {
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'account', type: 'address' },
|
||||
{ internalType: 'address', name: 'asset', type: 'address' }
|
||||
],
|
||||
name: 'getCollateral',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [TEST_USER, YT_A]);
|
||||
console.log(r1.success ? ` ✅ 成功 - 抵押量: ${formatUnits(r1.value, 18)}` : ` ❌ 失败 - ${r1.error}`);
|
||||
|
||||
// 2. getBalance - 用户余额
|
||||
console.log('\n2️⃣ getBalance(account):');
|
||||
const r2 = await testFunction('getBalance', {
|
||||
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
||||
name: 'getBalance',
|
||||
outputs: [{ internalType: 'int256', name: '', type: 'int256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [TEST_USER]);
|
||||
console.log(r2.success ? ` ✅ 成功 - 余额: ${r2.value.toString()}` : ` ❌ 失败 - ${r2.error}`);
|
||||
|
||||
// 3. borrowBalanceOf - 借款余额
|
||||
console.log('\n3️⃣ borrowBalanceOf(account):');
|
||||
const r3 = await testFunction('borrowBalanceOf', {
|
||||
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
||||
name: 'borrowBalanceOf',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}, [TEST_USER]);
|
||||
console.log(r3.success ? ` ✅ 成功 - 借款: ${formatUnits(r3.value, 6)} USDC` : ` ❌ 失败 - ${r3.error}`);
|
||||
|
||||
// 4. getTotalBorrow - 系统总借款
|
||||
console.log('\n4️⃣ getTotalBorrow():');
|
||||
const r4 = await testFunction('getTotalBorrow', {
|
||||
inputs: [],
|
||||
name: 'getTotalBorrow',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
});
|
||||
console.log(r4.success ? ` ✅ 成功 - 总借款: ${formatUnits(r4.value, 6)} USDC` : ` ❌ 失败 - ${r4.error}`);
|
||||
|
||||
// 5. getTotalSupply - 系统总供应
|
||||
console.log('\n5️⃣ getTotalSupply():');
|
||||
const r5 = await testFunction('getTotalSupply', {
|
||||
inputs: [],
|
||||
name: 'getTotalSupply',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
});
|
||||
console.log(r5.success ? ` ✅ 成功 - 总供应: ${formatUnits(r5.value, 6)} USDC` : ` ❌ 失败 - ${r5.error}`);
|
||||
|
||||
// 6. getUtilization - 资金利用率
|
||||
console.log('\n6️⃣ getUtilization():');
|
||||
const r6 = await testFunction('getUtilization', {
|
||||
inputs: [],
|
||||
name: 'getUtilization',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
});
|
||||
console.log(r6.success ? ` ✅ 成功 - 利用率: ${Number(r6.value) / 1e16}%` : ` ❌ 失败 - ${r6.error}`);
|
||||
|
||||
// 7. getBorrowRate - 借款利率
|
||||
console.log('\n7️⃣ getBorrowRate():');
|
||||
const r7 = await testFunction('getBorrowRate', {
|
||||
inputs: [],
|
||||
name: 'getBorrowRate',
|
||||
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
});
|
||||
console.log(r7.success ? ` ✅ 成功 - 借款利率: ${Number(r7.value) / 1e16}%` : ` ❌ 失败 - ${r7.error}`);
|
||||
|
||||
// 8. getSupplyRate - 存款利率
|
||||
console.log('\n8️⃣ getSupplyRate():');
|
||||
const r8 = await testFunction('getSupplyRate', {
|
||||
inputs: [],
|
||||
name: 'getSupplyRate',
|
||||
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
});
|
||||
console.log(r8.success ? ` ✅ 成功 - 存款利率: ${Number(r8.value) / 1e16}%` : ` ❌ 失败 - ${r8.error}`);
|
||||
|
||||
// 9. owner - 管理员
|
||||
console.log('\n9️⃣ owner():');
|
||||
const r9 = await testFunction('owner', {
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
});
|
||||
console.log(r9.success ? ` ✅ 成功 - Owner: ${r9.value}` : ` ❌ 失败 - ${r9.error}`);
|
||||
|
||||
// 10. paused - 暂停状态
|
||||
console.log('\n🔟 paused():');
|
||||
const r10 = await testFunction('paused', {
|
||||
inputs: [],
|
||||
name: 'paused',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
});
|
||||
console.log(r10.success ? ` ✅ 成功 - Paused: ${r10.value}` : ` ❌ 失败 - ${r10.error}`);
|
||||
|
||||
// 汇总
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 测试结果汇总:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`✅ 成功: ${testResults.success.length}/10`);
|
||||
console.log(` ${testResults.success.join(', ')}`);
|
||||
if (testResults.failed.length > 0) {
|
||||
console.log(`❌ 失败: ${testResults.failed.length}/10`);
|
||||
console.log(` ${testResults.failed.join(', ')}`);
|
||||
}
|
||||
console.log('='.repeat(60) + '\n');
|
||||
|
||||
// 对比测试脚本中的错误函数
|
||||
console.log('⚠️ check-lending-setup.js 中使用的错误函数:');
|
||||
console.log(' getTotalLiquidity() - 不存在,应改为 getTotalSupply()');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
runAllTests().catch(console.error);
|
||||
53
frontend/scripts/verify-apy-calculation.js
Normal file
53
frontend/scripts/verify-apy-calculation.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// 验证 APY 计算逻辑
|
||||
|
||||
const borrowRatePerSecond = 14999999976144000n
|
||||
const secondsPerYear = BigInt(365 * 24 * 60 * 60) // 31536000
|
||||
|
||||
console.log('\n=== APY 计算验证 ===\n')
|
||||
|
||||
console.log('原始数据:')
|
||||
console.log(' borrowRatePerSecond:', borrowRatePerSecond.toString())
|
||||
console.log(' secondsPerYear:', secondsPerYear.toString())
|
||||
console.log()
|
||||
|
||||
// 方法1:错误的计算(之前的方式)
|
||||
const wrongAPY = Number(borrowRatePerSecond) / 10000
|
||||
console.log('❌ 错误计算(除以10000):')
|
||||
console.log(' APY:', wrongAPY.toFixed(2), '%')
|
||||
console.log(' = 1,499,999,997,614.40 %(明显错误)')
|
||||
console.log()
|
||||
|
||||
// 方法2:正确的计算
|
||||
// APY% = (ratePerSecond / 1e18) × secondsPerYear × 100
|
||||
const borrowAPY = (borrowRatePerSecond * secondsPerYear * BigInt(100)) / BigInt(10 ** 18)
|
||||
console.log('✅ 正确计算(Compound V3 格式):')
|
||||
console.log(' 每秒利率:', Number(borrowRatePerSecond) / 1e18)
|
||||
console.log(' 年化倍数 (1 + rate)^seconds ≈ rate × seconds:')
|
||||
console.log(' ', Number(borrowRatePerSecond) / 1e18, '× 31,536,000 =', Number(borrowRatePerSecond) * 31536000 / 1e18)
|
||||
console.log(' APY:', Number(borrowAPY).toFixed(2), '%')
|
||||
console.log()
|
||||
|
||||
// 详细计算步骤
|
||||
console.log('计算步骤:')
|
||||
console.log(' 1. borrowRatePerSecond × secondsPerYear × 100')
|
||||
console.log(' =', borrowRatePerSecond.toString(), '×', secondsPerYear.toString(), '× 100')
|
||||
const step1 = borrowRatePerSecond * secondsPerYear * BigInt(100)
|
||||
console.log(' =', step1.toString())
|
||||
console.log()
|
||||
console.log(' 2. 结果 ÷ 10^18')
|
||||
console.log(' =', step1.toString(), '÷ 1000000000000000000')
|
||||
console.log(' =', borrowAPY.toString())
|
||||
console.log()
|
||||
|
||||
console.log('最终结果: ', Number(borrowAPY).toFixed(2), '%')
|
||||
console.log()
|
||||
|
||||
// 验证使用率计算
|
||||
console.log('=== 使用率计算验证 ===\n')
|
||||
console.log('如果 utilizationRate = 0(当前没有借款)')
|
||||
const utilizationRate = 0n
|
||||
const utilizationPercent = utilizationRate > 0n
|
||||
? (utilizationRate * BigInt(100)) / BigInt(10 ** 18)
|
||||
: 0n
|
||||
console.log(' 使用率:', Number(utilizationPercent).toFixed(2), '%')
|
||||
console.log()
|
||||
116
frontend/scripts/verify-current-borrow.js
Normal file
116
frontend/scripts/verify-current-borrow.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { createPublicClient, http, getAddress } from 'viem'
|
||||
import { arbitrumSepolia } from 'viem/chains'
|
||||
|
||||
const client = createPublicClient({
|
||||
chain: arbitrumSepolia,
|
||||
transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07')
|
||||
})
|
||||
|
||||
const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D')
|
||||
const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d')
|
||||
|
||||
const LENDING_ABI = [
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'getBalance',
|
||||
outputs: [{ name: '', type: 'int256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
name: 'borrowBalanceOf',
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'getBorrowRate',
|
||||
outputs: [{ name: '', type: 'uint64' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('\n=== 验证当前借款状态 ===\n')
|
||||
console.log('用户:', USER)
|
||||
console.log('Lending 合约:', LENDING_PROXY)
|
||||
console.log()
|
||||
|
||||
// 获取账户余额
|
||||
const balance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getBalance',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
// 获取借款余额
|
||||
const borrowBalance = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'borrowBalanceOf',
|
||||
args: [USER]
|
||||
})
|
||||
|
||||
// 获取借款利率
|
||||
const borrowRate = await client.readContract({
|
||||
address: LENDING_PROXY,
|
||||
abi: LENDING_ABI,
|
||||
functionName: 'getBorrowRate'
|
||||
})
|
||||
|
||||
console.log('=== 账户状态 ===')
|
||||
console.log()
|
||||
console.log('getBalance() 返回值:', balance.toString())
|
||||
if (balance > 0) {
|
||||
console.log(' → 用户有存款:', Number(balance) / 1e6, 'USDC')
|
||||
} else if (balance < 0) {
|
||||
console.log(' → 用户有负余额(债务):', Number(-balance) / 1e6, 'USDC')
|
||||
} else {
|
||||
console.log(' → 用户余额为 0')
|
||||
}
|
||||
console.log()
|
||||
|
||||
console.log('borrowBalanceOf() 返回值:', borrowBalance.toString())
|
||||
console.log(' → 借款金额:', Number(borrowBalance) / 1e6, 'USDC')
|
||||
console.log()
|
||||
|
||||
console.log('=== 利息信息 ===')
|
||||
console.log()
|
||||
console.log('借款年化利率 (APY):', (Number(borrowRate) / 1e18 * 100).toFixed(2), '%')
|
||||
console.log()
|
||||
|
||||
// 分析结果
|
||||
console.log('=== 分析 ===')
|
||||
console.log()
|
||||
|
||||
if (borrowBalance > 0n) {
|
||||
console.log('✓ 用户确实有借款!')
|
||||
console.log()
|
||||
console.log('借款金额:', Number(borrowBalance) / 1e6, 'USDC')
|
||||
console.log()
|
||||
|
||||
if (balance === 0n) {
|
||||
console.log('余额状态: balance = 0,这意味着:')
|
||||
console.log(' - 用户之前可能有存款')
|
||||
console.log(' - withdraw 金额超过了存款')
|
||||
console.log(' - 超出部分形成了债务')
|
||||
console.log()
|
||||
console.log('这正是 Compound V3 的设计:')
|
||||
console.log(' withdraw(amount) 会:')
|
||||
console.log(' 1. 优先从存款中扣除')
|
||||
console.log(' 2. 存款不足时,自动创建债务')
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log('用户需要归还的总额:', Number(borrowBalance) / 1e6, 'USDC')
|
||||
console.log('(注意:每秒都在计息,实际金额会略高)')
|
||||
} else {
|
||||
console.log('✗ 用户当前没有借款')
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -1,24 +1,58 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { WagmiProvider } from 'wagmi'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useWeb3Modal } from '@web3modal/wagmi/react'
|
||||
import { useAccount, useChainId, useSwitchChain } from 'wagmi'
|
||||
import { getSupportedChainIds, getChainName, isSupportedChain } from './config/contracts'
|
||||
import { config, queryClient } from './config/wagmi'
|
||||
import { ConnectButton } from './components/ConnectButton'
|
||||
import { LanguageSwitch } from './components/LanguageSwitch'
|
||||
import { WUSDPanel } from './components/WUSDPanel'
|
||||
import { USDCPanel } from './components/USDCPanel'
|
||||
import { VaultPanel } from './components/VaultPanel'
|
||||
import { FactoryPanel } from './components/FactoryPanel'
|
||||
import { LPPanel } from './components/LPPanel'
|
||||
import { LPPanelNew as LPPanel } from './components/LP'
|
||||
import { LendingPanel } from './components/LendingPanel'
|
||||
import { HoldersPanel } from './components/HoldersPanel'
|
||||
// import { AutoTestPanel } from './components/AutoTestPanel' // 隐藏自动测试
|
||||
import { ToastProvider } from './components/Toast'
|
||||
import { TransactionProvider } from './context/TransactionContext'
|
||||
import { ErrorBoundary } from './components/ErrorBoundary'
|
||||
import './App.css'
|
||||
|
||||
type Tab = 'wusd' | 'vault' | 'factory' | 'lp'
|
||||
type Tab = 'usdc' | 'vault' | 'factory' | 'lp' | 'lending' | 'holders'
|
||||
|
||||
function AppContent() {
|
||||
const { t } = useTranslation()
|
||||
const [activeTab, setActiveTab] = useState<Tab>('vault')
|
||||
const { open } = useWeb3Modal()
|
||||
const { isConnected } = useAccount()
|
||||
|
||||
// ===== 多链支持:网络切换 =====
|
||||
const chainId = useChainId()
|
||||
const { switchChain } = useSwitchChain()
|
||||
const supportedChains = getSupportedChainIds()
|
||||
const currentChainName = getChainName(chainId)
|
||||
const isSupported = isSupportedChain(chainId)
|
||||
|
||||
// 切换网络
|
||||
const handleSwitchChain = (newChainId: number) => {
|
||||
if (switchChain && newChainId !== chainId) {
|
||||
switchChain({ chainId: newChainId })
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要自动打开连接弹窗(切换钱包后)
|
||||
useEffect(() => {
|
||||
const shouldAutoOpen = sessionStorage.getItem('autoOpenConnect')
|
||||
if (shouldAutoOpen && !isConnected) {
|
||||
sessionStorage.removeItem('autoOpenConnect')
|
||||
// 延迟打开,等待页面完全加载
|
||||
setTimeout(() => {
|
||||
open()
|
||||
}, 500)
|
||||
}
|
||||
}, [isConnected, open])
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
@@ -26,17 +60,51 @@ function AppContent() {
|
||||
<h1>{t('header.title')}</h1>
|
||||
<div className="header-info">
|
||||
<LanguageSwitch />
|
||||
<span className="network-badge">{t('common.network')}</span>
|
||||
|
||||
{/* 网络切换器 */}
|
||||
<div className="network-switcher">
|
||||
<select
|
||||
value={chainId}
|
||||
onChange={(e) => handleSwitchChain(Number(e.target.value))}
|
||||
className="network-select"
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: '6px',
|
||||
border: isSupported ? '1px solid #4CAF50' : '1px solid #f44336',
|
||||
backgroundColor: isSupported ? '#f1f8f4' : '#ffebee',
|
||||
color: isSupported ? '#2e7d32' : '#c62828',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{supportedChains.map(id => {
|
||||
const name = getChainName(id)
|
||||
return (
|
||||
<option key={id} value={id}>
|
||||
{name} (ID: {id})
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
{!isSupported && (
|
||||
<span style={{ color: '#f44336', fontSize: '12px', marginLeft: '8px' }}>
|
||||
⚠️ 不支持的网络
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="network-badge">{currentChainName}</span>
|
||||
<ConnectButton />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav className="nav">
|
||||
<button
|
||||
className={`nav-btn ${activeTab === 'wusd' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('wusd')}
|
||||
className={`nav-btn ${activeTab === 'usdc' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('usdc')}
|
||||
>
|
||||
{t('nav.wusd')}
|
||||
{t('nav.usdc')}
|
||||
</button>
|
||||
<button
|
||||
className={`nav-btn ${activeTab === 'vault' ? 'active' : ''}`}
|
||||
@@ -56,13 +124,27 @@ function AppContent() {
|
||||
>
|
||||
{t('nav.lpPool')}
|
||||
</button>
|
||||
<button
|
||||
className={`nav-btn ${activeTab === 'lending' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('lending')}
|
||||
>
|
||||
{t('nav.lending')}
|
||||
</button>
|
||||
<button
|
||||
className={`nav-btn ${activeTab === 'holders' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('holders')}
|
||||
>
|
||||
{t('nav.holders')}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<main className="main">
|
||||
{activeTab === 'wusd' && <WUSDPanel />}
|
||||
{activeTab === 'usdc' && <USDCPanel />}
|
||||
{activeTab === 'vault' && <VaultPanel />}
|
||||
{activeTab === 'factory' && <FactoryPanel />}
|
||||
{activeTab === 'lp' && <LPPanel />}
|
||||
{activeTab === 'lending' && <LendingPanel />}
|
||||
{activeTab === 'holders' && <HoldersPanel />}
|
||||
</main>
|
||||
|
||||
<footer className="footer">
|
||||
|
||||
999
frontend/src/components/AutoTestPanel.tsx
Normal file
999
frontend/src/components/AutoTestPanel.tsx
Normal file
@@ -0,0 +1,999 @@
|
||||
/**
|
||||
* 自动化测试面板
|
||||
*
|
||||
* 模拟用户点击操作,自动执行交易流程
|
||||
* 使用与前端一致的 Gas 配置和授权流程
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccount, usePublicClient, useWalletClient, useChainId, useChains } from 'wagmi'
|
||||
import {
|
||||
createWalletClient,
|
||||
createPublicClient,
|
||||
http,
|
||||
parseUnits,
|
||||
formatUnits,
|
||||
type WalletClient,
|
||||
type PublicClient,
|
||||
type Account,
|
||||
} from 'viem'
|
||||
import { privateKeyToAccount } from 'viem/accounts'
|
||||
import { arbitrumSepolia, bscTestnet } from 'viem/chains'
|
||||
import { GAS_CONFIG, getContracts, getDecimals, getChainConfig, getChainName } from '../config/contracts'
|
||||
|
||||
// 测试配置
|
||||
const TEST_AMOUNTS = {
|
||||
SMALL: '10',
|
||||
MEDIUM: '100',
|
||||
LARGE: '1000',
|
||||
}
|
||||
|
||||
// ABI 片段
|
||||
const ERC20_ABI = [
|
||||
{ inputs: [{ name: 'owner', type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'approve', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
|
||||
{ inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], name: 'allowance', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [{ name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }], name: 'transfer', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
|
||||
] as const
|
||||
|
||||
const USDC_LOCAL_ABI = [
|
||||
{ inputs: [{ name: 'owner', type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [{ name: '_to', type: 'address' }, { name: '_amount', type: 'uint256' }], name: 'mint', outputs: [], stateMutability: 'nonpayable', type: 'function' },
|
||||
{ inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'approve', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
|
||||
{ inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], name: 'allowance', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
] as const
|
||||
|
||||
const VAULT_ABI = [
|
||||
{ inputs: [{ name: 'usdcAmount', type: 'uint256' }], name: 'depositYT', outputs: [{ type: 'uint256' }], stateMutability: 'nonpayable', type: 'function' },
|
||||
{ inputs: [{ name: 'ytAmount', type: 'uint256' }], name: 'withdrawYT', outputs: [{ type: 'uint256' }], stateMutability: 'nonpayable', type: 'function' },
|
||||
{ inputs: [], name: 'hardCap', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [], name: 'totalSupply', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [{ name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }], name: 'transfer', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
|
||||
] as const
|
||||
|
||||
const ROUTER_ABI = [
|
||||
{
|
||||
inputs: [
|
||||
{ name: '_token', type: 'address' },
|
||||
{ name: '_amount', type: 'uint256' },
|
||||
{ name: '_minUsdy', type: 'uint256' },
|
||||
{ name: '_minYtLP', type: 'uint256' }
|
||||
],
|
||||
name: 'addLiquidity',
|
||||
outputs: [{ type: 'uint256' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ name: '_tokenOut', type: 'address' },
|
||||
{ name: '_ytLPAmount', type: 'uint256' },
|
||||
{ name: '_minOut', type: 'uint256' },
|
||||
{ name: '_receiver', type: 'address' }
|
||||
],
|
||||
name: 'removeLiquidity',
|
||||
outputs: [{ type: 'uint256' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ name: '_tokenIn', type: 'address' },
|
||||
{ name: '_tokenOut', type: 'address' },
|
||||
{ name: '_amountIn', type: 'uint256' },
|
||||
{ name: '_minOut', type: 'uint256' },
|
||||
{ name: '_receiver', type: 'address' }
|
||||
],
|
||||
name: 'swapYT',
|
||||
outputs: [{ type: 'uint256' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
] as const
|
||||
|
||||
// 测试结果类型
|
||||
interface TestResult {
|
||||
name: string
|
||||
status: 'pending' | 'running' | 'success' | 'failed' | 'skipped'
|
||||
message?: string
|
||||
txHash?: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
// 测试上下文
|
||||
interface TestContext {
|
||||
publicClient: PublicClient
|
||||
walletClient: WalletClient
|
||||
account: Account | `0x${string}`
|
||||
address: `0x${string}`
|
||||
log: (message: string) => void
|
||||
}
|
||||
|
||||
// 测试项定义
|
||||
interface TestItem {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
run: (ctx: TestContext) => Promise<{ success: boolean; message: string; txHash?: string }>
|
||||
}
|
||||
|
||||
// 辅助函数:执行授权(如果需要)
|
||||
async function ensureApproval(
|
||||
ctx: TestContext,
|
||||
tokenAddress: `0x${string}`,
|
||||
spenderAddress: `0x${string}`,
|
||||
amount: bigint,
|
||||
tokenName: string,
|
||||
decimals: number = TOKEN_DECIMALS.DEFAULT
|
||||
): Promise<boolean> {
|
||||
const allowance = await ctx.publicClient.readContract({
|
||||
address: tokenAddress,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'allowance',
|
||||
args: [ctx.address, spenderAddress],
|
||||
})
|
||||
|
||||
if (allowance >= amount) {
|
||||
ctx.log(` ${tokenName} 已有足够授权,跳过`)
|
||||
return true
|
||||
}
|
||||
|
||||
ctx.log(`>>> 模拟操作: 点击 [授权 ${tokenName}] 按钮`)
|
||||
ctx.log(` 当前授权: ${formatUnits(allowance, decimals)}, 需要: ${formatUnits(amount, decimals)}`)
|
||||
|
||||
const approveTx = await ctx.walletClient.writeContract({
|
||||
address: tokenAddress,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'approve',
|
||||
args: [spenderAddress, amount],
|
||||
account: ctx.account,
|
||||
chain: currentChain,
|
||||
gas: GAS_CONFIG.SIMPLE,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
|
||||
ctx.log(` 授权交易已发送: ${approveTx.slice(0, 18)}...`)
|
||||
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: approveTx })
|
||||
|
||||
if (receipt.status === 'success') {
|
||||
ctx.log(` 授权成功`)
|
||||
return true
|
||||
} else {
|
||||
ctx.log(` [ERROR] 授权失败`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function AutoTestPanel() {
|
||||
const { t } = useTranslation()
|
||||
const { address: connectedAddress, isConnected } = useAccount()
|
||||
const publicClientHook = usePublicClient()
|
||||
const { data: walletClientHook } = useWalletClient()
|
||||
|
||||
// ===== 多链支持 =====
|
||||
const chainId = useChainId()
|
||||
const CONTRACTS = getContracts(chainId)
|
||||
const TOKEN_DECIMALS = getDecimals(chainId)
|
||||
const currentChainName = getChainName(chainId)
|
||||
|
||||
// 根据 chainId 获取对应的 viem chain 配置
|
||||
const currentChain = chainId === 97 ? bscTestnet : arbitrumSepolia
|
||||
|
||||
const isZh = t('nav.autoTest') === '自动测试'
|
||||
|
||||
// 状态
|
||||
const [mode, setMode] = useState<'wallet' | 'privateKey'>('wallet')
|
||||
const [privateKey, setPrivateKey] = useState('')
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [results, setResults] = useState<TestResult[]>([])
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
const [selectedTests, setSelectedTests] = useState<string[]>([
|
||||
'check_balance',
|
||||
'usdc_mint',
|
||||
'vault_buy',
|
||||
'vault_sell',
|
||||
])
|
||||
|
||||
const abortRef = useRef(false)
|
||||
const logsEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [logs])
|
||||
|
||||
const addLog = (message: string) => {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
setLogs(prev => [...prev, `[${timestamp}] ${message}`])
|
||||
}
|
||||
|
||||
// ===== 测试项定义 =====
|
||||
const testItems: TestItem[] = [
|
||||
{
|
||||
id: 'check_balance',
|
||||
name: isZh ? '检查余额' : 'Check Balance',
|
||||
description: isZh ? '读取 USDC / YT / ytLP 余额' : 'Read USDC / YT / ytLP balance',
|
||||
run: async (ctx) => {
|
||||
ctx.log('>>> 模拟操作: 读取用户余额...')
|
||||
const balances: string[] = []
|
||||
|
||||
const usdcBalance = await ctx.publicClient.readContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [ctx.address],
|
||||
})
|
||||
balances.push(`USDC: ${formatUnits(usdcBalance, TOKEN_DECIMALS.USDC)}`)
|
||||
|
||||
const ytaBalance = await ctx.publicClient.readContract({
|
||||
address: CONTRACTS.VAULTS.YT_A,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [ctx.address],
|
||||
})
|
||||
balances.push(`YT-A: ${formatUnits(ytaBalance, TOKEN_DECIMALS.YT)}`)
|
||||
|
||||
const ytlpBalance = await ctx.publicClient.readContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [ctx.address],
|
||||
})
|
||||
balances.push(`ytLP: ${formatUnits(ytlpBalance, TOKEN_DECIMALS.YT_LP)}`)
|
||||
|
||||
return { success: true, message: balances.join(' | ') }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'usdc_mint',
|
||||
name: isZh ? 'USDC 铸造' : 'USDC Mint',
|
||||
description: isZh ? `铸造 ${TEST_AMOUNTS.LARGE} USDC` : `Mint ${TEST_AMOUNTS.LARGE} USDC`,
|
||||
run: async (ctx) => {
|
||||
const amount = parseUnits(TEST_AMOUNTS.LARGE, TOKEN_DECIMALS.USDC)
|
||||
ctx.log('>>> 模拟操作: 进入 USDC 页面')
|
||||
ctx.log(`>>> 模拟操作: 输入铸造金额 ${TEST_AMOUNTS.LARGE}`)
|
||||
ctx.log('>>> 模拟操作: 点击 [铸造] 按钮')
|
||||
|
||||
const mintTx = await ctx.walletClient.writeContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: USDC_LOCAL_ABI,
|
||||
functionName: 'mint',
|
||||
args: [ctx.address, amount],
|
||||
account: ctx.account,
|
||||
chain: currentChain,
|
||||
gas: GAS_CONFIG.SIMPLE,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
|
||||
ctx.log(` 交易已发送: ${mintTx.slice(0, 18)}...`)
|
||||
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: mintTx })
|
||||
|
||||
if (receipt.status === 'success') {
|
||||
return { success: true, message: `铸造 ${TEST_AMOUNTS.LARGE} USDC 成功`, txHash: mintTx }
|
||||
}
|
||||
return { success: false, message: '交易失败', txHash: mintTx }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'vault_buy',
|
||||
name: isZh ? 'Vault 买入 YT' : 'Vault Buy YT',
|
||||
description: isZh ? `${TEST_AMOUNTS.SMALL} USDC -> 授权 -> 买入` : `${TEST_AMOUNTS.SMALL} USDC -> Approve -> Buy`,
|
||||
run: async (ctx) => {
|
||||
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.USDC)
|
||||
const vault = CONTRACTS.VAULTS.YT_A as `0x${string}`
|
||||
|
||||
ctx.log('>>> 模拟操作: 进入金库交易页面')
|
||||
ctx.log(`>>> 模拟操作: 选择 YT-A 金库`)
|
||||
ctx.log(`>>> 模拟操作: 输入买入金额 ${TEST_AMOUNTS.SMALL} USDC`)
|
||||
|
||||
const balance = await ctx.publicClient.readContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [ctx.address],
|
||||
})
|
||||
|
||||
if (balance < amount) {
|
||||
return { success: false, message: `USDC 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.USDC)}` }
|
||||
}
|
||||
ctx.log(` USDC 余额: ${formatUnits(balance, TOKEN_DECIMALS.USDC)}`)
|
||||
|
||||
const [totalSupply, hardCap] = await Promise.all([
|
||||
ctx.publicClient.readContract({ address: vault, abi: VAULT_ABI, functionName: 'totalSupply' }),
|
||||
ctx.publicClient.readContract({ address: vault, abi: VAULT_ABI, functionName: 'hardCap' }),
|
||||
])
|
||||
|
||||
if (totalSupply >= hardCap) {
|
||||
return { success: false, message: '已达硬顶,无法买入' }
|
||||
}
|
||||
ctx.log(` 当前供应: ${formatUnits(totalSupply, TOKEN_DECIMALS.YT)} / ${formatUnits(hardCap, TOKEN_DECIMALS.USDC)}`)
|
||||
|
||||
const approved = await ensureApproval(ctx, CONTRACTS.USDC as `0x${string}`, vault, amount, 'USDC', TOKEN_DECIMALS.USDC)
|
||||
if (!approved) return { success: false, message: '授权失败' }
|
||||
|
||||
ctx.log('>>> 模拟操作: 点击 [买入] 按钮')
|
||||
const buyTx = await ctx.walletClient.writeContract({
|
||||
address: vault,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'depositYT',
|
||||
args: [amount],
|
||||
account: ctx.account,
|
||||
chain: currentChain,
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
|
||||
ctx.log(` 买入交易已发送: ${buyTx.slice(0, 18)}...`)
|
||||
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: buyTx })
|
||||
|
||||
if (receipt.status === 'success') {
|
||||
return { success: true, message: `买入成功`, txHash: buyTx }
|
||||
}
|
||||
return { success: false, message: '交易失败', txHash: buyTx }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'vault_sell',
|
||||
name: isZh ? 'Vault 卖出 YT' : 'Vault Sell YT',
|
||||
description: isZh ? `${TEST_AMOUNTS.SMALL} YT-A -> 卖出` : `${TEST_AMOUNTS.SMALL} YT-A -> Sell`,
|
||||
run: async (ctx) => {
|
||||
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.YT)
|
||||
const vault = CONTRACTS.VAULTS.YT_A as `0x${string}`
|
||||
|
||||
ctx.log('>>> 模拟操作: 切换到卖出 Tab')
|
||||
ctx.log(`>>> 模拟操作: 输入卖出金额 ${TEST_AMOUNTS.SMALL} YT-A`)
|
||||
|
||||
const balance = await ctx.publicClient.readContract({
|
||||
address: vault,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [ctx.address],
|
||||
})
|
||||
|
||||
if (balance < amount) {
|
||||
return { success: false, message: `YT-A 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.YT)}` }
|
||||
}
|
||||
ctx.log(` YT-A 余额: ${formatUnits(balance, TOKEN_DECIMALS.YT)}`)
|
||||
|
||||
ctx.log('>>> 模拟操作: 点击 [卖出] 按钮')
|
||||
const sellTx = await ctx.walletClient.writeContract({
|
||||
address: vault,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'withdrawYT',
|
||||
args: [amount],
|
||||
account: ctx.account,
|
||||
chain: currentChain,
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
|
||||
ctx.log(` 卖出交易已发送: ${sellTx.slice(0, 18)}...`)
|
||||
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: sellTx })
|
||||
|
||||
if (receipt.status === 'success') {
|
||||
return { success: true, message: `已加入队列`, txHash: sellTx }
|
||||
}
|
||||
return { success: false, message: '交易失败', txHash: sellTx }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'yt_transfer',
|
||||
name: isZh ? 'YT 转账' : 'YT Transfer',
|
||||
description: isZh ? `转账 1 YT-A 给自己` : `Transfer 1 YT-A to self`,
|
||||
run: async (ctx) => {
|
||||
const amount = parseUnits('1', TOKEN_DECIMALS.YT)
|
||||
const vault = CONTRACTS.VAULTS.YT_A as `0x${string}`
|
||||
|
||||
ctx.log('>>> 模拟操作: 进入 YT 转账')
|
||||
|
||||
const balance = await ctx.publicClient.readContract({
|
||||
address: vault,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [ctx.address],
|
||||
})
|
||||
|
||||
if (balance < amount) {
|
||||
return { success: false, message: `YT-A 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.YT)}` }
|
||||
}
|
||||
|
||||
ctx.log('>>> 模拟操作: 点击 [转账] 按钮')
|
||||
const transferTx = await ctx.walletClient.writeContract({
|
||||
address: vault,
|
||||
abi: VAULT_ABI,
|
||||
functionName: 'transfer',
|
||||
args: [ctx.address, amount],
|
||||
account: ctx.account,
|
||||
chain: currentChain,
|
||||
gas: GAS_CONFIG.SIMPLE,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
|
||||
ctx.log(` 交易已发送: ${transferTx.slice(0, 18)}...`)
|
||||
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: transferTx })
|
||||
|
||||
if (receipt.status === 'success') {
|
||||
return { success: true, message: `转账成功`, txHash: transferTx }
|
||||
}
|
||||
return { success: false, message: '交易失败', txHash: transferTx }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'lp_add',
|
||||
name: isZh ? 'LP 添加流动性' : 'LP Add Liquidity',
|
||||
description: isZh ? `USDC ${TEST_AMOUNTS.SMALL} -> 添加` : `USDC ${TEST_AMOUNTS.SMALL} -> Add`,
|
||||
run: async (ctx) => {
|
||||
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.USDC)
|
||||
const router = CONTRACTS.YT_REWARD_ROUTER as `0x${string}`
|
||||
|
||||
ctx.log('>>> 模拟操作: 进入 LP 流动池页面')
|
||||
ctx.log(`>>> 模拟操作: 输入金额 ${TEST_AMOUNTS.SMALL}`)
|
||||
|
||||
const balance = await ctx.publicClient.readContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [ctx.address],
|
||||
})
|
||||
|
||||
if (balance < amount) {
|
||||
return { success: false, message: `USDC 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.USDC)}` }
|
||||
}
|
||||
|
||||
const approved = await ensureApproval(ctx, CONTRACTS.USDC as `0x${string}`, router, amount, 'USDC', TOKEN_DECIMALS.USDC)
|
||||
if (!approved) return { success: false, message: '授权失败' }
|
||||
|
||||
ctx.log('>>> 模拟操作: 点击 [添加流动性] 按钮')
|
||||
const addTx = await ctx.walletClient.writeContract({
|
||||
address: router,
|
||||
abi: ROUTER_ABI,
|
||||
functionName: 'addLiquidity',
|
||||
args: [CONTRACTS.USDC as `0x${string}`, amount, 0n, 0n],
|
||||
account: ctx.account,
|
||||
chain: currentChain,
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
|
||||
ctx.log(` 交易已发送: ${addTx.slice(0, 18)}...`)
|
||||
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: addTx })
|
||||
|
||||
if (receipt.status === 'success') {
|
||||
return { success: true, message: `添加成功`, txHash: addTx }
|
||||
}
|
||||
return { success: false, message: '交易失败', txHash: addTx }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'lp_remove',
|
||||
name: isZh ? 'LP 移除流动性' : 'LP Remove Liquidity',
|
||||
description: isZh ? `ytLP ${TEST_AMOUNTS.SMALL} -> 移除` : `ytLP ${TEST_AMOUNTS.SMALL} -> Remove`,
|
||||
run: async (ctx) => {
|
||||
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.YT_LP)
|
||||
const router = CONTRACTS.YT_REWARD_ROUTER as `0x${string}`
|
||||
|
||||
ctx.log('>>> 模拟操作: 切换到移除流动性 Tab')
|
||||
|
||||
const balance = await ctx.publicClient.readContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [ctx.address],
|
||||
})
|
||||
|
||||
if (balance < amount) {
|
||||
return { success: false, message: `ytLP 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.YT_LP)}` }
|
||||
}
|
||||
|
||||
const approved = await ensureApproval(ctx, CONTRACTS.YT_LP_TOKEN as `0x${string}`, router, amount, 'ytLP', TOKEN_DECIMALS.YT_LP)
|
||||
if (!approved) return { success: false, message: '授权失败' }
|
||||
|
||||
ctx.log('>>> 模拟操作: 点击 [移除流动性] 按钮')
|
||||
const removeTx = await ctx.walletClient.writeContract({
|
||||
address: router,
|
||||
abi: ROUTER_ABI,
|
||||
functionName: 'removeLiquidity',
|
||||
args: [CONTRACTS.USDC as `0x${string}`, amount, 0n, ctx.address],
|
||||
account: ctx.account,
|
||||
chain: currentChain,
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
|
||||
ctx.log(` 交易已发送: ${removeTx.slice(0, 18)}...`)
|
||||
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: removeTx })
|
||||
|
||||
if (receipt.status === 'success') {
|
||||
return { success: true, message: `移除成功`, txHash: removeTx }
|
||||
}
|
||||
return { success: false, message: '交易失败', txHash: removeTx }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'lp_swap',
|
||||
name: isZh ? 'LP Swap' : 'LP Swap',
|
||||
description: isZh ? `USDC ${TEST_AMOUNTS.SMALL} -> YT-A` : `USDC ${TEST_AMOUNTS.SMALL} -> YT-A`,
|
||||
run: async (ctx) => {
|
||||
const amount = parseUnits(TEST_AMOUNTS.SMALL, TOKEN_DECIMALS.USDC)
|
||||
const router = CONTRACTS.YT_REWARD_ROUTER as `0x${string}`
|
||||
|
||||
ctx.log('>>> 模拟操作: 切换到代币互换 Tab')
|
||||
|
||||
const balance = await ctx.publicClient.readContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: ERC20_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: [ctx.address],
|
||||
})
|
||||
|
||||
if (balance < amount) {
|
||||
return { success: false, message: `USDC 余额不足: ${formatUnits(balance, TOKEN_DECIMALS.USDC)}` }
|
||||
}
|
||||
|
||||
const approved = await ensureApproval(ctx, CONTRACTS.USDC as `0x${string}`, router, amount, 'USDC', TOKEN_DECIMALS.USDC)
|
||||
if (!approved) return { success: false, message: '授权失败' }
|
||||
|
||||
ctx.log('>>> 模拟操作: 点击 [交换] 按钮')
|
||||
const swapTx = await ctx.walletClient.writeContract({
|
||||
address: router,
|
||||
abi: ROUTER_ABI,
|
||||
functionName: 'swapYT',
|
||||
args: [
|
||||
CONTRACTS.USDC as `0x${string}`,
|
||||
CONTRACTS.VAULTS.YT_A as `0x${string}`,
|
||||
amount,
|
||||
0n,
|
||||
ctx.address
|
||||
],
|
||||
account: ctx.account,
|
||||
chain: currentChain,
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
|
||||
ctx.log(` 交易已发送: ${swapTx.slice(0, 18)}...`)
|
||||
const receipt = await ctx.publicClient.waitForTransactionReceipt({ hash: swapTx })
|
||||
|
||||
if (receipt.status === 'success') {
|
||||
return { success: true, message: `Swap 成功`, txHash: swapTx }
|
||||
}
|
||||
return { success: false, message: '交易失败', txHash: swapTx }
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
// 运行测试
|
||||
const runTests = async () => {
|
||||
setIsRunning(true)
|
||||
setLogs([])
|
||||
abortRef.current = false
|
||||
|
||||
let publicClient: PublicClient
|
||||
let walletClient: WalletClient
|
||||
let account: Account | `0x${string}`
|
||||
let address: `0x${string}`
|
||||
|
||||
try {
|
||||
if (mode === 'privateKey') {
|
||||
if (!privateKey || !privateKey.startsWith('0x')) {
|
||||
addLog('[ERROR] 请输入有效的私钥 (0x开头)')
|
||||
setIsRunning(false)
|
||||
return
|
||||
}
|
||||
|
||||
const acc = privateKeyToAccount(privateKey as `0x${string}`)
|
||||
account = acc
|
||||
address = acc.address
|
||||
|
||||
publicClient = createPublicClient({
|
||||
chain: currentChain,
|
||||
transport: http(),
|
||||
})
|
||||
|
||||
walletClient = createWalletClient({
|
||||
account: acc,
|
||||
chain: currentChain,
|
||||
transport: http(),
|
||||
})
|
||||
|
||||
addLog(`[INFO] 私钥模式 - 全自动测试`)
|
||||
addLog(`[INFO] 测试地址: ${address}`)
|
||||
} else {
|
||||
if (!isConnected || !connectedAddress || !walletClientHook || !publicClientHook) {
|
||||
addLog('[ERROR] 请先连接钱包')
|
||||
setIsRunning(false)
|
||||
return
|
||||
}
|
||||
|
||||
publicClient = publicClientHook as PublicClient
|
||||
walletClient = walletClientHook as WalletClient
|
||||
account = connectedAddress
|
||||
address = connectedAddress
|
||||
|
||||
addLog(`[INFO] 钱包模式 - 需要手动确认交易`)
|
||||
addLog(`[INFO] 测试地址: ${address}`)
|
||||
}
|
||||
|
||||
const testsToRun = testItems.filter(t => selectedTests.includes(t.id))
|
||||
|
||||
setResults(testsToRun.map(t => ({
|
||||
name: t.name,
|
||||
status: 'pending',
|
||||
})))
|
||||
|
||||
addLog(``)
|
||||
addLog(`========================================`)
|
||||
addLog(` 开始运行 ${testsToRun.length} 项测试`)
|
||||
addLog(`========================================`)
|
||||
|
||||
for (let i = 0; i < testsToRun.length; i++) {
|
||||
if (abortRef.current) {
|
||||
addLog('[INFO] 测试已中止')
|
||||
break
|
||||
}
|
||||
|
||||
const test = testsToRun[i]
|
||||
addLog(``)
|
||||
addLog(`----------------------------------------`)
|
||||
addLog(`[${i + 1}/${testsToRun.length}] ${test.name}`)
|
||||
addLog(`----------------------------------------`)
|
||||
|
||||
setResults(prev => prev.map((r, idx) =>
|
||||
idx === i ? { ...r, status: 'running' } : r
|
||||
))
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const ctx: TestContext = {
|
||||
publicClient,
|
||||
walletClient,
|
||||
account,
|
||||
address,
|
||||
log: addLog,
|
||||
}
|
||||
|
||||
const result = await test.run(ctx)
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
setResults(prev => prev.map((r, idx) =>
|
||||
idx === i ? {
|
||||
...r,
|
||||
status: result.success ? 'success' : 'failed',
|
||||
message: result.message,
|
||||
txHash: result.txHash,
|
||||
duration,
|
||||
} : r
|
||||
))
|
||||
|
||||
if (result.success) {
|
||||
addLog(`[PASS] ${result.message} (${duration}ms)`)
|
||||
} else {
|
||||
addLog(`[FAIL] ${result.message}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
const duration = Date.now() - startTime
|
||||
const errorMsg = error?.shortMessage || error?.message || '未知错误'
|
||||
|
||||
setResults(prev => prev.map((r, idx) =>
|
||||
idx === i ? {
|
||||
...r,
|
||||
status: 'failed',
|
||||
message: errorMsg,
|
||||
duration,
|
||||
} : r
|
||||
))
|
||||
|
||||
addLog(`[ERROR] ${errorMsg}`)
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
}
|
||||
|
||||
addLog(``)
|
||||
addLog(`========================================`)
|
||||
addLog(` 测试完成`)
|
||||
addLog(`========================================`)
|
||||
|
||||
} catch (error: any) {
|
||||
addLog(`[ERROR] 初始化失败: ${error.message}`)
|
||||
}
|
||||
|
||||
setIsRunning(false)
|
||||
}
|
||||
|
||||
const stopTests = () => {
|
||||
abortRef.current = true
|
||||
addLog('[INFO] 正在停止测试...')
|
||||
}
|
||||
|
||||
const toggleTest = (id: string) => {
|
||||
setSelectedTests(prev =>
|
||||
prev.includes(id)
|
||||
? prev.filter(t => t !== id)
|
||||
: [...prev, id]
|
||||
)
|
||||
}
|
||||
|
||||
const toggleAll = () => {
|
||||
if (selectedTests.length === testItems.length) {
|
||||
setSelectedTests([])
|
||||
} else {
|
||||
setSelectedTests(testItems.map(t => t.id))
|
||||
}
|
||||
}
|
||||
|
||||
const stats = {
|
||||
total: results.length,
|
||||
success: results.filter(r => r.status === 'success').length,
|
||||
failed: results.filter(r => r.status === 'failed').length,
|
||||
pending: results.filter(r => r.status === 'pending' || r.status === 'running').length,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
<h2>{t('nav.autoTest')}</h2>
|
||||
|
||||
{/* 模式选择 */}
|
||||
<div className="test-status">
|
||||
<div className="form-group">
|
||||
<label style={{ marginBottom: '12px', display: 'block' }}>
|
||||
{isZh ? '测试模式' : 'Test Mode'}
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '24px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="radio"
|
||||
name="testMode"
|
||||
checked={mode === 'wallet'}
|
||||
onChange={() => setMode('wallet')}
|
||||
disabled={isRunning}
|
||||
style={{ width: '16px', height: '16px' }}
|
||||
/>
|
||||
<span>{isZh ? '钱包模式 (需手动确认)' : 'Wallet Mode (Manual)'}</span>
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="radio"
|
||||
name="testMode"
|
||||
checked={mode === 'privateKey'}
|
||||
onChange={() => setMode('privateKey')}
|
||||
disabled={isRunning}
|
||||
style={{ width: '16px', height: '16px' }}
|
||||
/>
|
||||
<span>{isZh ? '私钥模式 (全自动)' : 'Private Key Mode (Auto)'}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mode === 'privateKey' && (
|
||||
<div className="form-group" style={{ marginTop: '16px' }}>
|
||||
<label>
|
||||
{isZh ? '测试私钥' : 'Test Private Key'}
|
||||
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#ff9800' }}>
|
||||
{isZh ? '仅用于测试网' : 'Testnet Only'}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="input"
|
||||
placeholder="0x..."
|
||||
value={privateKey}
|
||||
onChange={(e) => setPrivateKey(e.target.value)}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'wallet' && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '16px',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '6px',
|
||||
background: isConnected ? '#e8f5e9' : '#fff3e0',
|
||||
color: isConnected ? '#2e7d32' : '#e65100',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
{isConnected
|
||||
? `[OK] ${isZh ? '已连接' : 'Connected'}: ${connectedAddress?.slice(0, 6)}...${connectedAddress?.slice(-4)}`
|
||||
: `[!] ${isZh ? '请先连接钱包' : 'Please connect wallet first'}`
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 测试项选择 */}
|
||||
<div className="section" style={{ marginTop: '20px', paddingTop: '16px' }}>
|
||||
<div className="section-header">
|
||||
<h3>{isZh ? '选择测试项' : 'Select Tests'}</h3>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={toggleAll}
|
||||
disabled={isRunning}
|
||||
>
|
||||
{selectedTests.length === testItems.length
|
||||
? (isZh ? '取消全选' : 'Deselect All')
|
||||
: (isZh ? '全选' : 'Select All')
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="test-grid" style={{ marginTop: '12px' }}>
|
||||
{testItems.map(test => (
|
||||
<div
|
||||
key={test.id}
|
||||
className="test-card"
|
||||
style={{
|
||||
opacity: isRunning ? 0.7 : 1,
|
||||
cursor: isRunning ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
onClick={() => !isRunning && toggleTest(test.id)}
|
||||
>
|
||||
<div className="test-card-left">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTests.includes(test.id)}
|
||||
onChange={() => toggleTest(test.id)}
|
||||
disabled={isRunning}
|
||||
style={{ width: '14px', height: '14px' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="test-name">{test.name}</span>
|
||||
</div>
|
||||
<p className="test-desc" style={{ marginLeft: '22px' }}>{test.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', gap: '12px', marginTop: '20px' }}>
|
||||
{!isRunning ? (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={runTests}
|
||||
disabled={selectedTests.length === 0 || (mode === 'wallet' && !isConnected)}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{isZh ? `开始测试 (${selectedTests.length})` : `Start Test (${selectedTests.length})`}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={stopTests}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{isZh ? '停止测试' : 'Stop Test'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => { setLogs([]); setResults([]) }}
|
||||
disabled={isRunning}
|
||||
>
|
||||
{isZh ? '清空' : 'Clear'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 测试结果 */}
|
||||
{results.length > 0 && (
|
||||
<div className="section" style={{ marginTop: '24px', paddingTop: '20px' }}>
|
||||
<div className="section-header">
|
||||
<h3>{isZh ? '测试结果' : 'Test Results'}</h3>
|
||||
<div style={{ fontSize: '13px', color: '#666' }}>
|
||||
{isZh ? '通过' : 'Pass'}: {stats.success} |
|
||||
{isZh ? ' 失败' : ' Fail'}: {stats.failed} |
|
||||
{isZh ? ' 等待' : ' Pending'}: {stats.pending}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '12px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{results.map((result, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '6px',
|
||||
background: result.status === 'success' ? '#e8f5e9' :
|
||||
result.status === 'failed' ? '#ffebee' :
|
||||
result.status === 'running' ? '#e3f2fd' :
|
||||
'#f5f5f5',
|
||||
borderLeft: `3px solid ${
|
||||
result.status === 'success' ? '#4caf50' :
|
||||
result.status === 'failed' ? '#f44336' :
|
||||
result.status === 'running' ? '#2196f3' :
|
||||
'#ddd'
|
||||
}`,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
color: result.status === 'success' ? '#2e7d32' :
|
||||
result.status === 'failed' ? '#c62828' :
|
||||
result.status === 'running' ? '#1565c0' :
|
||||
'#666'
|
||||
}}>
|
||||
<span style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '11px',
|
||||
padding: '2px 6px',
|
||||
background: 'rgba(0,0,0,0.05)',
|
||||
borderRadius: '3px'
|
||||
}}>
|
||||
{result.status === 'pending' && '...'}
|
||||
{result.status === 'running' && 'RUN'}
|
||||
{result.status === 'success' && 'OK'}
|
||||
{result.status === 'failed' && 'FAIL'}
|
||||
</span>
|
||||
{result.name}
|
||||
</span>
|
||||
<span style={{ fontSize: '12px', color: '#666', maxWidth: '220px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{result.message}
|
||||
{result.duration && ` (${result.duration}ms)`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 日志输出 */}
|
||||
{logs.length > 0 && (
|
||||
<div className="section" style={{ marginTop: '24px', paddingTop: '20px' }}>
|
||||
<h3>{isZh ? '执行日志' : 'Execution Log'}</h3>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
background: '#1a1a1a',
|
||||
color: '#e0e0e0',
|
||||
borderRadius: '6px',
|
||||
padding: '12px 16px',
|
||||
height: '280px',
|
||||
overflowY: 'auto',
|
||||
fontFamily: 'Monaco, Consolas, monospace',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.6',
|
||||
}}
|
||||
>
|
||||
{logs.map((log, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
color: log.includes('[ERROR]') ? '#f44336' :
|
||||
log.includes('[FAIL]') ? '#ff9800' :
|
||||
log.includes('[PASS]') ? '#4caf50' :
|
||||
log.includes('[INFO]') ? '#2196f3' :
|
||||
log.includes('>>>') ? '#9e9e9e' :
|
||||
'#e0e0e0'
|
||||
}}
|
||||
>
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,9 +11,10 @@ const clearAllCache = () => {
|
||||
key.startsWith('wc@') ||
|
||||
key.startsWith('wagmi') ||
|
||||
key.startsWith('@w3m') ||
|
||||
key.startsWith('@reown') ||
|
||||
key.includes('walletconnect') ||
|
||||
key.includes('WalletConnect') ||
|
||||
key === 'yt_asset_tx_history' // 也清除可能损坏的交易历史
|
||||
key.includes('appkit')
|
||||
)) {
|
||||
keysToRemove.push(key)
|
||||
}
|
||||
@@ -25,17 +26,55 @@ const clearAllCache = () => {
|
||||
export function ConnectButton() {
|
||||
const { t } = useTranslation()
|
||||
const { open } = useWeb3Modal()
|
||||
const { address, isConnected } = useAccount()
|
||||
const { address, isConnected, connector } = useAccount()
|
||||
const { disconnect } = useDisconnect()
|
||||
|
||||
const formatAddress = (addr: string) => {
|
||||
return `${addr.slice(0, 6)}...${addr.slice(-4)}`
|
||||
}
|
||||
|
||||
// 断开连接并清理缓存
|
||||
const handleDisconnect = () => {
|
||||
disconnect()
|
||||
// 断开连接并清理缓存,然后刷新页面
|
||||
const handleDisconnect = async () => {
|
||||
try {
|
||||
disconnect()
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
clearAllCache()
|
||||
// 设置跳过自动重连标记
|
||||
sessionStorage.setItem('skipReconnect', 'true')
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// 切换账户:弹出钱包账户选择
|
||||
const handleSwitchAccount = async () => {
|
||||
try {
|
||||
// 对于 injected 钱包(MetaMask 等),使用 wallet_requestPermissions
|
||||
if (connector?.id === 'injected' || connector?.id === 'metaMask' || connector?.id === 'io.metamask') {
|
||||
const provider = await connector.getProvider() as { request?: (args: { method: string; params?: unknown[] }) => Promise<unknown> }
|
||||
if (provider?.request) {
|
||||
await provider.request({
|
||||
method: 'wallet_requestPermissions',
|
||||
params: [{ eth_accounts: {} }],
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Switch account error:', err)
|
||||
}
|
||||
// 失败或其他钱包:断开后重新连接
|
||||
try {
|
||||
disconnect()
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
clearAllCache()
|
||||
sessionStorage.setItem('skipReconnect', 'true')
|
||||
sessionStorage.setItem('autoOpenConnect', 'true')
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
// 重置连接(清理缓存后刷新)
|
||||
@@ -47,9 +86,14 @@ export function ConnectButton() {
|
||||
if (isConnected && address) {
|
||||
return (
|
||||
<div className="connect-info">
|
||||
<span className="address">{formatAddress(address)}</span>
|
||||
<button onClick={handleDisconnect} className="btn btn-secondary">
|
||||
{t('common.disconnect')}
|
||||
<span className="address" title={address}>
|
||||
{formatAddress(address)}
|
||||
</span>
|
||||
<button onClick={handleSwitchAccount} className="btn btn-outline btn-sm">
|
||||
切换
|
||||
</button>
|
||||
<button onClick={handleDisconnect} className="btn btn-secondary btn-sm">
|
||||
断开
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
|
||||
import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt, useChainId } from 'wagmi'
|
||||
import { parseUnits, formatUnits } from 'viem'
|
||||
import { CONTRACTS, GAS_CONFIG, FACTORY_ABI, VAULT_ABI } from '../config/contracts'
|
||||
import { GAS_CONFIG, FACTORY_ABI, VAULT_ABI, getContracts, getDecimals, getChainName } from '../config/contracts'
|
||||
|
||||
export function FactoryPanel() {
|
||||
const { t } = useTranslation()
|
||||
const { address, isConnected } = useAccount()
|
||||
|
||||
// ===== 多链支持 =====
|
||||
const chainId = useChainId()
|
||||
const CONTRACTS = getContracts(chainId)
|
||||
const TOKEN_DECIMALS = getDecimals(chainId)
|
||||
const currentChainName = getChainName(chainId)
|
||||
const [createForm, setCreateForm] = useState({
|
||||
name: '',
|
||||
symbol: '',
|
||||
manager: '',
|
||||
hardCap: '',
|
||||
redemptionTime: '',
|
||||
initialWusdPrice: '1',
|
||||
initialYtPrice: '1',
|
||||
})
|
||||
const [priceForm, setPriceForm] = useState({
|
||||
vault: '',
|
||||
wusdPrice: '',
|
||||
ytPrice: '',
|
||||
})
|
||||
const [showPermissionTest, setShowPermissionTest] = useState(false)
|
||||
const [showOwnerConfig, setShowOwnerConfig] = useState(false)
|
||||
const [newDefaultHardCap, setNewDefaultHardCap] = useState('')
|
||||
const [showBatchOps, setShowBatchOps] = useState(false)
|
||||
const [batchPriceForm, setBatchPriceForm] = useState({ wusdPrice: '1', ytPrice: '1' })
|
||||
const [batchPriceForm, setBatchPriceForm] = useState({ ytPrice: '1' })
|
||||
const [batchHardCapForm, setBatchHardCapForm] = useState('')
|
||||
const [selectedVaultsForBatch, setSelectedVaultsForBatch] = useState<string[]>([])
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
@@ -41,12 +45,18 @@ export function FactoryPanel() {
|
||||
manager: string
|
||||
hardCap: string
|
||||
redemptionTime: string
|
||||
wusdPrice: string
|
||||
ytPrice: string
|
||||
}[]>([
|
||||
{ name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', wusdPrice: '1', ytPrice: '1' }
|
||||
{ name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', ytPrice: '1' }
|
||||
])
|
||||
|
||||
// 单金库管理配置 state
|
||||
const [selectedVaultForManage, setSelectedVaultForManage] = useState('')
|
||||
const [singleVaultPriceForm, setSingleVaultPriceForm] = useState({ ytPrice: '1' })
|
||||
const [singleVaultHardCapForm, setSingleVaultHardCapForm] = useState('')
|
||||
const [singleVaultRedemptionTime, setSingleVaultRedemptionTime] = useState('')
|
||||
const [singleVaultManager, setSingleVaultManager] = useState('')
|
||||
|
||||
const { data: allVaults, refetch: refetchVaults } = useReadContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
@@ -138,7 +148,19 @@ export function FactoryPanel() {
|
||||
}, [writeError])
|
||||
|
||||
const handleCreateVault = () => {
|
||||
const redemptionTimestamp = Math.floor(new Date(createForm.redemptionTime).getTime() / 1000)
|
||||
// 表单验证
|
||||
if (!createForm.name || !createForm.symbol || !createForm.manager || !createForm.hardCap || !createForm.redemptionTime) {
|
||||
console.error('请填写所有必填字段')
|
||||
return
|
||||
}
|
||||
|
||||
const redemptionDate = new Date(createForm.redemptionTime)
|
||||
if (isNaN(redemptionDate.getTime())) {
|
||||
console.error('无效的赎回时间')
|
||||
return
|
||||
}
|
||||
|
||||
const redemptionTimestamp = Math.floor(redemptionDate.getTime() / 1000)
|
||||
|
||||
writeContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
@@ -148,11 +170,11 @@ export function FactoryPanel() {
|
||||
createForm.name,
|
||||
createForm.symbol,
|
||||
createForm.manager as `0x${string}`,
|
||||
parseUnits(createForm.hardCap, 18),
|
||||
CONTRACTS.WUSD,
|
||||
parseUnits(createForm.hardCap, TOKEN_DECIMALS.YT), // hardCap 是 YT 代币数量上限,使用 18 位精度
|
||||
CONTRACTS.USDC,
|
||||
BigInt(redemptionTimestamp),
|
||||
parseUnits(createForm.initialWusdPrice, 30), // wusdPrice 使用 30 位精度
|
||||
parseUnits(createForm.initialYtPrice, 30), // ytPrice 使用 30 位精度
|
||||
parseUnits(createForm.initialYtPrice, TOKEN_DECIMALS.INTERNAL_PRICE), // ytPrice 使用 30 位精度
|
||||
CONTRACTS.USDC_PRICE_FEED, // USDC 价格来源 (Chainlink)
|
||||
],
|
||||
gas: GAS_CONFIG.VERY_COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
@@ -167,8 +189,7 @@ export function FactoryPanel() {
|
||||
functionName: 'updateVaultPrices',
|
||||
args: [
|
||||
priceForm.vault as `0x${string}`,
|
||||
parseUnits(priceForm.wusdPrice, 30), // wusdPrice 使用 30 位精度
|
||||
parseUnits(priceForm.ytPrice, 30), // ytPrice 使用 30 位精度
|
||||
parseUnits(priceForm.ytPrice, TOKEN_DECIMALS.INTERNAL_PRICE), // ytPrice 使用 30 位精度 (USDC价格来自Chainlink)
|
||||
],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
@@ -182,7 +203,7 @@ export function FactoryPanel() {
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'setDefaultHardCap',
|
||||
args: [parseUnits(newDefaultHardCap, 18)],
|
||||
args: [parseUnits(newDefaultHardCap, TOKEN_DECIMALS.YT)], // hardCap 是 YT 代币数量上限,使用 18 位精度
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
@@ -192,13 +213,12 @@ export function FactoryPanel() {
|
||||
// 批量更新价格
|
||||
const handleBatchUpdatePrices = () => {
|
||||
if (selectedVaultsForBatch.length === 0) return
|
||||
const wusdPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.wusdPrice, 30)) // wusdPrice 使用 30 位精度
|
||||
const ytPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.ytPrice, 30)) // ytPrice 使用 30 位精度
|
||||
const ytPrices = selectedVaultsForBatch.map(() => parseUnits(batchPriceForm.ytPrice, TOKEN_DECIMALS.INTERNAL_PRICE)) // ytPrice 使用 30 位精度 (USDC价格来自Chainlink)
|
||||
writeContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'updateVaultPricesBatch',
|
||||
args: [selectedVaultsForBatch as `0x${string}`[], wusdPrices, ytPrices],
|
||||
args: [selectedVaultsForBatch as `0x${string}`[], ytPrices],
|
||||
gas: GAS_CONFIG.VERY_COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
@@ -208,7 +228,7 @@ export function FactoryPanel() {
|
||||
// 批量设置硬顶
|
||||
const handleBatchSetHardCap = () => {
|
||||
if (selectedVaultsForBatch.length === 0 || !batchHardCapForm) return
|
||||
const hardCaps = selectedVaultsForBatch.map(() => parseUnits(batchHardCapForm, 18))
|
||||
const hardCaps = selectedVaultsForBatch.map(() => parseUnits(batchHardCapForm, TOKEN_DECIMALS.YT)) // hardCap 是 YT 代币数量上限
|
||||
writeContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
@@ -316,16 +336,15 @@ export function FactoryPanel() {
|
||||
const names = validVaults.map(v => v.name)
|
||||
const symbols = validVaults.map(v => v.symbol)
|
||||
const managers = validVaults.map(v => v.manager as `0x${string}`)
|
||||
const hardCaps = validVaults.map(v => parseUnits(v.hardCap, 18))
|
||||
const hardCaps = validVaults.map(v => parseUnits(v.hardCap, TOKEN_DECIMALS.YT)) // hardCap 是 YT 代币数量上限
|
||||
const redemptionTimes = validVaults.map(v => BigInt(Math.floor(new Date(v.redemptionTime).getTime() / 1000)))
|
||||
const wusdPrices = validVaults.map(v => parseUnits(v.wusdPrice || '1', 30))
|
||||
const ytPrices = validVaults.map(v => parseUnits(v.ytPrice || '1', 30))
|
||||
const ytPrices = validVaults.map(v => parseUnits(v.ytPrice || '1', TOKEN_DECIMALS.INTERNAL_PRICE))
|
||||
|
||||
writeContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'createVaultBatch',
|
||||
args: [names, symbols, managers, hardCaps, CONTRACTS.WUSD, redemptionTimes, wusdPrices, ytPrices],
|
||||
args: [names, symbols, managers, hardCaps, CONTRACTS.USDC, redemptionTimes, ytPrices, CONTRACTS.USDC_PRICE_FEED],
|
||||
gas: GAS_CONFIG.VERY_COMPLEX * BigInt(validVaults.length),
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
@@ -334,7 +353,7 @@ export function FactoryPanel() {
|
||||
|
||||
// 添加新的金库配置行
|
||||
const addBatchCreateRow = () => {
|
||||
setBatchCreateVaults([...batchCreateVaults, { name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', wusdPrice: '1', ytPrice: '1' }])
|
||||
setBatchCreateVaults([...batchCreateVaults, { name: '', symbol: '', manager: '', hardCap: '100000', redemptionTime: '', ytPrice: '1' }])
|
||||
}
|
||||
|
||||
// 删除金库配置行
|
||||
@@ -393,6 +412,69 @@ export function FactoryPanel() {
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 单金库管理配置(从VaultPanel移动过来)=====
|
||||
|
||||
// 更新单个金库YT价格(USDC价格来自Chainlink,无需手动更新)
|
||||
const handleUpdateSingleVaultYTPrice = () => {
|
||||
if (!selectedVaultForManage) return
|
||||
writeContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'updateVaultPrices',
|
||||
args: [
|
||||
selectedVaultForManage as `0x${string}`,
|
||||
parseUnits(singleVaultPriceForm.ytPrice, TOKEN_DECIMALS.INTERNAL_PRICE),
|
||||
],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置单个金库硬顶
|
||||
const handleSetSingleVaultHardCap = () => {
|
||||
if (!selectedVaultForManage || !singleVaultHardCapForm) return
|
||||
const hardCap = parseUnits(singleVaultHardCapForm, TOKEN_DECIMALS.YT)
|
||||
writeContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'setHardCap',
|
||||
args: [selectedVaultForManage as `0x${string}`, hardCap],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置单个金库赎回时间
|
||||
const handleSetSingleVaultRedemptionTime = () => {
|
||||
if (!selectedVaultForManage || !singleVaultRedemptionTime) return
|
||||
const timestamp = BigInt(Math.floor(new Date(singleVaultRedemptionTime).getTime() / 1000))
|
||||
writeContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'setVaultNextRedemptionTime',
|
||||
args: [selectedVaultForManage as `0x${string}`, timestamp],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置单个金库管理员
|
||||
const handleSetSingleVaultManager = () => {
|
||||
if (!selectedVaultForManage || !singleVaultManager) return
|
||||
writeContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'setVaultManager',
|
||||
args: [selectedVaultForManage as `0x${string}`, singleVaultManager as `0x${string}`],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 权限测试函数
|
||||
const runPermissionTest = (testType: string) => {
|
||||
const testVault = allVaults && allVaults.length > 0 ? allVaults[0] : CONTRACTS.VAULTS.YT_A
|
||||
@@ -403,7 +485,7 @@ export function FactoryPanel() {
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'updateVaultPrices',
|
||||
args: [testVault as `0x${string}`, parseUnits('1', 30), parseUnits('1', 30)], // wusdPrice 和 ytPrice 都使用 30 位精度
|
||||
args: [testVault as `0x${string}`, parseUnits('1', TOKEN_DECIMALS.INTERNAL_PRICE)], // 只有 ytPrice (USDC价格来自Chainlink)
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
@@ -458,8 +540,8 @@ export function FactoryPanel() {
|
||||
<code style={{ fontSize: '10px' }}>{vaultImplementation || '-'}</code>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span title="默认硬顶 Default Hard Cap - 新创建金库的默认最大存款额度" style={{ cursor: 'help' }}>{t('factory.defaultHardCap')}:</span>
|
||||
<strong>{defaultHardCap ? formatUnits(defaultHardCap, 18) : '0'}</strong>
|
||||
<span title="默认硬顶 Default Hard Cap - 新创建金库的默认 YT 代币数量上限" style={{ cursor: 'help' }}>{t('factory.defaultHardCap')}:</span>
|
||||
<strong>{defaultHardCap ? formatUnits(defaultHardCap, TOKEN_DECIMALS.YT) : '0'}</strong>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span title="金库总数 Total Vaults - 工厂已创建的金库数量" style={{ cursor: 'help' }}>{t('factory.totalVaults')}:</span>
|
||||
@@ -486,9 +568,10 @@ export function FactoryPanel() {
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '11px',
|
||||
background: isPaused ? '#ffebee' : '#e8f5e9',
|
||||
color: isPaused ? '#c62828' : '#2e7d32',
|
||||
fontWeight: 'bold'
|
||||
background: isPaused ? '#fafafa' : '#f5f5f5',
|
||||
color: isPaused ? '#666' : '#333',
|
||||
fontWeight: 'bold',
|
||||
border: '1px solid #e0e0e0'
|
||||
}}>
|
||||
{isPaused ? t('factory.paused') : t('factory.active')}
|
||||
</span>
|
||||
@@ -573,16 +656,6 @@ export function FactoryPanel() {
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>{t('factory.initialWusdPrice')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={createForm.initialWusdPrice}
|
||||
onChange={(e) => setCreateForm({ ...createForm, initialWusdPrice: e.target.value })}
|
||||
placeholder="1"
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>{t('factory.initialYtPrice')}</label>
|
||||
<input
|
||||
@@ -673,7 +746,7 @@ export function FactoryPanel() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '10px', color: '#666' }}>{t('factory.redemptionTime')}</label>
|
||||
<input
|
||||
@@ -684,17 +757,6 @@ export function FactoryPanel() {
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '10px', color: '#666' }}>{t('vault.wusdPrice')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={vault.wusdPrice}
|
||||
onChange={(e) => updateBatchCreateRow(index, 'wusdPrice', e.target.value)}
|
||||
placeholder="1"
|
||||
className="input"
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '10px', color: '#666' }}>{t('vault.ytPrice')}</label>
|
||||
<input
|
||||
@@ -733,7 +795,7 @@ export function FactoryPanel() {
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 title="更新价格 Update Prices - 修改指定金库的 WUSD 和 YT 价格" style={{ cursor: 'help' }}>{t('factory.updatePrices')}</h3>
|
||||
<h3 title="更新价格 Update Prices - 修改指定金库的 YT 价格 (USDC 价格来自 Chainlink)" style={{ cursor: 'help' }}>{t('factory.updatePrices')}</h3>
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label title="金库地址 Vault Address - 选择要更新价格的金库" style={{ cursor: 'help' }}>{t('factory.vaultAddress')}</label>
|
||||
@@ -750,16 +812,6 @@ export function FactoryPanel() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>{t('factory.newWusdPrice')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={priceForm.wusdPrice}
|
||||
onChange={(e) => setPriceForm({ ...priceForm, wusdPrice: e.target.value })}
|
||||
placeholder="e.g. 1.05"
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>{t('factory.newYtPrice')}</label>
|
||||
<input
|
||||
@@ -794,6 +846,128 @@ export function FactoryPanel() {
|
||||
|
||||
{showOwnerConfig && (
|
||||
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
|
||||
{/* 单金库管理配置 */}
|
||||
<div style={{ marginBottom: '20px', padding: '12px', background: '#f8f9fa', borderRadius: '8px', border: '1px solid #e0e0e0' }}>
|
||||
<h5 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: 'bold', color: '#333' }}>单金库管理配置</h5>
|
||||
|
||||
{/* 选择金库 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}>选择金库</label>
|
||||
<select
|
||||
value={selectedVaultForManage}
|
||||
onChange={(e) => setSelectedVaultForManage(e.target.value)}
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
>
|
||||
<option value="">-- 请选择金库 --</option>
|
||||
{allVaults?.map((vault, index) => (
|
||||
<option key={index} value={vault}>
|
||||
Vault {index + 1}: {vault.slice(0, 10)}...
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedVaultForManage && (
|
||||
<>
|
||||
{/* 更新金库YT价格(USDC价格来自Chainlink) */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}>更新金库 YT 价格</label>
|
||||
<p style={{ fontSize: '11px', color: '#666', marginBottom: '8px' }}>
|
||||
USDC 价格由 Chainlink 预言机自动提供,无需手动更新
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>YT 价格 (30位精度)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={singleVaultPriceForm.ytPrice}
|
||||
onChange={(e) => setSingleVaultPriceForm({ ytPrice: e.target.value })}
|
||||
placeholder="1"
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleUpdateSingleVaultYTPrice}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-primary btn-sm"
|
||||
style={{ marginTop: '18px' }}
|
||||
>
|
||||
{isProcessing ? '...' : '更新价格'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设置硬顶 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}>设置硬顶</label>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<input
|
||||
type="number"
|
||||
value={singleVaultHardCapForm}
|
||||
onChange={(e) => setSingleVaultHardCapForm(e.target.value)}
|
||||
placeholder="100000"
|
||||
className="input"
|
||||
style={{ flex: 1, fontSize: '13px' }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSetSingleVaultHardCap}
|
||||
disabled={isProcessing || !singleVaultHardCapForm}
|
||||
className="btn btn-secondary btn-sm"
|
||||
>
|
||||
{isProcessing ? '...' : '设置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设置赎回时间 */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}>设置赎回时间</label>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={singleVaultRedemptionTime}
|
||||
onChange={(e) => setSingleVaultRedemptionTime(e.target.value)}
|
||||
className="input"
|
||||
style={{ flex: 1, fontSize: '13px' }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSetSingleVaultRedemptionTime}
|
||||
disabled={isProcessing || !singleVaultRedemptionTime}
|
||||
className="btn btn-secondary btn-sm"
|
||||
>
|
||||
{isProcessing ? '...' : '设置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设置管理员 */}
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}>设置管理员</label>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={singleVaultManager}
|
||||
onChange={(e) => setSingleVaultManager(e.target.value)}
|
||||
placeholder="0x..."
|
||||
className="input"
|
||||
style={{ flex: 1, fontSize: '13px' }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSetSingleVaultManager}
|
||||
disabled={isProcessing || !singleVaultManager}
|
||||
className="btn btn-secondary btn-sm"
|
||||
>
|
||||
{isProcessing ? '...' : '设置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 设置默认硬顶 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500 }}>
|
||||
@@ -804,7 +978,7 @@ export function FactoryPanel() {
|
||||
type="number"
|
||||
value={newDefaultHardCap}
|
||||
onChange={(e) => setNewDefaultHardCap(e.target.value)}
|
||||
placeholder={defaultHardCap ? formatUnits(defaultHardCap, 18) : '1000000'}
|
||||
placeholder={defaultHardCap ? formatUnits(defaultHardCap, TOKEN_DECIMALS.YT) : '1000000'}
|
||||
className="input"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
@@ -848,11 +1022,11 @@ export function FactoryPanel() {
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
padding: '4px 8px',
|
||||
background: selectedVaultsForBatch.includes(vault) ? '#e3f2fd' : '#f8f9fa',
|
||||
background: selectedVaultsForBatch.includes(vault) ? '#f5f5f5' : '#fafafa',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
border: selectedVaultsForBatch.includes(vault) ? '1px solid #2196f3' : '1px solid #e0e0e0'
|
||||
border: selectedVaultsForBatch.includes(vault) ? '1px solid #999' : '1px solid #e0e0e0'
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -872,17 +1046,8 @@ export function FactoryPanel() {
|
||||
{/* 批量更新价格 */}
|
||||
<div style={{ marginBottom: '16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
|
||||
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px' }}>{t('factory.batchUpdatePrices')}</h5>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: '8px', alignItems: 'end' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('vault.wusdPrice')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={batchPriceForm.wusdPrice}
|
||||
onChange={(e) => setBatchPriceForm({ ...batchPriceForm, wusdPrice: e.target.value })}
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
/>
|
||||
</div>
|
||||
<p style={{ fontSize: '11px', color: '#666', margin: '0 0 8px 0' }}>USDC 价格来自 Chainlink 预言机</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '8px', alignItems: 'end' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('vault.ytPrice')}</label>
|
||||
<input
|
||||
@@ -947,14 +1112,14 @@ export function FactoryPanel() {
|
||||
</div>
|
||||
|
||||
{/* 批量暂停/恢复金库 */}
|
||||
<div style={{ padding: '10px', background: '#fff3e0', borderRadius: '6px' }}>
|
||||
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px', color: '#e65100' }}>{t('factory.batchPauseUnpause')}</h5>
|
||||
<div style={{ padding: '10px', background: '#f5f5f5', borderRadius: '6px', border: '1px solid #e0e0e0' }}>
|
||||
<h5 style={{ margin: '0 0 10px 0', fontSize: '13px', color: '#666' }}>{t('factory.batchPauseUnpause')}</h5>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={handleBatchPauseVaults}
|
||||
disabled={isProcessing || selectedVaultsForBatch.length === 0}
|
||||
className="btn btn-sm"
|
||||
style={{ flex: 1, background: '#ff9800', color: '#fff', border: 'none' }}
|
||||
style={{ flex: 1, background: '#666', color: '#fff', border: 'none' }}
|
||||
>
|
||||
{isProcessing ? '...' : t('factory.pauseSelected')}
|
||||
</button>
|
||||
@@ -962,12 +1127,12 @@ export function FactoryPanel() {
|
||||
onClick={handleBatchUnpauseVaults}
|
||||
disabled={isProcessing || selectedVaultsForBatch.length === 0}
|
||||
className="btn btn-sm"
|
||||
style={{ flex: 1, background: '#4caf50', color: '#fff', border: 'none' }}
|
||||
style={{ flex: 1, background: '#333', color: '#fff', border: 'none' }}
|
||||
>
|
||||
{isProcessing ? '...' : t('factory.unpauseSelected')}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#e65100', marginTop: '6px' }}>
|
||||
<div style={{ fontSize: '11px', color: '#666', marginTop: '6px' }}>
|
||||
{t('factory.pauseWarning')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -988,7 +1153,7 @@ export function FactoryPanel() {
|
||||
{showAdvanced && (
|
||||
<div style={{ marginTop: '10px', padding: '12px', background: '#fff', borderRadius: '6px' }}>
|
||||
{/* 警告提示 */}
|
||||
<div style={{ marginBottom: '16px', padding: '8px', background: '#fff3e0', borderRadius: '4px', fontSize: '12px', color: '#e65100' }}>
|
||||
<div style={{ marginBottom: '16px', padding: '8px', background: '#f5f5f5', borderRadius: '4px', fontSize: '12px', color: '#666', border: '1px solid #e0e0e0' }}>
|
||||
{t('factory.advancedWarning')}
|
||||
</div>
|
||||
|
||||
|
||||
248
frontend/src/components/HoldersPanel.css
Normal file
248
frontend/src/components/HoldersPanel.css
Normal file
@@ -0,0 +1,248 @@
|
||||
.holders-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.holders-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.holders-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.holders-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.update-btn {
|
||||
padding: 10px 20px;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.update-btn:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.update-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.last-update {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stat-card.active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.stat-type {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-count {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 持有者列表 */
|
||||
.holders-list {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.list-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.holder-count {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.holders-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.holders-table thead {
|
||||
background: var(--color-bg-section);
|
||||
}
|
||||
|
||||
.holders-table th {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.holders-table td {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.holders-table tbody tr {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.holders-table tbody tr:hover {
|
||||
background-color: var(--color-bg-section);
|
||||
}
|
||||
|
||||
.rank {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.address a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-family: monospace;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.address a:hover {
|
||||
color: var(--color-primary-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.holding-time {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 加载和错误状态 */
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid var(--color-border);
|
||||
border-top: 4px solid var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: var(--color-warning-bg);
|
||||
border: 1px solid var(--color-warning);
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.holders-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.holders-table {
|
||||
min-width: 600px;
|
||||
}
|
||||
}
|
||||
268
frontend/src/components/HoldersPanel.tsx
Normal file
268
frontend/src/components/HoldersPanel.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatUnits } from 'viem'
|
||||
import { useChainId } from 'wagmi'
|
||||
import { getDecimals } from '../config/contracts'
|
||||
import './HoldersPanel.css'
|
||||
|
||||
interface Holder {
|
||||
id: number
|
||||
holder_address: string
|
||||
token_type: string
|
||||
token_address: string
|
||||
balance: string
|
||||
first_seen: number
|
||||
last_updated: number
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
token_type: string
|
||||
holder_count: number
|
||||
total_balance: number
|
||||
}
|
||||
|
||||
const API_BASE_URL = '/api'
|
||||
|
||||
export function HoldersPanel() {
|
||||
const { t } = useTranslation()
|
||||
// ===== 多链支持 =====
|
||||
const chainId = useChainId()
|
||||
const TOKEN_DECIMALS = getDecimals(chainId)
|
||||
|
||||
const [selectedType, setSelectedType] = useState<string>('YT-A')
|
||||
const [holders, setHolders] = useState<Holder[]>([])
|
||||
const [stats, setStats] = useState<Stats[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [lastUpdate, setLastUpdate] = useState<Date | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const tokenTypes = ['YT-A', 'YT-B', 'YT-C', 'ytLP', 'Lending']
|
||||
|
||||
// 获取统计数据
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/stats`)
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setStats(data.data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch stats:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取持有者数据
|
||||
const fetchHolders = async (type: string) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/holders/${type}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setHolders(data.data)
|
||||
setLastUpdate(new Date())
|
||||
} else {
|
||||
setError(data.error || 'Failed to fetch holders')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Network error: ' + (err as Error).message)
|
||||
console.error('Failed to fetch holders:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 手动触发更新
|
||||
const triggerUpdate = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/update`, {
|
||||
method: 'POST'
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
// 等待后端更新完成
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchHolders(selectedType),
|
||||
fetchStats()
|
||||
])
|
||||
} catch (err) {
|
||||
setError('Failed to refresh data: ' + (err as Error).message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, 5000)
|
||||
} else {
|
||||
setError(data.error || 'Update failed')
|
||||
setLoading(false)
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to trigger update: ' + (err as Error).message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载和类型切换时获取数据
|
||||
useEffect(() => {
|
||||
fetchHolders(selectedType)
|
||||
}, [selectedType])
|
||||
|
||||
// 初始加载统计数据
|
||||
useEffect(() => {
|
||||
fetchStats()
|
||||
// 每30秒刷新一次统计
|
||||
const interval = setInterval(fetchStats, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// 格式化地址(显示前6位和后4位)
|
||||
const formatAddress = (address: string) => {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
}
|
||||
|
||||
// 格式化余额
|
||||
const formatBalance = (balance: string) => {
|
||||
try {
|
||||
// ✅ 使用配置中的 YT 代币精度
|
||||
const formatted = formatUnits(BigInt(balance), TOKEN_DECIMALS.YT)
|
||||
return parseFloat(formatted).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 6
|
||||
})
|
||||
} catch {
|
||||
return balance
|
||||
}
|
||||
}
|
||||
|
||||
// 计算持有时长
|
||||
const formatHoldingTime = (firstSeen: number) => {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const duration = now - firstSeen
|
||||
|
||||
const days = Math.floor(duration / 86400)
|
||||
const hours = Math.floor((duration % 86400) / 3600)
|
||||
const minutes = Math.floor((duration % 3600) / 60)
|
||||
|
||||
if (days > 0) return `${days}${t('holders.days')} ${hours}${t('holders.hours')}`
|
||||
if (hours > 0) return `${hours}${t('holders.hours')} ${minutes}${t('holders.minutes')}`
|
||||
return `${minutes}${t('holders.minutes')}`
|
||||
}
|
||||
|
||||
// 获取某类型的统计数据
|
||||
const getStatForType = (type: string) => {
|
||||
return stats.find(s => s.token_type === type)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="holders-panel">
|
||||
<div className="holders-header">
|
||||
<h2>{t('holders.title')}</h2>
|
||||
<div className="holders-actions">
|
||||
<button
|
||||
className="update-btn"
|
||||
onClick={triggerUpdate}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('holders.updateNow')}
|
||||
</button>
|
||||
{lastUpdate && (
|
||||
<span className="last-update">
|
||||
{t('holders.lastUpdate')}: {lastUpdate.toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="stats-grid">
|
||||
{tokenTypes.map(type => {
|
||||
const stat = getStatForType(type)
|
||||
return (
|
||||
<div
|
||||
key={type}
|
||||
className={`stat-card ${selectedType === type ? 'active' : ''}`}
|
||||
onClick={() => setSelectedType(type)}
|
||||
>
|
||||
<div className="stat-type">{type}</div>
|
||||
<div className="stat-count">
|
||||
{stat?.holder_count || 0} {t('holders.holders')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 持有者列表 */}
|
||||
<div className="holders-list">
|
||||
<div className="list-header">
|
||||
<h3>{selectedType} {t('holders.holdersList')}</h3>
|
||||
<span className="holder-count">
|
||||
{t('holders.total')}: {holders.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="loading">
|
||||
<div className="spinner"></div>
|
||||
<p>{t('common.loading')}</p>
|
||||
</div>
|
||||
) : holders.length === 0 ? (
|
||||
<div className="no-data">
|
||||
<p>{t('holders.noHolders')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-container">
|
||||
<table className="holders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('holders.rank')}</th>
|
||||
<th>{t('holders.address')}</th>
|
||||
<th>{t('holders.balance')}</th>
|
||||
<th>{t('holders.holdingTime')}</th>
|
||||
<th>{t('holders.lastUpdated')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{holders.map((holder, index) => (
|
||||
<tr key={holder.id}>
|
||||
<td className="rank">#{index + 1}</td>
|
||||
<td className="address">
|
||||
<a
|
||||
href={`https://sepolia.arbiscan.io/address/${holder.holder_address}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{formatAddress(holder.holder_address)}
|
||||
</a>
|
||||
</td>
|
||||
<td className="balance">
|
||||
{formatBalance(holder.balance)}
|
||||
</td>
|
||||
<td className="holding-time">
|
||||
{formatHoldingTime(holder.first_seen)}
|
||||
</td>
|
||||
<td className="last-updated">
|
||||
{new Date(holder.last_updated * 1000).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1017
frontend/src/components/LP/LPAdminConfig.tsx
Normal file
1017
frontend/src/components/LP/LPAdminConfig.tsx
Normal file
File diff suppressed because it is too large
Load Diff
247
frontend/src/components/LP/LPBoundaryTest.tsx
Normal file
247
frontend/src/components/LP/LPBoundaryTest.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* LP 边界测试组件
|
||||
* 用于测试各种边界条件
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { parseUnits } from 'viem'
|
||||
import {
|
||||
CONTRACTS,
|
||||
GAS_CONFIG,
|
||||
TOKEN_DECIMALS,
|
||||
YT_REWARD_ROUTER_ABI,
|
||||
} from '../../config/contracts'
|
||||
import type { TransactionType } from '../../context/TransactionContext'
|
||||
|
||||
interface LPBoundaryTestProps {
|
||||
address: `0x${string}` | undefined
|
||||
ytLPBalance?: bigint
|
||||
isProcessing: boolean
|
||||
writeContract: (config: any) => void
|
||||
recordTx: (type: TransactionType, amount?: string, token?: string) => void
|
||||
show: boolean
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
export function LPBoundaryTest({
|
||||
address,
|
||||
ytLPBalance,
|
||||
isProcessing,
|
||||
writeContract,
|
||||
recordTx,
|
||||
show,
|
||||
onToggle,
|
||||
}: LPBoundaryTestProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const runBoundaryTest = (testType: string) => {
|
||||
if (!address) return
|
||||
recordTx('test', undefined, 'LP')
|
||||
|
||||
switch (testType) {
|
||||
case 'add_zero':
|
||||
// 添加流动性金额为0
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'addLiquidity',
|
||||
args: [CONTRACTS.VAULTS.YT_A, BigInt(0), BigInt(0), BigInt(0)],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
case 'add_exceed_balance':
|
||||
// 添加超过余额
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'addLiquidity',
|
||||
args: [CONTRACTS.VAULTS.YT_A, parseUnits('999999999', TOKEN_DECIMALS.YT), BigInt(0), BigInt(0)],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
case 'remove_zero':
|
||||
// 移除流动性金额为0
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'removeLiquidity',
|
||||
args: [CONTRACTS.VAULTS.YT_A, BigInt(0), BigInt(0), address],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
case 'remove_exceed_balance':
|
||||
// 移除超过ytLP余额
|
||||
const exceedAmount = ytLPBalance
|
||||
? ytLPBalance + parseUnits('999999', TOKEN_DECIMALS.YT_LP)
|
||||
: parseUnits('999999999', TOKEN_DECIMALS.YT_LP)
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'removeLiquidity',
|
||||
args: [CONTRACTS.VAULTS.YT_A, exceedAmount, BigInt(0), address],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
case 'remove_high_minout':
|
||||
// 移除时minOut过高
|
||||
const testAmount = ytLPBalance && ytLPBalance > BigInt(0)
|
||||
? (ytLPBalance > parseUnits('0.1', TOKEN_DECIMALS.YT_LP) ? parseUnits('0.1', TOKEN_DECIMALS.YT_LP) : ytLPBalance)
|
||||
: parseUnits('0.001', TOKEN_DECIMALS.YT_LP)
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'removeLiquidity',
|
||||
args: [CONTRACTS.VAULTS.YT_A, testAmount, parseUnits('999999999', TOKEN_DECIMALS.YT), address],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
case 'swap_zero':
|
||||
// 互换金额为0
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'swapYT',
|
||||
args: [CONTRACTS.VAULTS.YT_A, CONTRACTS.VAULTS.YT_B, BigInt(0), BigInt(0), address],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
case 'swap_same_token':
|
||||
// 相同代币互换
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'swapYT',
|
||||
args: [CONTRACTS.VAULTS.YT_A, CONTRACTS.VAULTS.YT_A, parseUnits('1', TOKEN_DECIMALS.YT), BigInt(0), address],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
case 'swap_exceed_balance':
|
||||
// 互换超过余额
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'swapYT',
|
||||
args: [CONTRACTS.VAULTS.YT_A, CONTRACTS.VAULTS.YT_B, parseUnits('999999999', TOKEN_DECIMALS.YT), BigInt(0), address],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px', border: '1px solid #e0e0e0' }}>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }}>{t('lp.boundaryTest')}</h4>
|
||||
<span style={{ color: '#999', fontSize: '16px' }}>{show ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
|
||||
{show && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<p style={{ fontSize: '11px', color: '#666', marginBottom: '8px' }}>
|
||||
点击按钮测试各种边界条件,预期这些测试都会失败(合约应正确拒绝)
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '8px' }}>
|
||||
{/* Add Liquidity Tests */}
|
||||
<div style={{ padding: '8px', background: '#fff', borderRadius: '4px', border: '1px solid #e0e0e0' }}>
|
||||
<div style={{ fontSize: '11px', fontWeight: 'bold', color: '#333', marginBottom: '6px' }}>添加流动性</div>
|
||||
<button
|
||||
onClick={() => runBoundaryTest('add_zero')}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-sm btn-outline"
|
||||
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
|
||||
>
|
||||
金额=0
|
||||
</button>
|
||||
<button
|
||||
onClick={() => runBoundaryTest('add_exceed_balance')}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-sm btn-outline"
|
||||
style={{ fontSize: '10px' }}
|
||||
>
|
||||
超额
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Remove Liquidity Tests */}
|
||||
<div style={{ padding: '8px', background: '#fff', borderRadius: '4px', border: '1px solid #e0e0e0' }}>
|
||||
<div style={{ fontSize: '11px', fontWeight: 'bold', color: '#333', marginBottom: '6px' }}>移除流动性</div>
|
||||
<button
|
||||
onClick={() => runBoundaryTest('remove_zero')}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-sm btn-outline"
|
||||
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
|
||||
>
|
||||
金额=0
|
||||
</button>
|
||||
<button
|
||||
onClick={() => runBoundaryTest('remove_exceed_balance')}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-sm btn-outline"
|
||||
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
|
||||
>
|
||||
超额
|
||||
</button>
|
||||
<button
|
||||
onClick={() => runBoundaryTest('remove_high_minout')}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-sm btn-outline"
|
||||
style={{ fontSize: '10px' }}
|
||||
>
|
||||
高minOut
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Swap Tests */}
|
||||
<div style={{ padding: '8px', background: '#fff', borderRadius: '4px', border: '1px solid #e0e0e0' }}>
|
||||
<div style={{ fontSize: '11px', fontWeight: 'bold', color: '#333', marginBottom: '6px' }}>交换</div>
|
||||
<button
|
||||
onClick={() => runBoundaryTest('swap_zero')}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-sm btn-outline"
|
||||
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
|
||||
>
|
||||
金额=0
|
||||
</button>
|
||||
<button
|
||||
onClick={() => runBoundaryTest('swap_same_token')}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-sm btn-outline"
|
||||
style={{ fontSize: '10px', marginRight: '4px', marginBottom: '4px' }}
|
||||
>
|
||||
同币种
|
||||
</button>
|
||||
<button
|
||||
onClick={() => runBoundaryTest('swap_exceed_balance')}
|
||||
disabled={isProcessing}
|
||||
className="btn btn-sm btn-outline"
|
||||
style={{ fontSize: '10px' }}
|
||||
>
|
||||
超额
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
153
frontend/src/components/LP/LPDebugInfo.tsx
Normal file
153
frontend/src/components/LP/LPDebugInfo.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* LP 调试信息组件
|
||||
* 显示合约地址和管理员信息
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useChainId } from 'wagmi'
|
||||
import { getContracts, getChainName } from '../../config/contracts'
|
||||
|
||||
interface LPDebugInfoProps {
|
||||
vaultGov?: string
|
||||
poolManagerGov?: string
|
||||
show: boolean
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
export function LPDebugInfo({
|
||||
vaultGov,
|
||||
poolManagerGov,
|
||||
show,
|
||||
onToggle,
|
||||
}: LPDebugInfoProps) {
|
||||
const { t } = useTranslation()
|
||||
// ===== 多链支持 =====
|
||||
const chainId = useChainId()
|
||||
const CONTRACTS = getContracts(chainId)
|
||||
const currentChainName = getChainName(chainId)
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
marginTop: '16px',
|
||||
padding: '12px 16px',
|
||||
background: '#f9f9f9',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e0e0e0',
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
title: {
|
||||
margin: 0,
|
||||
color: '#333',
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
},
|
||||
content: {
|
||||
marginTop: '16px',
|
||||
},
|
||||
section: {
|
||||
marginBottom: '16px',
|
||||
background: '#fff',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #e8e8e8',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
sectionTitle: {
|
||||
padding: '10px 12px',
|
||||
background: '#fafafa',
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
},
|
||||
sectionBody: {
|
||||
padding: '8px 0',
|
||||
},
|
||||
row: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '6px 12px',
|
||||
fontSize: '12px',
|
||||
},
|
||||
label: {
|
||||
color: '#666',
|
||||
fontWeight: 500,
|
||||
minWidth: '120px',
|
||||
},
|
||||
address: {
|
||||
fontFamily: 'monospace',
|
||||
color: '#1890ff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
wordBreak: 'break-all' as const,
|
||||
textAlign: 'right' as const,
|
||||
flex: 1,
|
||||
marginLeft: '12px',
|
||||
},
|
||||
hint: {
|
||||
fontSize: '11px',
|
||||
color: '#999',
|
||||
textAlign: 'center' as const,
|
||||
padding: '8px',
|
||||
},
|
||||
}
|
||||
|
||||
const AddressRow = ({ label, address }: { label: string; address?: string }) => (
|
||||
<div style={styles.row}>
|
||||
<span style={styles.label}>{label}</span>
|
||||
<span
|
||||
style={styles.address}
|
||||
onClick={() => address && copyToClipboard(address)}
|
||||
title={address ? '点击复制完整地址' : ''}
|
||||
>
|
||||
{address || 'Loading...'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.header} onClick={onToggle}>
|
||||
<h4 style={styles.title}>{t('lp.debugInfo')}</h4>
|
||||
<span style={{ color: '#999', fontSize: '12px' }}>{show ? '收起' : '展开'}</span>
|
||||
</div>
|
||||
|
||||
{show && (
|
||||
<div style={styles.content}>
|
||||
{/* 合约地址 */}
|
||||
<div style={styles.section}>
|
||||
<div style={styles.sectionTitle}>合约地址</div>
|
||||
<div style={styles.sectionBody}>
|
||||
<AddressRow label="YTRewardRouter" address={CONTRACTS.YT_REWARD_ROUTER} />
|
||||
<AddressRow label="YTLPToken" address={CONTRACTS.YT_LP_TOKEN} />
|
||||
<AddressRow label="YTPoolManager" address={CONTRACTS.YT_POOL_MANAGER} />
|
||||
<AddressRow label="YTVault" address={CONTRACTS.YT_VAULT} />
|
||||
<AddressRow label="USDY" address={CONTRACTS.USDY} />
|
||||
<AddressRow label="USDC" address={CONTRACTS.USDC} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 管理员地址 */}
|
||||
<div style={{ ...styles.section, marginBottom: 0 }}>
|
||||
<div style={styles.sectionTitle}>管理员地址</div>
|
||||
<div style={styles.sectionBody}>
|
||||
<AddressRow label="PoolManager Gov" address={poolManagerGov} />
|
||||
<AddressRow label="Vault Gov" address={vaultGov} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.hint}>点击地址可复制到剪贴板</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
882
frontend/src/components/LP/LPOperations.tsx
Normal file
882
frontend/src/components/LP/LPOperations.tsx
Normal file
@@ -0,0 +1,882 @@
|
||||
/**
|
||||
* LP 操作组件
|
||||
* 包含添加流动性、移除流动性、代币交换功能
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { parseUnits, formatUnits } from 'viem'
|
||||
import { useReadContract, useChainId } from 'wagmi'
|
||||
import {
|
||||
CONTRACTS,
|
||||
GAS_CONFIG,
|
||||
TOKEN_DECIMALS,
|
||||
getTokenDecimals,
|
||||
getDecimals,
|
||||
YT_REWARD_ROUTER_ABI,
|
||||
YT_VAULT_ABI,
|
||||
USDC_ABI,
|
||||
YT_POOL_MANAGER_ABI,
|
||||
} from '../../config/contracts'
|
||||
import type { TransactionType } from '../../context/TransactionContext'
|
||||
import type { ExtendedPoolToken } from './useLPPoolData'
|
||||
import type { AddLiquidityForm, RemoveLiquidityForm, SwapForm } from './types'
|
||||
|
||||
interface LPOperationsProps {
|
||||
address: `0x${string}` | undefined
|
||||
poolTokens: ExtendedPoolToken[]
|
||||
ytLPBalance?: bigint
|
||||
ytLPPrice?: bigint
|
||||
isProcessing: boolean
|
||||
writeContract: (config: any) => void
|
||||
recordTx: (type: TransactionType, amount?: string, token?: string) => void
|
||||
getTokenSymbol: (address: string) => string
|
||||
// 手续费信息
|
||||
swapFee?: bigint
|
||||
taxBasisPoints?: bigint
|
||||
dynamicFees?: boolean
|
||||
}
|
||||
|
||||
export function LPOperations({
|
||||
address,
|
||||
poolTokens,
|
||||
ytLPBalance,
|
||||
ytLPPrice,
|
||||
isProcessing,
|
||||
writeContract,
|
||||
recordTx,
|
||||
getTokenSymbol,
|
||||
swapFee,
|
||||
taxBasisPoints,
|
||||
dynamicFees,
|
||||
}: LPOperationsProps) {
|
||||
const { t } = useTranslation()
|
||||
// ===== 多链支持 =====
|
||||
const chainId = useChainId()
|
||||
const TOKEN_DECIMALS_DYNAMIC = getDecimals(chainId)
|
||||
|
||||
// Tab 状态
|
||||
const [activeTab, setActiveTab] = useState<'add' | 'remove' | 'swap'>('add')
|
||||
|
||||
// 表单状态
|
||||
const [addLiquidityForm, setAddLiquidityForm] = useState<AddLiquidityForm>({
|
||||
token: poolTokens.find(t => t.isWhitelisted)?.address || '',
|
||||
amount: '',
|
||||
slippage: '0.5',
|
||||
})
|
||||
|
||||
const [removeLiquidityForm, setRemoveLiquidityForm] = useState<RemoveLiquidityForm>({
|
||||
token: poolTokens.find(t => t.isWhitelisted)?.address || '',
|
||||
amount: '',
|
||||
slippage: '1',
|
||||
})
|
||||
|
||||
const [swapForm, setSwapForm] = useState<SwapForm>({
|
||||
tokenIn: poolTokens.find(t => t.isWhitelisted)?.address || '',
|
||||
tokenOut: poolTokens.filter(t => t.isWhitelisted)[1]?.address || '',
|
||||
amount: '',
|
||||
slippage: '0.5',
|
||||
})
|
||||
|
||||
// 白名单代币
|
||||
const whitelistedTokens = useMemo(() =>
|
||||
poolTokens.filter(t => t.isWhitelisted),
|
||||
[poolTokens]
|
||||
)
|
||||
|
||||
// 当 poolTokens 加载完成后,自动设置默认代币
|
||||
useEffect(() => {
|
||||
if (whitelistedTokens.length > 0) {
|
||||
// 添加流动性:如果当前没有选中有效代币,设置第一个白名单代币
|
||||
if (!addLiquidityForm.token || !whitelistedTokens.find(t => t.address === addLiquidityForm.token)) {
|
||||
setAddLiquidityForm(prev => ({ ...prev, token: whitelistedTokens[0].address }))
|
||||
}
|
||||
// 移除流动性
|
||||
if (!removeLiquidityForm.token || !whitelistedTokens.find(t => t.address === removeLiquidityForm.token)) {
|
||||
setRemoveLiquidityForm(prev => ({ ...prev, token: whitelistedTokens[0].address }))
|
||||
}
|
||||
// Swap
|
||||
if (!swapForm.tokenIn || !whitelistedTokens.find(t => t.address === swapForm.tokenIn)) {
|
||||
setSwapForm(prev => ({ ...prev, tokenIn: whitelistedTokens[0].address }))
|
||||
}
|
||||
if (!swapForm.tokenOut || !whitelistedTokens.find(t => t.address === swapForm.tokenOut)) {
|
||||
const secondToken = whitelistedTokens[1]?.address || whitelistedTokens[0]?.address || ''
|
||||
setSwapForm(prev => ({ ...prev, tokenOut: secondToken }))
|
||||
}
|
||||
}
|
||||
}, [whitelistedTokens, addLiquidityForm.token, removeLiquidityForm.token, swapForm.tokenIn, swapForm.tokenOut])
|
||||
|
||||
// 验证代币地址是否有效
|
||||
const isValidAddToken = !!(addLiquidityForm.token && addLiquidityForm.token.length === 42 && addLiquidityForm.token.startsWith('0x'))
|
||||
const isValidRemoveToken = !!(removeLiquidityForm.token && removeLiquidityForm.token.length === 42 && removeLiquidityForm.token.startsWith('0x'))
|
||||
const isValidSwapTokenIn = !!(swapForm.tokenIn && swapForm.tokenIn.length === 42 && swapForm.tokenIn.startsWith('0x'))
|
||||
const isValidSwapTokenOut = !!(swapForm.tokenOut && swapForm.tokenOut.length === 42 && swapForm.tokenOut.startsWith('0x'))
|
||||
|
||||
// 追踪 isProcessing 变化,用于在交易完成后刷新授权
|
||||
const prevIsProcessingRef = useRef(isProcessing)
|
||||
|
||||
// 读取授权额度
|
||||
const { data: tokenAllowance, refetch: refetchTokenAllowance } = useReadContract({
|
||||
address: isValidAddToken ? (addLiquidityForm.token as `0x${string}`) : undefined,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'allowance',
|
||||
args: address && isValidAddToken ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined,
|
||||
query: {
|
||||
enabled: !!address && isValidAddToken,
|
||||
},
|
||||
})
|
||||
|
||||
const { data: ytLPAllowance, refetch: refetchYtLPAllowance } = useReadContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'allowance',
|
||||
args: address ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined,
|
||||
query: {
|
||||
enabled: !!address,
|
||||
},
|
||||
})
|
||||
|
||||
const { data: swapAllowance, refetch: refetchSwapAllowance } = useReadContract({
|
||||
address: isValidSwapTokenIn ? (swapForm.tokenIn as `0x${string}`) : undefined,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'allowance',
|
||||
args: address && isValidSwapTokenIn ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined,
|
||||
query: {
|
||||
enabled: !!address && isValidSwapTokenIn,
|
||||
},
|
||||
})
|
||||
|
||||
// 交易完成后刷新授权额度 (isProcessing 从 true 变为 false)
|
||||
useEffect(() => {
|
||||
if (prevIsProcessingRef.current && !isProcessing) {
|
||||
// 交易完成,刷新所有授权额度
|
||||
setTimeout(() => {
|
||||
refetchTokenAllowance()
|
||||
refetchYtLPAllowance()
|
||||
refetchSwapAllowance()
|
||||
}, 1000) // 延迟1秒确保链上状态已更新
|
||||
}
|
||||
prevIsProcessingRef.current = isProcessing
|
||||
}, [isProcessing, refetchTokenAllowance, refetchYtLPAllowance, refetchSwapAllowance])
|
||||
|
||||
// 读取添加流动性代币价格
|
||||
const { data: addLiquidityTokenPrice } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'getMinPrice',
|
||||
args: isValidAddToken ? [addLiquidityForm.token as `0x${string}`] : undefined,
|
||||
query: {
|
||||
enabled: isValidAddToken,
|
||||
},
|
||||
})
|
||||
|
||||
// 读取移除流动性代币价格
|
||||
const { data: removeLiquidityTokenMaxPrice } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'getMaxPrice',
|
||||
args: isValidRemoveToken ? [removeLiquidityForm.token as `0x${string}`] : undefined,
|
||||
query: {
|
||||
enabled: isValidRemoveToken,
|
||||
},
|
||||
})
|
||||
|
||||
// 读取 Swap tokenIn 最大价格 (用于卖出)
|
||||
const { data: swapTokenInMaxPrice } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'getMaxPrice',
|
||||
args: isValidSwapTokenIn ? [swapForm.tokenIn as `0x${string}`] : undefined,
|
||||
query: {
|
||||
enabled: isValidSwapTokenIn,
|
||||
},
|
||||
})
|
||||
|
||||
// 读取 Swap tokenOut 最小价格 (用于买入)
|
||||
const { data: swapTokenOutMinPrice } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'getMinPrice',
|
||||
args: isValidSwapTokenOut ? [swapForm.tokenOut as `0x${string}`] : undefined,
|
||||
query: {
|
||||
enabled: isValidSwapTokenOut,
|
||||
},
|
||||
})
|
||||
|
||||
// 检查 RewardRouter 是否被设置为 Swapper
|
||||
const { data: isRewardRouterSwapper } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'isSwapper',
|
||||
args: [CONTRACTS.YT_REWARD_ROUTER],
|
||||
})
|
||||
|
||||
// 检查 RewardRouter 是否被设置为 PoolManager 的 Handler
|
||||
const { data: isRewardRouterHandler } = useReadContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'isHandler',
|
||||
args: [CONTRACTS.YT_REWARD_ROUTER],
|
||||
})
|
||||
|
||||
// 读取 maxSwapAmount 限制
|
||||
const { data: maxSwapAmountIn } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'maxSwapAmounts',
|
||||
args: isValidSwapTokenIn ? [swapForm.tokenIn as `0x${string}`] : undefined,
|
||||
query: {
|
||||
enabled: isValidSwapTokenIn,
|
||||
},
|
||||
})
|
||||
|
||||
// 读取 isSwapEnabled 状态
|
||||
const { data: isSwapEnabled } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'isSwapEnabled',
|
||||
})
|
||||
|
||||
// 读取池子中 tokenOut 的余额
|
||||
const { data: poolAmountOut } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'poolAmounts',
|
||||
args: isValidSwapTokenOut ? [swapForm.tokenOut as `0x${string}`] : undefined,
|
||||
query: {
|
||||
enabled: isValidSwapTokenOut,
|
||||
},
|
||||
})
|
||||
|
||||
// 读取池子中 tokenIn 的 usdyAmount
|
||||
const { data: usdyAmountIn } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'usdyAmounts',
|
||||
args: isValidSwapTokenIn ? [swapForm.tokenIn as `0x${string}`] : undefined,
|
||||
query: {
|
||||
enabled: isValidSwapTokenIn,
|
||||
},
|
||||
})
|
||||
|
||||
// 读取池子中 tokenOut 的 usdyAmount
|
||||
const { data: usdyAmountOut } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'usdyAmounts',
|
||||
args: isValidSwapTokenOut ? [swapForm.tokenOut as `0x${string}`] : undefined,
|
||||
query: {
|
||||
enabled: isValidSwapTokenOut,
|
||||
},
|
||||
})
|
||||
|
||||
// 计算添加流动性预估输出
|
||||
const addLiquidityPreviewAmount = useMemo(() => {
|
||||
if (!addLiquidityForm.amount || !addLiquidityTokenPrice || !ytLPPrice) return null
|
||||
if (ytLPPrice === 0n) return null
|
||||
try {
|
||||
const tokenDecimals = getTokenDecimals(addLiquidityForm.token)
|
||||
const amountIn = parseUnits(addLiquidityForm.amount, tokenDecimals)
|
||||
const PRECISION_DIFF = 10n ** 12n
|
||||
const ytLPAmount = (amountIn * addLiquidityTokenPrice) / ytLPPrice / PRECISION_DIFF
|
||||
return ytLPAmount
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [addLiquidityForm.amount, addLiquidityForm.token, addLiquidityTokenPrice, ytLPPrice])
|
||||
|
||||
// 计算移除流动性预估输出
|
||||
const removeLiquidityPreviewAmount = useMemo(() => {
|
||||
if (!removeLiquidityForm.amount || !ytLPPrice || !removeLiquidityTokenMaxPrice) return null
|
||||
if (removeLiquidityTokenMaxPrice === 0n) return null
|
||||
try {
|
||||
const ytLPAmount = parseUnits(removeLiquidityForm.amount, TOKEN_DECIMALS.YT_LP)
|
||||
const PRECISION_DIFF = 10n ** 12n
|
||||
const tokenAmount = (ytLPAmount * ytLPPrice * PRECISION_DIFF) / removeLiquidityTokenMaxPrice
|
||||
return tokenAmount
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [removeLiquidityForm.amount, ytLPPrice, removeLiquidityTokenMaxPrice])
|
||||
|
||||
// 检查是否需要授权
|
||||
const needsApproval = () => {
|
||||
if (!addLiquidityForm.amount) return false
|
||||
try {
|
||||
const amount = parseUnits(addLiquidityForm.amount, getTokenDecimals(addLiquidityForm.token))
|
||||
if (tokenAllowance === undefined || tokenAllowance === null) return true
|
||||
return tokenAllowance < amount
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const needsYtLPApproval = () => {
|
||||
if (!removeLiquidityForm.amount) return false
|
||||
try {
|
||||
const amount = parseUnits(removeLiquidityForm.amount, TOKEN_DECIMALS.YT_LP)
|
||||
if (ytLPAllowance === undefined || ytLPAllowance === null) return true
|
||||
return ytLPAllowance < amount
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const needsSwapApproval = () => {
|
||||
if (!swapForm.amount) return false
|
||||
try {
|
||||
const amount = parseUnits(swapForm.amount, getTokenDecimals(swapForm.tokenIn))
|
||||
if (swapAllowance === undefined || swapAllowance === null) return true
|
||||
return swapAllowance < amount
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理函数
|
||||
const handleApproveToken = () => {
|
||||
if (!address || !addLiquidityForm.amount || !isValidAddToken) return
|
||||
const tokenDecimals = getTokenDecimals(addLiquidityForm.token)
|
||||
const amount = parseUnits(addLiquidityForm.amount, tokenDecimals)
|
||||
recordTx('approve', addLiquidityForm.amount, getTokenSymbol(addLiquidityForm.token))
|
||||
writeContract({
|
||||
address: addLiquidityForm.token as `0x${string}`,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'approve',
|
||||
args: [CONTRACTS.YT_REWARD_ROUTER, amount],
|
||||
gas: GAS_CONFIG.SIMPLE,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
const handleApproveYtLP = () => {
|
||||
if (!address || !removeLiquidityForm.amount) return
|
||||
const amount = parseUnits(removeLiquidityForm.amount, TOKEN_DECIMALS.YT_LP)
|
||||
recordTx('approve', removeLiquidityForm.amount, 'ytLP')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'approve',
|
||||
args: [CONTRACTS.YT_REWARD_ROUTER, amount],
|
||||
gas: GAS_CONFIG.SIMPLE,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
const handleApproveSwapToken = () => {
|
||||
if (!address || !swapForm.amount || !isValidSwapTokenIn) return
|
||||
const tokenDecimals = getTokenDecimals(swapForm.tokenIn)
|
||||
const amount = parseUnits(swapForm.amount, tokenDecimals)
|
||||
recordTx('approve', swapForm.amount, getTokenSymbol(swapForm.tokenIn))
|
||||
writeContract({
|
||||
address: swapForm.tokenIn as `0x${string}`,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'approve',
|
||||
args: [CONTRACTS.YT_REWARD_ROUTER, amount],
|
||||
gas: GAS_CONFIG.SIMPLE,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
const handleAddLiquidity = () => {
|
||||
if (!address || !addLiquidityForm.amount || !isValidAddToken) return
|
||||
const tokenDecimals = getTokenDecimals(addLiquidityForm.token)
|
||||
const amount = parseUnits(addLiquidityForm.amount, tokenDecimals)
|
||||
const slippageBps = Math.floor(Number(addLiquidityForm.slippage) * 100)
|
||||
|
||||
// 基于预期的 ytLP 输出计算 minOut,而不是输入金额
|
||||
let minOut = BigInt(0)
|
||||
if (addLiquidityPreviewAmount && addLiquidityPreviewAmount > 0n) {
|
||||
minOut = (addLiquidityPreviewAmount * BigInt(10000 - slippageBps)) / BigInt(10000)
|
||||
}
|
||||
|
||||
recordTx('addLiquidity', addLiquidityForm.amount, getTokenSymbol(addLiquidityForm.token))
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'addLiquidity',
|
||||
args: [addLiquidityForm.token as `0x${string}`, amount, minOut, BigInt(0)],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveLiquidity = () => {
|
||||
if (!address || !removeLiquidityForm.amount || !isValidRemoveToken) return
|
||||
const amount = parseUnits(removeLiquidityForm.amount, TOKEN_DECIMALS.YT_LP)
|
||||
const slippageBps = Math.floor(Number(removeLiquidityForm.slippage) * 100)
|
||||
|
||||
// 基于预期的代币输出计算 minOut
|
||||
let minOut = BigInt(0)
|
||||
if (removeLiquidityPreviewAmount && removeLiquidityPreviewAmount > 0n) {
|
||||
minOut = (removeLiquidityPreviewAmount * BigInt(10000 - slippageBps)) / BigInt(10000)
|
||||
}
|
||||
|
||||
recordTx('removeLiquidity', removeLiquidityForm.amount, 'ytLP')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'removeLiquidity',
|
||||
args: [removeLiquidityForm.token as `0x${string}`, amount, minOut, address],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
const handleSwap = () => {
|
||||
if (!address || !swapForm.amount || !isValidSwapTokenIn || !isValidSwapTokenOut) return
|
||||
const tokenInDecimals = getTokenDecimals(swapForm.tokenIn)
|
||||
const tokenOutDecimals = getTokenDecimals(swapForm.tokenOut)
|
||||
const amount = parseUnits(swapForm.amount, tokenInDecimals)
|
||||
const slippageBps = Math.floor(Number(swapForm.slippage) * 100)
|
||||
|
||||
// 基于预期的输出计算 minOut
|
||||
let minOut = BigInt(0)
|
||||
if (swapTokenInMaxPrice && swapTokenOutMinPrice && swapTokenOutMinPrice > 0n) {
|
||||
// 计算预期输出: amountOut = (amountIn * tokenInMaxPrice) / tokenOutMinPrice
|
||||
const expectedOut = (amount * swapTokenInMaxPrice) / swapTokenOutMinPrice
|
||||
// 调整精度差异
|
||||
const precisionDiff = tokenInDecimals - tokenOutDecimals
|
||||
const adjustedOut = precisionDiff > 0
|
||||
? expectedOut / (10n ** BigInt(precisionDiff))
|
||||
: expectedOut * (10n ** BigInt(-precisionDiff))
|
||||
minOut = (adjustedOut * BigInt(10000 - slippageBps)) / BigInt(10000)
|
||||
}
|
||||
|
||||
recordTx('swap', swapForm.amount, `${getTokenSymbol(swapForm.tokenIn)} → ${getTokenSymbol(swapForm.tokenOut)}`)
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'swapYT',
|
||||
args: [
|
||||
swapForm.tokenIn as `0x${string}`,
|
||||
swapForm.tokenOut as `0x${string}`,
|
||||
amount,
|
||||
minOut,
|
||||
address,
|
||||
],
|
||||
gas: GAS_CONFIG.COMPLEX,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Tab Navigation */}
|
||||
<div className="lp-tabs">
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'add' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('add')}
|
||||
>
|
||||
{t('lp.addLiquidity')}
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'remove' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('remove')}
|
||||
>
|
||||
{t('lp.removeLiquidity')}
|
||||
</button>
|
||||
<button
|
||||
className={`tab-btn ${activeTab === 'swap' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('swap')}
|
||||
>
|
||||
{t('lp.swapTokens')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add Liquidity Form */}
|
||||
{activeTab === 'add' && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<p className="text-muted" style={{ marginBottom: '8px', fontSize: '12px' }}>{t('lp.addLiquidityDesc')}</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 2fr 1fr', gap: '8px', marginBottom: '8px' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.selectToken')}</label>
|
||||
<select
|
||||
value={addLiquidityForm.token}
|
||||
onChange={(e) => setAddLiquidityForm({ ...addLiquidityForm, token: e.target.value })}
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
>
|
||||
{whitelistedTokens.map((token) => (
|
||||
<option key={token.address} value={token.address}>
|
||||
{token.symbol} - {token.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.amount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={addLiquidityForm.amount}
|
||||
onChange={(e) => setAddLiquidityForm({ ...addLiquidityForm, amount: e.target.value })}
|
||||
placeholder="0.0"
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.slippage')} (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={addLiquidityForm.slippage}
|
||||
onChange={(e) => setAddLiquidityForm({ ...addLiquidityForm, slippage: e.target.value })}
|
||||
placeholder="0.5"
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 预估输出 */}
|
||||
{addLiquidityForm.amount && addLiquidityPreviewAmount && (
|
||||
<div style={{ marginBottom: '12px', padding: '10px', background: '#f8f9fa', borderRadius: '6px', fontSize: '13px' }}>
|
||||
<span style={{ color: '#666' }}>{t('vault.youWillReceive')}: </span>
|
||||
<strong style={{ color: '#333', fontSize: '15px' }}>
|
||||
{formatUnits(addLiquidityPreviewAmount, TOKEN_DECIMALS.YT_LP)} ytLP
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 调试信息 */}
|
||||
{addLiquidityForm.amount && (
|
||||
<div style={{ marginBottom: '12px', padding: '10px', background: '#e8f4e8', borderRadius: '6px', fontSize: '11px', fontFamily: 'monospace' }}>
|
||||
<div style={{ marginBottom: '4px', color: '#666' }}>
|
||||
<strong>代币:</strong>{' '}
|
||||
{getTokenSymbol(addLiquidityForm.token)} ({addLiquidityForm.token ? `${addLiquidityForm.token.slice(0, 6)}...${addLiquidityForm.token.slice(-4)}` : '未选择'}) |{' '}
|
||||
有效: {isValidAddToken ? <span style={{ color: '#4caf50' }}>是</span> : <span style={{ color: '#f44336' }}>否</span>}
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#666' }}>
|
||||
<strong>授权信息:</strong>{' '}
|
||||
授权额度: {tokenAllowance !== undefined ? formatUnits(tokenAllowance, getTokenDecimals(addLiquidityForm.token)) : '加载中'} {getTokenSymbol(addLiquidityForm.token)} |{' '}
|
||||
需要: {addLiquidityForm.amount} |{' '}
|
||||
需授权: {needsApproval() ? <span style={{ color: '#f44336' }}>是</span> : <span style={{ color: '#4caf50' }}>否</span>} |{' '}
|
||||
处理中: {isProcessing ? <span style={{ color: '#f44336' }}>是</span> : <span style={{ color: '#4caf50' }}>否</span>}
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#555' }}>
|
||||
<strong>价格信息:</strong>{' '}
|
||||
tokenMinPrice: {addLiquidityTokenPrice?.toString() || 'N/A'} (显示: {addLiquidityTokenPrice ? (Number(addLiquidityTokenPrice) / 1e30).toFixed(8) : 'N/A'})
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#555' }}>
|
||||
ytLPPrice: {ytLPPrice?.toString() || 'N/A'} (显示: {ytLPPrice ? (Number(ytLPPrice) / 1e30).toFixed(8) : 'N/A'})
|
||||
</div>
|
||||
{addLiquidityPreviewAmount && (
|
||||
<div style={{ color: '#555' }}>
|
||||
<strong>计算:</strong>{' '}
|
||||
预估输出: {formatUnits(addLiquidityPreviewAmount, TOKEN_DECIMALS.YT_LP)} ytLP |{' '}
|
||||
minOut (扣滑点): {formatUnits((addLiquidityPreviewAmount * BigInt(10000 - Math.floor(Number(addLiquidityForm.slippage) * 100))) / BigInt(10000), TOKEN_DECIMALS.YT_LP)} ytLP
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 按钮 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
|
||||
{needsApproval() ? (
|
||||
<button onClick={handleApproveToken} disabled={isProcessing} className="btn btn-secondary btn-sm">
|
||||
{isProcessing ? '...' : t('lp.approveToken')}
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={handleAddLiquidity} disabled={isProcessing || !addLiquidityForm.amount} className="btn btn-primary btn-sm">
|
||||
{isProcessing ? '...' : t('lp.addLiquidity')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remove Liquidity Form */}
|
||||
{activeTab === 'remove' && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<p className="text-muted" style={{ marginBottom: '8px', fontSize: '12px' }}>{t('lp.removeLiquidityDesc')}</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '2fr 2fr 1fr', gap: '8px', marginBottom: '8px' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.outputToken')}</label>
|
||||
<select
|
||||
value={removeLiquidityForm.token}
|
||||
onChange={(e) => setRemoveLiquidityForm({ ...removeLiquidityForm, token: e.target.value })}
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
>
|
||||
{whitelistedTokens.map((token) => (
|
||||
<option key={token.address} value={token.address}>
|
||||
{token.symbol} - {token.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.ytLPAmount')}</label>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<input
|
||||
type="number"
|
||||
value={removeLiquidityForm.amount}
|
||||
onChange={(e) => setRemoveLiquidityForm({ ...removeLiquidityForm, amount: e.target.value })}
|
||||
placeholder="0.0"
|
||||
className="input"
|
||||
style={{ fontSize: '13px', flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm btn-link"
|
||||
onClick={() => setRemoveLiquidityForm({
|
||||
...removeLiquidityForm,
|
||||
amount: ytLPBalance ? formatUnits(ytLPBalance, TOKEN_DECIMALS.YT_LP) : '0'
|
||||
})}
|
||||
style={{ fontSize: '10px', padding: '2px 6px' }}
|
||||
>
|
||||
MAX
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.slippage')} (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={removeLiquidityForm.slippage}
|
||||
onChange={(e) => setRemoveLiquidityForm({ ...removeLiquidityForm, slippage: e.target.value })}
|
||||
placeholder="1"
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 预估输出 */}
|
||||
{removeLiquidityForm.amount && removeLiquidityPreviewAmount && (
|
||||
<div style={{ marginBottom: '12px', padding: '10px', background: '#f8f9fa', borderRadius: '6px', fontSize: '13px' }}>
|
||||
<span style={{ color: '#666' }}>{t('vault.youWillReceive')}: </span>
|
||||
<strong style={{ color: '#333', fontSize: '15px' }}>
|
||||
{formatUnits(removeLiquidityPreviewAmount, getTokenDecimals(removeLiquidityForm.token))} {getTokenSymbol(removeLiquidityForm.token)}
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 调试信息 */}
|
||||
{removeLiquidityForm.amount && (
|
||||
<div style={{ marginBottom: '12px', padding: '10px', background: '#fff3e0', borderRadius: '6px', fontSize: '11px', fontFamily: 'monospace' }}>
|
||||
<div style={{ marginBottom: '4px', color: '#666' }}>
|
||||
<strong>授权信息:</strong>{' '}
|
||||
ytLP授权额度: {ytLPAllowance !== undefined ? formatUnits(ytLPAllowance, TOKEN_DECIMALS.YT_LP) : '加载中'} ytLP |{' '}
|
||||
需要: {removeLiquidityForm.amount} |{' '}
|
||||
需授权: {needsYtLPApproval() ? <span style={{ color: '#f44336' }}>是</span> : <span style={{ color: '#4caf50' }}>否</span>} |{' '}
|
||||
处理中: {isProcessing ? <span style={{ color: '#f44336' }}>是</span> : <span style={{ color: '#4caf50' }}>否</span>}
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#555' }}>
|
||||
<strong>余额信息:</strong>{' '}
|
||||
ytLP余额: {ytLPBalance !== undefined ? formatUnits(ytLPBalance, TOKEN_DECIMALS.YT_LP) : 'N/A'} ytLP
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#555' }}>
|
||||
<strong>价格信息:</strong>{' '}
|
||||
tokenMaxPrice: {removeLiquidityTokenMaxPrice?.toString() || 'N/A'} (显示: {removeLiquidityTokenMaxPrice ? (Number(removeLiquidityTokenMaxPrice) / 1e30).toFixed(8) : 'N/A'})
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#555' }}>
|
||||
ytLPPrice: {ytLPPrice?.toString() || 'N/A'} (显示: {ytLPPrice ? (Number(ytLPPrice) / 1e30).toFixed(8) : 'N/A'})
|
||||
</div>
|
||||
{removeLiquidityPreviewAmount && (
|
||||
<div style={{ color: '#555' }}>
|
||||
<strong>计算:</strong>{' '}
|
||||
预估输出原始值: {removeLiquidityPreviewAmount.toString()} |{' '}
|
||||
公式: (ytLPAmount × ytLPPrice × 1e12) / tokenMaxPrice
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 按钮 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
|
||||
{needsYtLPApproval() ? (
|
||||
<button onClick={handleApproveYtLP} disabled={isProcessing} className="btn btn-secondary btn-sm">
|
||||
{isProcessing ? '...' : t('lp.approveYtLP')}
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={handleRemoveLiquidity} disabled={isProcessing || !removeLiquidityForm.amount} className="btn btn-primary btn-sm">
|
||||
{isProcessing ? '...' : t('lp.removeLiquidity')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Swap Form */}
|
||||
{activeTab === 'swap' && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<p className="text-muted" style={{ marginBottom: '8px', fontSize: '12px' }}>{t('lp.swapDesc')}</p>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '8px', marginBottom: '8px' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.fromToken')}</label>
|
||||
<select
|
||||
value={swapForm.tokenIn}
|
||||
onChange={(e) => setSwapForm({ ...swapForm, tokenIn: e.target.value })}
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
>
|
||||
{whitelistedTokens.map((token) => (
|
||||
<option key={token.address} value={token.address}>
|
||||
{token.symbol}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.toToken')}</label>
|
||||
<select
|
||||
value={swapForm.tokenOut}
|
||||
onChange={(e) => setSwapForm({ ...swapForm, tokenOut: e.target.value })}
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
>
|
||||
{whitelistedTokens.map((token) => (
|
||||
<option key={token.address} value={token.address}>
|
||||
{token.symbol}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.amount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={swapForm.amount}
|
||||
onChange={(e) => setSwapForm({ ...swapForm, amount: e.target.value })}
|
||||
placeholder="0.0"
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: '11px', color: '#666' }}>{t('lp.slippage')} (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={swapForm.slippage}
|
||||
onChange={(e) => setSwapForm({ ...swapForm, slippage: e.target.value })}
|
||||
placeholder="0.5"
|
||||
className="input"
|
||||
style={{ fontSize: '13px' }}
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 警告 */}
|
||||
{swapForm.tokenIn === swapForm.tokenOut && (
|
||||
<div style={{ marginBottom: '8px', padding: '8px', background: '#f5f5f5', borderRadius: '6px', fontSize: '11px', color: '#666', border: '1px solid #e0e0e0' }}>
|
||||
⚠️ {t('lp.sameTokenWarning')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 调试信息 */}
|
||||
{swapForm.amount && (
|
||||
<div style={{ marginBottom: '12px', padding: '10px', background: '#e3f2fd', borderRadius: '6px', fontSize: '11px', fontFamily: 'monospace' }}>
|
||||
{/* 权限检查 - 最重要 */}
|
||||
<div style={{ marginBottom: '4px', padding: '4px', background: isRewardRouterSwapper && isRewardRouterHandler ? '#e8f5e9' : '#ffebee', borderRadius: '4px' }}>
|
||||
<strong>⚠️ 权限检查:</strong>{' '}
|
||||
RewardRouter是Swapper: {isRewardRouterSwapper === undefined ? '加载中...' : isRewardRouterSwapper ? <span style={{ color: '#4caf50' }}>✓ 是</span> : <span style={{ color: '#f44336' }}>✗ 否 (需在管理员配置中设置)</span>} |{' '}
|
||||
RewardRouter是Handler: {isRewardRouterHandler === undefined ? '加载中...' : isRewardRouterHandler ? <span style={{ color: '#4caf50' }}>✓ 是</span> : <span style={{ color: '#f44336' }}>✗ 否 (需在管理员配置中设置)</span>}
|
||||
</div>
|
||||
{(!isRewardRouterSwapper || !isRewardRouterHandler) && (
|
||||
<div style={{ marginBottom: '4px', padding: '4px', background: '#fff3e0', borderRadius: '4px', color: '#e65100' }}>
|
||||
<strong>🔧 解决方案:</strong> 管理员需要在"管理员配置"中执行: {!isRewardRouterSwapper && 'setSwapper(RewardRouter, true)'} {!isRewardRouterHandler && 'setHandler(RewardRouter, true)'}
|
||||
</div>
|
||||
)}
|
||||
{/* Swap 开关状态 */}
|
||||
<div style={{ marginBottom: '4px', color: '#666' }}>
|
||||
<strong>Swap状态:</strong>{' '}
|
||||
isSwapEnabled: {isSwapEnabled === undefined ? '加载中...' : isSwapEnabled ? <span style={{ color: '#4caf50' }}>✓ 启用</span> : <span style={{ color: '#f44336' }}>✗ 禁用</span>}
|
||||
</div>
|
||||
{/* 池子余额信息 */}
|
||||
<div style={{ marginBottom: '4px', color: '#666' }}>
|
||||
<strong>池子余额:</strong>{' '}
|
||||
{getTokenSymbol(swapForm.tokenOut)}池内: {poolAmountOut !== undefined ? formatUnits(poolAmountOut as bigint, getTokenDecimals(swapForm.tokenOut)) : '加载中'} |{' '}
|
||||
{/* ✅ 使用配置中的 USDY 精度 */}
|
||||
{getTokenSymbol(swapForm.tokenIn)} USDY债务: {usdyAmountIn !== undefined ? formatUnits(usdyAmountIn as bigint, TOKEN_DECIMALS_DYNAMIC.USDY) : '加载中'} |{' '}
|
||||
{getTokenSymbol(swapForm.tokenOut)} USDY债务: {usdyAmountOut !== undefined ? formatUnits(usdyAmountOut as bigint, TOKEN_DECIMALS_DYNAMIC.USDY) : '加载中'}
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#666' }}>
|
||||
<strong>授权信息:</strong>{' '}
|
||||
{getTokenSymbol(swapForm.tokenIn)}授权额度: {swapAllowance !== undefined ? formatUnits(swapAllowance, getTokenDecimals(swapForm.tokenIn)) : '加载中'} |{' '}
|
||||
需要: {swapForm.amount} |{' '}
|
||||
需授权: {needsSwapApproval() ? <span style={{ color: '#f44336' }}>是</span> : <span style={{ color: '#4caf50' }}>否</span>} |{' '}
|
||||
处理中: {isProcessing ? <span style={{ color: '#f44336' }}>是</span> : <span style={{ color: '#4caf50' }}>否</span>}
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#666' }}>
|
||||
<strong>限额信息:</strong>{' '}
|
||||
maxSwapAmount({getTokenSymbol(swapForm.tokenIn)}): {maxSwapAmountIn !== undefined ? (maxSwapAmountIn === 0n ? '无限制' : formatUnits(maxSwapAmountIn, getTokenDecimals(swapForm.tokenIn))) : '加载中'}
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#ff9800' }}>
|
||||
<strong>手续费:</strong>{' '}
|
||||
swapFee: {swapFee !== undefined ? `${Number(swapFee) / 100}%` : 'N/A'} |{' '}
|
||||
taxBps: {taxBasisPoints !== undefined ? `${Number(taxBasisPoints) / 100}%` : 'N/A'} |{' '}
|
||||
动态: {dynamicFees ? '是' : '否'} |{' '}
|
||||
<span style={{ color: '#f44336' }}>建议滑点: ≥{swapFee && taxBasisPoints ? `${(Number(swapFee) + Number(taxBasisPoints)) / 100 + 0.5}%` : '2%'}</span>
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#555' }}>
|
||||
<strong>TokenIn价格 ({getTokenSymbol(swapForm.tokenIn)}):</strong>{' '}
|
||||
maxPrice: {swapTokenInMaxPrice?.toString() || 'N/A'} (显示: {swapTokenInMaxPrice ? (Number(swapTokenInMaxPrice) / 1e30).toFixed(8) : 'N/A'})
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px', color: '#555' }}>
|
||||
<strong>TokenOut价格 ({getTokenSymbol(swapForm.tokenOut)}):</strong>{' '}
|
||||
minPrice: {swapTokenOutMinPrice?.toString() || 'N/A'} (显示: {swapTokenOutMinPrice ? (Number(swapTokenOutMinPrice) / 1e30).toFixed(8) : 'N/A'})
|
||||
</div>
|
||||
{swapTokenInMaxPrice && swapTokenOutMinPrice && swapTokenOutMinPrice > 0n && (() => {
|
||||
try {
|
||||
const tokenInDecimals = getTokenDecimals(swapForm.tokenIn)
|
||||
const tokenOutDecimals = getTokenDecimals(swapForm.tokenOut)
|
||||
const amount = parseUnits(swapForm.amount, tokenInDecimals)
|
||||
const expectedOut = (amount * swapTokenInMaxPrice) / swapTokenOutMinPrice
|
||||
const precisionDiff = tokenInDecimals - tokenOutDecimals
|
||||
const adjustedOut = precisionDiff > 0
|
||||
? expectedOut / (10n ** BigInt(precisionDiff))
|
||||
: expectedOut * (10n ** BigInt(-precisionDiff))
|
||||
const slippageBps = Math.floor(Number(swapForm.slippage) * 100)
|
||||
const minOut = (adjustedOut * BigInt(10000 - slippageBps)) / BigInt(10000)
|
||||
return (
|
||||
<div style={{ marginBottom: '4px', color: '#555' }}>
|
||||
<strong>计算:</strong>{' '}
|
||||
预期输出: {formatUnits(adjustedOut, tokenOutDecimals)} {getTokenSymbol(swapForm.tokenOut)} |{' '}
|
||||
minOut: {formatUnits(minOut, tokenOutDecimals)} {getTokenSymbol(swapForm.tokenOut)}
|
||||
</div>
|
||||
)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()}
|
||||
{(!swapTokenInMaxPrice || !swapTokenOutMinPrice) && (
|
||||
<div style={{ color: '#f44336' }}>
|
||||
<strong>警告:</strong> 代币价格未加载,minOut将为0!请确保代币价格已在合约中设置。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 按钮 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
|
||||
{needsSwapApproval() ? (
|
||||
<button onClick={handleApproveSwapToken} disabled={isProcessing} className="btn btn-secondary btn-sm">
|
||||
{isProcessing ? '...' : t('lp.approveToken')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSwap}
|
||||
disabled={isProcessing || !swapForm.amount || swapForm.tokenIn === swapForm.tokenOut}
|
||||
className="btn btn-primary btn-sm"
|
||||
>
|
||||
{isProcessing ? '...' : t('lp.swap')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
203
frontend/src/components/LP/LPPanelNew.tsx
Normal file
203
frontend/src/components/LP/LPPanelNew.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* LP 流动性池面板 (重构版)
|
||||
* 使用拆分的组件和 hooks
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTransactions } from '../../context/TransactionContext'
|
||||
import { TransactionHistory } from '../TransactionHistory'
|
||||
|
||||
import { useLPPoolData } from './useLPPoolData'
|
||||
import { useLPTransactions } from './useLPTransactions'
|
||||
import { useLPAdminActions } from './useLPAdminActions'
|
||||
import { LPPoolInfo } from './LPPoolInfo'
|
||||
import { LPOperations } from './LPOperations'
|
||||
import { LPTokenBalances } from './LPTokenBalances'
|
||||
import { LPAdminConfig } from './LPAdminConfig'
|
||||
import { LPDebugInfo } from './LPDebugInfo'
|
||||
import { LPUsdyPanel } from './LPUsdyPanel'
|
||||
|
||||
export function LPPanelNew() {
|
||||
const { t } = useTranslation()
|
||||
const { transactions, clearHistory } = useTransactions()
|
||||
|
||||
// 使用拆分的 hooks
|
||||
const poolData = useLPPoolData()
|
||||
|
||||
// 解构 refetch 函数,这些函数引用是稳定的
|
||||
const {
|
||||
refetchBalance,
|
||||
refetchTokenBalances,
|
||||
refetchYtLPTotalSupply,
|
||||
refetchYtLPPrice,
|
||||
refetchAumInUsdy,
|
||||
refetchAccountValue,
|
||||
refetchUsdyAmounts,
|
||||
refetchTokenWeights,
|
||||
refetchEmergencyMode,
|
||||
refetchSwapEnabled,
|
||||
refetchUsdySupply,
|
||||
refetchPoolValue,
|
||||
refetchVaultWhitelist,
|
||||
} = poolData
|
||||
|
||||
// 使用 useCallback 稳定回调,依赖解构后的稳定函数引用
|
||||
const handleSuccess = useCallback(() => {
|
||||
// 刷新数据
|
||||
refetchBalance()
|
||||
refetchTokenBalances()
|
||||
refetchYtLPTotalSupply()
|
||||
refetchYtLPPrice()
|
||||
refetchAumInUsdy()
|
||||
refetchAccountValue()
|
||||
refetchUsdyAmounts()
|
||||
refetchTokenWeights()
|
||||
refetchEmergencyMode()
|
||||
refetchSwapEnabled()
|
||||
refetchUsdySupply()
|
||||
refetchPoolValue()
|
||||
refetchVaultWhitelist()
|
||||
}, [
|
||||
refetchBalance, refetchTokenBalances, refetchYtLPTotalSupply, refetchYtLPPrice,
|
||||
refetchAumInUsdy, refetchAccountValue, refetchUsdyAmounts, refetchTokenWeights,
|
||||
refetchEmergencyMode, refetchSwapEnabled, refetchUsdySupply, refetchPoolValue,
|
||||
refetchVaultWhitelist
|
||||
])
|
||||
|
||||
// 使用 useMemo 稳定 callbacks 对象
|
||||
const txCallbacks = useMemo(() => ({
|
||||
onSuccess: handleSuccess,
|
||||
}), [handleSuccess])
|
||||
|
||||
const {
|
||||
writeContract,
|
||||
recordTx,
|
||||
isProcessing,
|
||||
} = useLPTransactions(txCallbacks)
|
||||
|
||||
// 管理员操作 - recordTx 需要 cast 为 string 类型
|
||||
const adminActions = useLPAdminActions({
|
||||
address: poolData.address,
|
||||
writeContract,
|
||||
recordTx: recordTx as (type: string, amount?: string, token?: string) => void,
|
||||
})
|
||||
|
||||
// UI 状态
|
||||
const [showAdminConfig, setShowAdminConfig] = useState(false)
|
||||
const [showDebugInfo, setShowDebugInfo] = useState(false)
|
||||
const [showUsdyPanel, setShowUsdyPanel] = useState(false)
|
||||
|
||||
// 未连接钱包
|
||||
if (!poolData.isConnected) {
|
||||
return (
|
||||
<div className="panel">
|
||||
<h2>{t('lp.title')}</h2>
|
||||
<p className="text-muted">{t('common.connectFirst')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
<h2>{t('lp.title')}</h2>
|
||||
|
||||
{/* 池子信息 */}
|
||||
<LPPoolInfo
|
||||
ytLPName={poolData.ytLPName as string}
|
||||
ytLPSymbol={poolData.ytLPSymbol as string}
|
||||
ytLPDecimals={poolData.ytLPDecimals as number}
|
||||
ytLPBalance={poolData.ytLPBalance as bigint}
|
||||
ytLPTotalSupply={poolData.ytLPTotalSupply as bigint}
|
||||
ytLPPrice={poolData.ytLPPrice as bigint}
|
||||
aumInUsdy={poolData.aumInUsdy as bigint}
|
||||
accountValue={poolData.accountValue as bigint}
|
||||
cooldownDuration={poolData.cooldownDuration as bigint}
|
||||
lastAddedAt={poolData.lastAddedAt as bigint}
|
||||
emergencyMode={poolData.emergencyMode as boolean}
|
||||
swapEnabled={poolData.swapEnabled as boolean}
|
||||
poolValue={poolData.poolValue as bigint}
|
||||
/>
|
||||
|
||||
{/* 代币余额表格 */}
|
||||
<LPTokenBalances
|
||||
poolTokens={poolData.poolTokens}
|
||||
allAvailableTokens={poolData.allAvailableTokens}
|
||||
/>
|
||||
|
||||
{/* LP 操作 */}
|
||||
<LPOperations
|
||||
address={poolData.address}
|
||||
poolTokens={poolData.poolTokens}
|
||||
ytLPBalance={poolData.ytLPBalance as bigint}
|
||||
ytLPPrice={poolData.ytLPPrice as bigint}
|
||||
isProcessing={isProcessing}
|
||||
writeContract={writeContract}
|
||||
recordTx={recordTx}
|
||||
getTokenSymbol={poolData.getTokenSymbol}
|
||||
swapFee={poolData.swapFee as bigint}
|
||||
taxBasisPoints={poolData.taxBasisPoints as bigint}
|
||||
dynamicFees={poolData.dynamicFees as boolean}
|
||||
/>
|
||||
|
||||
{/* 管理员配置 */}
|
||||
<LPAdminConfig
|
||||
address={poolData.address}
|
||||
isProcessing={isProcessing}
|
||||
poolPaused={poolData.poolPaused as boolean}
|
||||
dynamicFees={poolData.dynamicFees as boolean}
|
||||
emergencyMode={poolData.emergencyMode as boolean}
|
||||
swapEnabled={poolData.swapEnabled as boolean}
|
||||
cooldownDuration={poolData.cooldownDuration as bigint}
|
||||
usdcPriceSource={poolData.usdcPriceSource as string}
|
||||
availableTokens={poolData.allAvailableTokens}
|
||||
handleSetCooldownDuration={adminActions.handleSetCooldownDuration}
|
||||
handleSetSwapFees={adminActions.handleSetSwapFees}
|
||||
handleWithdrawToken={adminActions.handleWithdrawToken}
|
||||
handleSetDynamicFees={adminActions.handleSetDynamicFees}
|
||||
handlePausePool={adminActions.handlePausePool}
|
||||
handleUnpausePool={adminActions.handleUnpausePool}
|
||||
handleSetMaxSwapAmount={adminActions.handleSetMaxSwapAmount}
|
||||
handleSetAumAdjustment={adminActions.handleSetAumAdjustment}
|
||||
handleSetMaxSwapSlippageBps={adminActions.handleSetMaxSwapSlippageBps}
|
||||
handleSetMaxPriceChangeBps={adminActions.handleSetMaxPriceChangeBps}
|
||||
handleSetPoolManagerGov={adminActions.handleSetPoolManagerGov}
|
||||
handleSetVaultGov={adminActions.handleSetVaultGov}
|
||||
handleSetHandler={adminActions.handleSetHandler}
|
||||
handleSetKeeper={adminActions.handleSetKeeper}
|
||||
handleSetSwapper={adminActions.handleSetSwapper}
|
||||
handleSetPoolManager={adminActions.handleSetPoolManager}
|
||||
handleSetMinter={adminActions.handleSetMinter}
|
||||
handleSetWhitelistedToken={adminActions.handleSetWhitelistedToken}
|
||||
handleClearWhitelistedToken={adminActions.handleClearWhitelistedToken}
|
||||
handleSetEmergencyMode={adminActions.handleSetEmergencyMode}
|
||||
handleSetSwapEnabled={adminActions.handleSetSwapEnabled}
|
||||
handleSetPrice={adminActions.handleSetPrice}
|
||||
handleSetSpread={adminActions.handleSetSpread}
|
||||
handleSetUsdcPriceSource={adminActions.handleSetUsdcPriceSource}
|
||||
handleSetStableToken={adminActions.handleSetStableToken}
|
||||
handleAddUsdyVault={adminActions.handleAddUsdyVault}
|
||||
handleRemoveUsdyVault={adminActions.handleRemoveUsdyVault}
|
||||
show={showAdminConfig}
|
||||
onToggle={() => setShowAdminConfig(!showAdminConfig)}
|
||||
/>
|
||||
|
||||
{/* USDY 面板 */}
|
||||
<LPUsdyPanel
|
||||
show={showUsdyPanel}
|
||||
onToggle={() => setShowUsdyPanel(!showUsdyPanel)}
|
||||
/>
|
||||
|
||||
{/* 调试信息 */}
|
||||
<LPDebugInfo
|
||||
vaultGov={poolData.vaultGov as string}
|
||||
poolManagerGov={poolData.poolManagerGov as string}
|
||||
show={showDebugInfo}
|
||||
onToggle={() => setShowDebugInfo(!showDebugInfo)}
|
||||
/>
|
||||
|
||||
{/* 交易历史 */}
|
||||
<TransactionHistory transactions={transactions} onClear={clearHistory} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
110
frontend/src/components/LP/LPPoolInfo.tsx
Normal file
110
frontend/src/components/LP/LPPoolInfo.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* LP 池子信息显示组件
|
||||
* 显示 ytLP 代币信息、池子数据、用户数据
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatUnits } from 'viem'
|
||||
import { useChainId } from 'wagmi'
|
||||
import { getContracts, getDecimals } from '../../config/contracts'
|
||||
|
||||
interface LPPoolInfoProps {
|
||||
// ytLP 代币信息
|
||||
ytLPName?: string
|
||||
ytLPSymbol?: string
|
||||
ytLPDecimals?: number
|
||||
ytLPBalance?: bigint
|
||||
ytLPTotalSupply?: bigint
|
||||
ytLPPrice?: bigint
|
||||
// 池子数据
|
||||
aumInUsdy?: bigint
|
||||
accountValue?: bigint
|
||||
cooldownDuration?: bigint
|
||||
lastAddedAt?: bigint
|
||||
// 状态
|
||||
emergencyMode?: boolean
|
||||
swapEnabled?: boolean
|
||||
poolValue?: bigint
|
||||
}
|
||||
|
||||
export function LPPoolInfo({
|
||||
ytLPName,
|
||||
ytLPSymbol,
|
||||
ytLPDecimals,
|
||||
ytLPBalance,
|
||||
ytLPTotalSupply,
|
||||
ytLPPrice,
|
||||
aumInUsdy,
|
||||
accountValue,
|
||||
cooldownDuration,
|
||||
lastAddedAt,
|
||||
emergencyMode,
|
||||
swapEnabled,
|
||||
poolValue,
|
||||
}: LPPoolInfoProps) {
|
||||
const { t } = useTranslation()
|
||||
// ===== 多链支持 =====
|
||||
const chainId = useChainId()
|
||||
const CONTRACTS = getContracts(chainId)
|
||||
const TOKEN_DECIMALS = getDecimals(chainId)
|
||||
|
||||
// 计算冷却剩余时间
|
||||
const getCooldownRemaining = () => {
|
||||
if (!lastAddedAt || !cooldownDuration) return 0
|
||||
const cooldownEnd = Number(lastAddedAt) + Number(cooldownDuration)
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
return Math.max(0, cooldownEnd - now)
|
||||
}
|
||||
|
||||
const formatCooldown = (seconds: number) => {
|
||||
if (seconds <= 0) return t('lp.noCooldown')
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins}m ${secs}s`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pool-info" style={{ fontSize: '12px' }}>
|
||||
{/* ytLP 代币信息 */}
|
||||
<div style={{ display: 'flex', gap: '16px', marginBottom: '8px', flexWrap: 'wrap', padding: '8px', background: '#f8f9fa', borderRadius: '4px' }}>
|
||||
<span style={{ color: '#666' }}>名称: <strong>{ytLPName || 'ytLP'}</strong></span>
|
||||
<span style={{ color: '#666' }}>符号: <strong>{ytLPSymbol || 'ytLP'}</strong></span>
|
||||
<span style={{ color: '#666' }}>精度: <strong>{ytLPDecimals?.toString() || '18'}</strong></span>
|
||||
<span style={{ color: '#666' }}>合约: <code style={{ fontSize: '11px' }}>{CONTRACTS.YT_LP_TOKEN.slice(0, 8)}...{CONTRACTS.YT_LP_TOKEN.slice(-6)}</code></span>
|
||||
</div>
|
||||
|
||||
{/* 池子数据 + 用户数据 - 网格布局 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '8px 16px', padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
|
||||
<div title="管理资产总额,池中所有代币的总价值 / Assets Under Management, total value of all tokens in pool" style={{ cursor: 'help' }}>
|
||||
<span style={{ color: '#666' }}>AUM:</span> <strong>{aumInUsdy ? Number(formatUnits(aumInUsdy, TOKEN_DECIMALS.USDY)).toFixed(2) : '0'} USDY</strong>
|
||||
</div>
|
||||
<div title="ytLP代币价格 = AUM / ytLP总供应量 / ytLP token price = AUM / total supply" style={{ cursor: 'help' }}>
|
||||
<span style={{ color: '#666' }}>ytLP价格:</span> <strong>{ytLPPrice ? Number(formatUnits(ytLPPrice, TOKEN_DECIMALS.INTERNAL_PRICE)).toFixed(6) : '1'}</strong>
|
||||
</div>
|
||||
<div title="ytLP代币总发行量 / Total ytLP tokens minted" style={{ cursor: 'help' }}>
|
||||
<span style={{ color: '#666' }}>总供应:</span> <strong>{ytLPTotalSupply ? Number(formatUnits(ytLPTotalSupply, TOKEN_DECIMALS.YT_LP)).toFixed(2) : '0'}</strong>
|
||||
</div>
|
||||
<div title="你持有的ytLP代币数量 / Your ytLP token balance" style={{ cursor: 'help' }}>
|
||||
<span style={{ color: '#666' }}>你的ytLP:</span> <strong>{ytLPBalance ? Number(formatUnits(ytLPBalance, TOKEN_DECIMALS.YT_LP)).toFixed(2) : '0'}</strong>
|
||||
</div>
|
||||
<div title="添加流动性后的冷却时间,期间无法移除流动性 / Cooldown after adding liquidity, cannot remove during this period" style={{ cursor: 'help' }}>
|
||||
<span style={{ color: '#666' }}>冷却:</span> <strong>{formatCooldown(getCooldownRemaining())}</strong>
|
||||
</div>
|
||||
<div title="你的账户在池中的总价值(USD) / Your account value in the pool (USD)" style={{ cursor: 'help' }}>
|
||||
<span style={{ color: '#666' }}>账户价值:</span> <strong>${accountValue ? Number(formatUnits(accountValue, TOKEN_DECIMALS.INTERNAL_PRICE)).toFixed(6) : '0'}</strong>
|
||||
</div>
|
||||
<div title="紧急模式开启时,部分功能将被禁用 / When emergency mode is ON, some functions are disabled" style={{ cursor: 'help' }}>
|
||||
<span style={{ color: '#666' }}>紧急模式:</span>{' '}
|
||||
<strong style={{ color: emergencyMode ? '#f44336' : '#4caf50' }}>{emergencyMode ? 'ON' : '正常'}</strong>
|
||||
</div>
|
||||
<div title="Swap 功能开关 / Swap function toggle" style={{ cursor: 'help' }}>
|
||||
<span style={{ color: '#666' }}>Swap:</span>{' '}
|
||||
<strong style={{ color: swapEnabled ? '#4caf50' : '#f44336' }}>{swapEnabled ? '开启' : '关闭'}</strong>
|
||||
</div>
|
||||
<div title="池子价值 (USD) / Pool value (USD)" style={{ cursor: 'help' }}>
|
||||
<span style={{ color: '#666' }}>池价值:</span> <strong>${poolValue ? Number(formatUnits(poolValue, TOKEN_DECIMALS.INTERNAL_PRICE)).toFixed(2) : '0'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
231
frontend/src/components/LP/LPTokenBalances.tsx
Normal file
231
frontend/src/components/LP/LPTokenBalances.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* LP 代币余额表格组件
|
||||
* 显示池子中所有代币的余额、权重、比例等信息
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatUnits } from 'viem'
|
||||
import { TOKEN_DECIMALS, getTokenDecimals } from '../../config/contracts'
|
||||
import type { ExtendedPoolToken, AvailableToken } from './useLPPoolData'
|
||||
|
||||
interface TokenWithStats extends ExtendedPoolToken {
|
||||
targetRatio: number
|
||||
currentRatio: number
|
||||
deviation: number
|
||||
inPool: boolean
|
||||
}
|
||||
|
||||
interface PoolStats {
|
||||
tokens: TokenWithStats[]
|
||||
totalUsdy: bigint
|
||||
totalWeight: bigint
|
||||
}
|
||||
|
||||
interface LPTokenBalancesProps {
|
||||
poolTokens: ExtendedPoolToken[]
|
||||
allAvailableTokens?: AvailableToken[]
|
||||
totalTokenWeights?: bigint
|
||||
}
|
||||
|
||||
export function LPTokenBalances({ poolTokens, allAvailableTokens, totalTokenWeights }: LPTokenBalancesProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 计算池子统计数据,合并显示所有代币(池子代币 + Factory代币)
|
||||
const poolStats = useMemo((): PoolStats | null => {
|
||||
if (!poolTokens || poolTokens.length === 0) {
|
||||
// 如果池子没有代币,但有 Factory 代币,也显示
|
||||
if (allAvailableTokens && allAvailableTokens.length > 0) {
|
||||
const factoryOnlyTokens: TokenWithStats[] = allAvailableTokens
|
||||
.filter(t => t.source === 'factory')
|
||||
.map(t => ({
|
||||
address: t.address as `0x${string}`,
|
||||
symbol: t.symbol,
|
||||
name: t.name,
|
||||
decimals: 18,
|
||||
balance: 0n,
|
||||
isWhitelisted: t.isWhitelisted,
|
||||
usdyAmount: 0n,
|
||||
isStable: t.isStable,
|
||||
weight: 0n,
|
||||
targetRatio: 0,
|
||||
currentRatio: 0,
|
||||
deviation: 0,
|
||||
inPool: false,
|
||||
}))
|
||||
if (factoryOnlyTokens.length > 0) {
|
||||
return { tokens: factoryOnlyTokens, totalUsdy: 0n, totalWeight: 0n }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const totalUsdy = poolTokens.reduce((sum, t) => sum + (t.usdyAmount || 0n), 0n)
|
||||
const totalWeight = totalTokenWeights || poolTokens.reduce((sum, t) => sum + (t.weight || 0n), 0n)
|
||||
|
||||
// 池子代币
|
||||
const poolTokensWithStats: TokenWithStats[] = poolTokens.map(token => {
|
||||
const targetRatio = totalWeight > 0n
|
||||
? (Number(token.weight || 0n) / Number(totalWeight)) * 100
|
||||
: 0
|
||||
const currentRatio = totalUsdy > 0n
|
||||
? (Number(token.usdyAmount || 0n) / Number(totalUsdy)) * 100
|
||||
: 0
|
||||
const deviation = currentRatio - targetRatio
|
||||
|
||||
return {
|
||||
...token,
|
||||
targetRatio,
|
||||
currentRatio,
|
||||
deviation,
|
||||
inPool: true,
|
||||
}
|
||||
})
|
||||
|
||||
// 添加 Factory 里有但池子里没有的代币
|
||||
const poolAddresses = new Set(poolTokens.map(t => t.address.toLowerCase()))
|
||||
const factoryOnlyTokens: TokenWithStats[] = (allAvailableTokens || [])
|
||||
.filter(t => t.source === 'factory' && !poolAddresses.has(t.address.toLowerCase()))
|
||||
.map(t => ({
|
||||
address: t.address as `0x${string}`,
|
||||
symbol: t.symbol,
|
||||
name: t.name,
|
||||
decimals: 18,
|
||||
balance: 0n,
|
||||
isWhitelisted: t.isWhitelisted,
|
||||
usdyAmount: 0n,
|
||||
isStable: t.isStable,
|
||||
weight: 0n,
|
||||
targetRatio: 0,
|
||||
currentRatio: 0,
|
||||
deviation: 0,
|
||||
inPool: false,
|
||||
}))
|
||||
|
||||
return { tokens: [...poolTokensWithStats, ...factoryOnlyTokens], totalUsdy, totalWeight }
|
||||
}, [poolTokens, allAvailableTokens, totalTokenWeights])
|
||||
|
||||
if (!poolStats) {
|
||||
return (
|
||||
<div className="balance-info" style={{ marginTop: '12px' }}>
|
||||
<h4 style={{ marginBottom: '8px', fontSize: '14px' }}>{t('lp.yourTokenBalances')}</h4>
|
||||
<p style={{ color: '#666', fontSize: '12px' }}>Loading...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="balance-info" style={{ marginTop: '12px' }}>
|
||||
<h4 style={{ marginBottom: '8px', fontSize: '14px' }}>{t('lp.yourTokenBalances')}</h4>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', fontSize: '12px', borderCollapse: 'collapse', minWidth: '600px' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid #ddd', background: '#f8f9fa' }}>
|
||||
<th style={{ textAlign: 'left', padding: '6px 4px', cursor: 'help' }} title="代币符号 / Token Symbol">
|
||||
{t('lp.selectToken')}
|
||||
</th>
|
||||
<th style={{ textAlign: 'center', padding: '6px 4px', cursor: 'help' }} title="是否在白名单中 / Is whitelisted">
|
||||
WL
|
||||
</th>
|
||||
<th style={{ textAlign: 'center', padding: '6px 4px', cursor: 'help' }} title="是否在池子中 / Is in pool">
|
||||
Pool
|
||||
</th>
|
||||
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="你的钱包余额 / Your wallet balance">
|
||||
{t('common.balance')}
|
||||
</th>
|
||||
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="池中代币价值 / Token value in pool">
|
||||
USDY
|
||||
</th>
|
||||
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="代币权重,用于计算目标比例 / Token weight for target ratio">
|
||||
Weight
|
||||
</th>
|
||||
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="目标比例 = 权重/总权重 / Target ratio = weight/totalWeights">
|
||||
Target
|
||||
</th>
|
||||
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="当前比例 = USDY数量/总USDY / Current ratio = usdyAmount/totalUsdy">
|
||||
Current
|
||||
</th>
|
||||
<th style={{ textAlign: 'right', padding: '6px 4px', cursor: 'help' }} title="偏差 = 当前 - 目标。正值:代币过多(卖出费率高)。负值:代币不足(买入费率低)">
|
||||
Deviation
|
||||
</th>
|
||||
<th style={{ textAlign: 'center', padding: '6px 4px', cursor: 'help' }} title="是否稳定币(更低的交换费率 0.04%)">
|
||||
Stable
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{poolStats.tokens.map((token) => (
|
||||
<tr key={token.address} style={{ borderBottom: '1px solid #eee', opacity: token.inPool ? 1 : 0.7 }}>
|
||||
<td style={{ padding: '4px' }}>
|
||||
<strong>{token.symbol}</strong>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center', padding: '4px' }}>
|
||||
{token.isWhitelisted ? (
|
||||
<span style={{ color: '#4caf50', fontWeight: 'bold' }}>✓</span>
|
||||
) : (
|
||||
<span style={{ color: '#f44336', fontWeight: 'bold' }}>✗</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ textAlign: 'center', padding: '4px' }}>
|
||||
{token.inPool ? (
|
||||
<span style={{ color: '#4caf50', fontWeight: 'bold' }}>✓</span>
|
||||
) : (
|
||||
<span style={{ color: '#ff9800', fontSize: '10px' }}>未添加</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px' }}>
|
||||
{token.balance ? Number(formatUnits(token.balance, getTokenDecimals(token.address))).toFixed(2) : '0'}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px' }}>
|
||||
{token.usdyAmount ? Number(formatUnits(token.usdyAmount, TOKEN_DECIMALS.USDY)).toFixed(2) : '0'}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px' }}>
|
||||
{token.weight ? token.weight.toString() : '0'}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px' }}>
|
||||
{token.targetRatio.toFixed(1)}%
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px' }}>
|
||||
{token.currentRatio.toFixed(1)}%
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px' }}>
|
||||
<span style={{
|
||||
color: Math.abs(token.deviation) < 1 ? '#4caf50' :
|
||||
Math.abs(token.deviation) < 5 ? '#ff9800' : '#f44336',
|
||||
fontWeight: Math.abs(token.deviation) >= 5 ? 'bold' : 'normal'
|
||||
}}>
|
||||
{token.deviation >= 0 ? '+' : ''}{token.deviation.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ textAlign: 'center', padding: '4px' }}>
|
||||
{token.isStable ? <span style={{ color: '#2196f3' }}>Yes</span> : <span style={{ color: '#999' }}>-</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr style={{ borderTop: '2px solid #ddd', background: '#f8f9fa', fontWeight: 'bold' }}>
|
||||
<td style={{ padding: '6px 4px' }}>Total</td>
|
||||
<td style={{ textAlign: 'center', padding: '6px 4px' }}>-</td>
|
||||
<td style={{ textAlign: 'center', padding: '6px 4px' }}>-</td>
|
||||
<td style={{ textAlign: 'right', padding: '6px 4px' }}>-</td>
|
||||
<td style={{ textAlign: 'right', padding: '6px 4px' }}>
|
||||
{Number(formatUnits(poolStats.totalUsdy, TOKEN_DECIMALS.USDY)).toFixed(2)}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '6px 4px' }}>
|
||||
{poolStats.totalWeight.toString()}
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '6px 4px' }}>100%</td>
|
||||
<td style={{ textAlign: 'right', padding: '6px 4px' }}>100%</td>
|
||||
<td style={{ textAlign: 'right', padding: '6px 4px' }}>-</td>
|
||||
<td style={{ textAlign: 'center', padding: '6px 4px' }}>-</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<p style={{ fontSize: '10px', color: '#999', marginTop: '6px' }}>
|
||||
Deviation: 偏差值。正值表示该代币过多(卖出费率较高),负值表示不足(买入费率较低)
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
202
frontend/src/components/LP/LPUsdyPanel.tsx
Normal file
202
frontend/src/components/LP/LPUsdyPanel.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* LP USDY 面板组件
|
||||
* 显示 USDY 相关操作
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { formatUnits, parseUnits } from 'viem'
|
||||
import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, useChainId } from 'wagmi'
|
||||
import { GAS_CONFIG, getContracts, getDecimals } from '../../config/contracts'
|
||||
import { ERC20_BASE_ABI } from './types'
|
||||
import { useToast } from '../Toast'
|
||||
|
||||
interface LPUsdyPanelProps {
|
||||
show: boolean
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
export function LPUsdyPanel({
|
||||
show,
|
||||
onToggle,
|
||||
}: LPUsdyPanelProps) {
|
||||
const { address } = useAccount()
|
||||
const { showToast } = useToast()
|
||||
// ===== 多链支持 =====
|
||||
const chainId = useChainId()
|
||||
const CONTRACTS = getContracts(chainId)
|
||||
const TOKEN_DECIMALS = getDecimals(chainId)
|
||||
|
||||
const [transferForm, setTransferForm] = useState({
|
||||
recipient: '',
|
||||
amount: '',
|
||||
})
|
||||
|
||||
// 读取 USDY 余额
|
||||
const { data: usdyBalance } = useReadContract({
|
||||
address: CONTRACTS.USDY as `0x${string}`,
|
||||
abi: ERC20_BASE_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: address ? [address] : undefined,
|
||||
})
|
||||
|
||||
// 读取 USDY 授权额度
|
||||
const { data: usdyAllowance } = useReadContract({
|
||||
address: CONTRACTS.USDY as `0x${string}`,
|
||||
abi: ERC20_BASE_ABI,
|
||||
functionName: 'allowance',
|
||||
args: address ? [address, CONTRACTS.YT_REWARD_ROUTER] : undefined,
|
||||
})
|
||||
|
||||
const { writeContract, data: hash, isPending } = useWriteContract()
|
||||
const { isLoading: isConfirming } = useWaitForTransactionReceipt({ hash })
|
||||
|
||||
const isProcessing = isPending || isConfirming
|
||||
|
||||
// 授权 USDY 给 Router
|
||||
const handleApproveUsdy = () => {
|
||||
if (!address) return
|
||||
writeContract({
|
||||
address: CONTRACTS.USDY as `0x${string}`,
|
||||
abi: ERC20_BASE_ABI,
|
||||
functionName: 'approve',
|
||||
args: [CONTRACTS.YT_REWARD_ROUTER, parseUnits('999999999', TOKEN_DECIMALS.USDY)],
|
||||
gas: GAS_CONFIG.SIMPLE,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
showToast('info', '正在授权 USDY...')
|
||||
}
|
||||
|
||||
// 转账 USDY
|
||||
const handleTransferUsdy = () => {
|
||||
if (!address || !transferForm.recipient || !transferForm.amount) return
|
||||
writeContract({
|
||||
address: CONTRACTS.USDY as `0x${string}`,
|
||||
abi: ERC20_BASE_ABI,
|
||||
functionName: 'transfer',
|
||||
args: [
|
||||
transferForm.recipient as `0x${string}`,
|
||||
parseUnits(transferForm.amount, TOKEN_DECIMALS.USDY),
|
||||
],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
showToast('info', '正在转账 USDY...')
|
||||
}
|
||||
|
||||
const inputStyle = {
|
||||
padding: '6px 8px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
width: '100%',
|
||||
marginBottom: '6px',
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f9f9f9', borderRadius: '8px', border: '1px solid #e0e0e0' }}>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<h4 style={{ margin: 0, color: '#333', fontSize: '13px' }}>USDY 操作</h4>
|
||||
<span style={{ color: '#999', fontSize: '16px' }}>{show ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
|
||||
{show && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
{/* USDY 余额信息 */}
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
background: '#fff',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '12px',
|
||||
border: '1px solid #e0e0e0'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||
<span style={{ fontSize: '11px', color: '#666' }}>USDY 余额:</span>
|
||||
<span style={{ fontSize: '13px', fontWeight: 'bold', color: '#333' }}>
|
||||
{usdyBalance
|
||||
? Number(formatUnits(usdyBalance, TOKEN_DECIMALS.USDY)).toFixed(4)
|
||||
: '0'} USDY
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '11px', color: '#666' }}>Router 授权额度:</span>
|
||||
<span style={{ fontSize: '11px', color: '#666' }}>
|
||||
{usdyAllowance !== undefined
|
||||
? (Number(formatUnits(usdyAllowance as bigint, TOKEN_DECIMALS.USDY)) > 1000000
|
||||
? 'Unlimited'
|
||||
: Number(formatUnits(usdyAllowance as bigint, TOKEN_DECIMALS.USDY)).toFixed(4))
|
||||
: '0'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 授权按钮 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<button
|
||||
onClick={handleApproveUsdy}
|
||||
disabled={isProcessing || !address}
|
||||
className="btn btn-sm btn-success"
|
||||
style={{ fontSize: '11px', width: '100%' }}
|
||||
>
|
||||
{isProcessing ? '处理中...' : '授权 USDY 给 Router (无限额度)'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 转账 USDY */}
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
background: '#fff',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #e0e0e0'
|
||||
}}>
|
||||
<div style={{ fontSize: '11px', fontWeight: 'bold', color: '#333', marginBottom: '8px' }}>
|
||||
转账 USDY
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={transferForm.recipient}
|
||||
onChange={(e) => setTransferForm({ ...transferForm, recipient: e.target.value })}
|
||||
style={inputStyle}
|
||||
placeholder="接收地址 (0x...)"
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={transferForm.amount}
|
||||
onChange={(e) => setTransferForm({ ...transferForm, amount: e.target.value })}
|
||||
style={{ ...inputStyle, flex: 1 }}
|
||||
placeholder="数量"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (usdyBalance) {
|
||||
setTransferForm({
|
||||
...transferForm,
|
||||
amount: formatUnits(usdyBalance, TOKEN_DECIMALS.USDY),
|
||||
})
|
||||
}
|
||||
}}
|
||||
className="btn btn-sm btn-outline"
|
||||
style={{ fontSize: '10px', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
MAX
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleTransferUsdy}
|
||||
disabled={isProcessing || !address || !transferForm.recipient || !transferForm.amount}
|
||||
className="btn btn-sm btn-primary"
|
||||
style={{ fontSize: '11px', width: '100%', marginTop: '8px' }}
|
||||
>
|
||||
{isProcessing ? '处理中...' : '转账'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
frontend/src/components/LP/index.ts
Normal file
15
frontend/src/components/LP/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* LP 模块导出
|
||||
*/
|
||||
|
||||
export * from './types'
|
||||
export * from './useLPPoolData'
|
||||
export * from './useLPTransactions'
|
||||
export * from './useLPAdminActions'
|
||||
export * from './LPPoolInfo'
|
||||
export * from './LPOperations'
|
||||
export * from './LPTokenBalances'
|
||||
export * from './LPAdminConfig'
|
||||
export * from './LPDebugInfo'
|
||||
export * from './LPUsdyPanel'
|
||||
export * from './LPPanelNew'
|
||||
134
frontend/src/components/LP/types.ts
Normal file
134
frontend/src/components/LP/types.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* LP 流动性池相关类型定义
|
||||
*/
|
||||
|
||||
// 池子代币信息
|
||||
export interface PoolToken {
|
||||
address: `0x${string}`
|
||||
symbol: string
|
||||
name: string
|
||||
decimals: number
|
||||
}
|
||||
|
||||
// 添加流动性表单
|
||||
export interface AddLiquidityForm {
|
||||
token: string
|
||||
amount: string
|
||||
slippage: string
|
||||
}
|
||||
|
||||
// 移除流动性表单
|
||||
export interface RemoveLiquidityForm {
|
||||
token: string
|
||||
amount: string
|
||||
slippage: string
|
||||
}
|
||||
|
||||
// 交换表单
|
||||
export interface SwapForm {
|
||||
tokenIn: string
|
||||
tokenOut: string
|
||||
amount: string
|
||||
slippage: string
|
||||
}
|
||||
|
||||
// 价格配置表单
|
||||
export interface PriceConfigForm {
|
||||
token: string
|
||||
price: string
|
||||
spreadBps: string
|
||||
}
|
||||
|
||||
// Vault 配置表单
|
||||
export interface VaultConfigForm {
|
||||
token: string
|
||||
tokenDecimals: string
|
||||
tokenWeight: string
|
||||
maxUsdyAmount: string
|
||||
}
|
||||
|
||||
// YT 价格表单
|
||||
export interface YtPriceForm {
|
||||
ytPrice: string
|
||||
}
|
||||
|
||||
// 稳定代币配置
|
||||
export interface StableTokenConfig {
|
||||
token: string
|
||||
isStable: boolean
|
||||
}
|
||||
|
||||
// P1: 冷却时间表单
|
||||
export interface CooldownForm {
|
||||
duration: string
|
||||
}
|
||||
|
||||
// P1: 交换手续费表单
|
||||
export interface SwapFeesForm {
|
||||
swapFee: string
|
||||
stableSwapFee: string
|
||||
taxBasisPoints: string
|
||||
stableTaxBasisPoints: string
|
||||
}
|
||||
|
||||
// P1: 提取代币表单
|
||||
export interface WithdrawForm {
|
||||
token: string
|
||||
receiver: string
|
||||
amount: string
|
||||
}
|
||||
|
||||
// P2: AUM 调整表单
|
||||
export interface AumAdjustForm {
|
||||
addition: string
|
||||
deduction: string
|
||||
}
|
||||
|
||||
// P2: 限制表单
|
||||
export interface LimitsForm {
|
||||
maxSwapSlippageBps: string
|
||||
maxPriceChangeBps: string
|
||||
}
|
||||
|
||||
// P2: 最大交换金额表单
|
||||
export interface MaxSwapForm {
|
||||
token: string
|
||||
amount: string
|
||||
}
|
||||
|
||||
// P3: 权限管理表单
|
||||
export interface PermissionForm {
|
||||
govAddress: string
|
||||
handlerAddress: string
|
||||
handlerActive: boolean
|
||||
keeperAddress: string
|
||||
keeperActive: boolean
|
||||
swapperAddress: string
|
||||
swapperActive: boolean
|
||||
minterAddress: string
|
||||
minterActive: boolean
|
||||
poolManagerAddress: string
|
||||
}
|
||||
|
||||
// LP Panel Context - 共享状态
|
||||
export interface LPPanelContext {
|
||||
address: `0x${string}` | undefined
|
||||
isConnected: boolean
|
||||
isProcessing: boolean
|
||||
poolTokens: PoolToken[]
|
||||
allAvailableTokens: PoolToken[]
|
||||
showToast: (type: 'success' | 'error' | 'info' | 'warning', message: string) => void
|
||||
recordTx: (type: string, amount?: string, token?: string) => void
|
||||
writeContract: (config: any) => void
|
||||
refetchAll: () => void
|
||||
}
|
||||
|
||||
// ERC20 基础 ABI
|
||||
export const ERC20_BASE_ABI = [
|
||||
{ inputs: [], name: 'symbol', outputs: [{ type: 'string' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [], name: 'name', outputs: [{ type: 'string' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [{ type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [{ type: 'address' }, { type: 'address' }], name: 'allowance', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
|
||||
{ inputs: [{ type: 'address' }, { type: 'uint256' }], name: 'approve', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
|
||||
{ inputs: [{ type: 'address' }, { type: 'uint256' }], name: 'transfer', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
|
||||
] as const
|
||||
525
frontend/src/components/LP/useLPAdminActions.ts
Normal file
525
frontend/src/components/LP/useLPAdminActions.ts
Normal file
@@ -0,0 +1,525 @@
|
||||
/**
|
||||
* LP 管理员操作 Hook
|
||||
* 包含 P1-P3 级别的管理功能
|
||||
*/
|
||||
|
||||
import { parseUnits } from 'viem'
|
||||
import {
|
||||
CONTRACTS,
|
||||
GAS_CONFIG,
|
||||
TOKEN_DECIMALS,
|
||||
getTokenDecimals,
|
||||
YT_POOL_MANAGER_ABI,
|
||||
YT_VAULT_ABI,
|
||||
YT_LP_TOKEN_ABI,
|
||||
YT_PRICE_FEED_ABI,
|
||||
FACTORY_ABI,
|
||||
USDY_ABI,
|
||||
} from '../../config/contracts'
|
||||
import type {
|
||||
CooldownForm,
|
||||
SwapFeesForm,
|
||||
WithdrawForm,
|
||||
AumAdjustForm,
|
||||
LimitsForm,
|
||||
MaxSwapForm,
|
||||
PermissionForm,
|
||||
PriceConfigForm,
|
||||
VaultConfigForm,
|
||||
StableTokenConfig,
|
||||
} from './types'
|
||||
|
||||
interface UseLPAdminActionsProps {
|
||||
address: `0x${string}` | undefined
|
||||
writeContract: (config: any) => void
|
||||
recordTx: (type: string, amount?: string, token?: string) => void
|
||||
}
|
||||
|
||||
export function useLPAdminActions({ address, writeContract, recordTx }: UseLPAdminActionsProps) {
|
||||
|
||||
// ===== P1: 核心配置 =====
|
||||
|
||||
// 设置冷却时间
|
||||
const handleSetCooldownDuration = (form: CooldownForm) => {
|
||||
if (!address || !form.duration) return
|
||||
recordTx('test', form.duration, 'SetCooldownDuration')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'setCooldownDuration',
|
||||
args: [BigInt(form.duration)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置交换手续费
|
||||
const handleSetSwapFees = (form: SwapFeesForm) => {
|
||||
if (!address) return
|
||||
recordTx('test', 'SwapFees', 'SetSwapFees')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setSwapFees',
|
||||
args: [
|
||||
BigInt(form.swapFee),
|
||||
BigInt(form.stableSwapFee),
|
||||
BigInt(form.taxBasisPoints),
|
||||
BigInt(form.stableTaxBasisPoints)
|
||||
],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 紧急提取代币
|
||||
const handleWithdrawToken = (form: WithdrawForm) => {
|
||||
if (!address || !form.token || !form.receiver || !form.amount) return
|
||||
recordTx('test', form.amount, 'WithdrawToken')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'withdrawToken',
|
||||
args: [
|
||||
form.token as `0x${string}`,
|
||||
form.receiver as `0x${string}`,
|
||||
parseUnits(form.amount, getTokenDecimals(form.token))
|
||||
],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// ===== P2: 交易限制 =====
|
||||
|
||||
// 设置动态手续费开关
|
||||
const handleSetDynamicFees = (enabled: boolean) => {
|
||||
if (!address) return
|
||||
recordTx('test', enabled ? 'ON' : 'OFF', 'SetDynamicFees')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setDynamicFees',
|
||||
args: [enabled],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 暂停 LP Pool
|
||||
const handlePausePool = () => {
|
||||
if (!address) return
|
||||
recordTx('test', 'Pause', 'PausePool')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'pause',
|
||||
args: [],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 恢复 LP Pool
|
||||
const handleUnpausePool = () => {
|
||||
if (!address) return
|
||||
recordTx('test', 'Unpause', 'UnpausePool')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'unpause',
|
||||
args: [],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置 AUM 调整
|
||||
const handleSetAumAdjustment = (form: AumAdjustForm) => {
|
||||
if (!address) return
|
||||
recordTx('test', `+${form.addition}/-${form.deduction}`, 'SetAumAdjustment')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'setAumAdjustment',
|
||||
args: [
|
||||
parseUnits(form.addition || '0', TOKEN_DECIMALS.USDY),
|
||||
parseUnits(form.deduction || '0', TOKEN_DECIMALS.USDY)
|
||||
],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置最大交换金额
|
||||
const handleSetMaxSwapAmount = (form: MaxSwapForm) => {
|
||||
if (!address || !form.token || !form.amount) return
|
||||
recordTx('test', form.amount, 'SetMaxSwapAmount')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setMaxSwapAmount',
|
||||
args: [
|
||||
form.token as `0x${string}`,
|
||||
parseUnits(form.amount, getTokenDecimals(form.token))
|
||||
],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置最大滑点
|
||||
const handleSetMaxSwapSlippageBps = (form: LimitsForm) => {
|
||||
if (!address || !form.maxSwapSlippageBps) return
|
||||
recordTx('test', form.maxSwapSlippageBps, 'SetMaxSwapSlippageBps')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setMaxSwapSlippageBps',
|
||||
args: [BigInt(form.maxSwapSlippageBps)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置最大价格变化
|
||||
const handleSetMaxPriceChangeBps = (form: LimitsForm) => {
|
||||
if (!address || !form.maxPriceChangeBps) return
|
||||
recordTx('test', form.maxPriceChangeBps, 'SetMaxPriceChangeBps')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setMaxPriceChangeBps',
|
||||
args: [BigInt(form.maxPriceChangeBps)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// ===== P3: 权限管理 =====
|
||||
|
||||
// 设置 PoolManager Gov
|
||||
const handleSetPoolManagerGov = (form: PermissionForm) => {
|
||||
if (!address || !form.govAddress) return
|
||||
recordTx('test', form.govAddress, 'SetPoolManagerGov')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'setGov',
|
||||
args: [form.govAddress as `0x${string}`],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置 YTVault Gov
|
||||
const handleSetVaultGov = (form: PermissionForm) => {
|
||||
if (!address || !form.govAddress) return
|
||||
recordTx('test', form.govAddress, 'SetVaultGov')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setGov',
|
||||
args: [form.govAddress as `0x${string}`],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置 Handler
|
||||
const handleSetHandler = (form: PermissionForm) => {
|
||||
if (!address || !form.handlerAddress) return
|
||||
recordTx('test', form.handlerAddress, 'SetHandler')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'setHandler',
|
||||
args: [form.handlerAddress as `0x${string}`, form.handlerActive],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置 Keeper
|
||||
const handleSetKeeper = (form: PermissionForm) => {
|
||||
if (!address || !form.keeperAddress) return
|
||||
recordTx('test', form.keeperAddress, 'SetKeeper')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setKeeper',
|
||||
args: [form.keeperAddress as `0x${string}`, form.keeperActive],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置 Swapper
|
||||
const handleSetSwapper = (form: PermissionForm) => {
|
||||
if (!address || !form.swapperAddress) return
|
||||
recordTx('test', form.swapperAddress, 'SetSwapper')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setSwapper',
|
||||
args: [form.swapperAddress as `0x${string}`, form.swapperActive],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置 PoolManager
|
||||
const handleSetPoolManager = (form: PermissionForm) => {
|
||||
if (!address || !form.poolManagerAddress) return
|
||||
recordTx('test', form.poolManagerAddress, 'SetPoolManager')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setPoolManager',
|
||||
args: [form.poolManagerAddress as `0x${string}`],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置 ytLP Minter
|
||||
const handleSetMinter = (form: PermissionForm) => {
|
||||
if (!address || !form.minterAddress) return
|
||||
recordTx('test', form.minterAddress, 'SetMinter')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: YT_LP_TOKEN_ABI,
|
||||
functionName: 'setMinter',
|
||||
args: [form.minterAddress as `0x${string}`, form.minterActive],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 价格配置 =====
|
||||
|
||||
// 设置价格
|
||||
const handleSetPrice = (form: PriceConfigForm) => {
|
||||
if (!address || !form.price) return
|
||||
recordTx('test', form.price, 'SetPrice')
|
||||
const priceIn30Decimals = parseUnits(form.price, TOKEN_DECIMALS.INTERNAL_PRICE)
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_PRICE_FEED,
|
||||
abi: YT_PRICE_FEED_ABI,
|
||||
functionName: 'setPrice',
|
||||
args: [form.token as `0x${string}`, priceIn30Decimals],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置价差
|
||||
const handleSetSpread = (form: PriceConfigForm) => {
|
||||
if (!address || !form.spreadBps) return
|
||||
recordTx('test', form.spreadBps, 'SetSpread')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_PRICE_FEED,
|
||||
abi: YT_PRICE_FEED_ABI,
|
||||
functionName: 'setSpreadBasisPoints',
|
||||
args: [form.token as `0x${string}`, BigInt(form.spreadBps)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置白名单代币
|
||||
const handleSetWhitelistedToken = (form: VaultConfigForm) => {
|
||||
if (!address || !form.token) return
|
||||
recordTx('test', form.token, 'SetWhitelistedToken')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setWhitelistedToken',
|
||||
args: [
|
||||
form.token as `0x${string}`,
|
||||
BigInt(form.tokenDecimals),
|
||||
BigInt(form.tokenWeight),
|
||||
parseUnits(form.maxUsdyAmount, TOKEN_DECIMALS.USDY),
|
||||
true
|
||||
],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 移除白名单代币
|
||||
const handleClearWhitelistedToken = (token: string) => {
|
||||
if (!address || !token) return
|
||||
recordTx('test', token, 'ClearWhitelistedToken')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'clearWhitelistedToken',
|
||||
args: [token as `0x${string}`],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置 USDC 价格源
|
||||
const handleSetUsdcPriceSource = (source: string) => {
|
||||
if (!address || !source) return
|
||||
recordTx('test', source, 'SetUsdcPriceSource')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_PRICE_FEED,
|
||||
abi: YT_PRICE_FEED_ABI,
|
||||
functionName: 'setUsdcPriceSource',
|
||||
args: [source as `0x${string}`],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置稳定币
|
||||
const handleSetStableToken = (config: StableTokenConfig) => {
|
||||
if (!address) return
|
||||
recordTx('test', config.token, 'SetStableToken')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_PRICE_FEED,
|
||||
abi: YT_PRICE_FEED_ABI,
|
||||
functionName: 'setStableTokens',
|
||||
args: [config.token as `0x${string}`, config.isStable],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置紧急模式
|
||||
const handleSetEmergencyMode = (enabled: boolean) => {
|
||||
if (!address) return
|
||||
recordTx('test', enabled ? 'ON' : 'OFF', 'SetEmergencyMode')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setEmergencyMode',
|
||||
args: [enabled],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置 Swap 开关
|
||||
const handleSetSwapEnabled = (enabled: boolean) => {
|
||||
if (!address) return
|
||||
recordTx('test', enabled ? 'ON' : 'OFF', 'SetSwapEnabled')
|
||||
writeContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'setSwapEnabled',
|
||||
args: [enabled],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 更新 ytPrice
|
||||
const handleUpdateYtPrice = (token: string, ytPrice: string, usdcPrice: bigint) => {
|
||||
if (!address || !ytPrice || !usdcPrice) return
|
||||
recordTx('test', ytPrice, 'UpdateYtPrice')
|
||||
const ytPriceIn30Decimals = parseUnits(ytPrice, TOKEN_DECIMALS.INTERNAL_PRICE)
|
||||
writeContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'updateVaultPrices',
|
||||
args: [token as `0x${string}`, usdcPrice, ytPriceIn30Decimals],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// USDY 白名单管理
|
||||
const handleAddUsdyVault = (vault: string) => {
|
||||
if (!address || !vault) return
|
||||
recordTx('test', vault, 'AddUsdyVault')
|
||||
writeContract({
|
||||
address: CONTRACTS.USDY,
|
||||
abi: USDY_ABI,
|
||||
functionName: 'addVault',
|
||||
args: [vault as `0x${string}`],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveUsdyVault = (vault: string) => {
|
||||
if (!address || !vault) return
|
||||
recordTx('test', vault, 'RemoveUsdyVault')
|
||||
writeContract({
|
||||
address: CONTRACTS.USDY,
|
||||
abi: USDY_ABI,
|
||||
functionName: 'removeVault',
|
||||
args: [vault as `0x${string}`],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
// P1
|
||||
handleSetCooldownDuration,
|
||||
handleSetSwapFees,
|
||||
handleWithdrawToken,
|
||||
// P2
|
||||
handleSetDynamicFees,
|
||||
handlePausePool,
|
||||
handleUnpausePool,
|
||||
handleSetAumAdjustment,
|
||||
handleSetMaxSwapAmount,
|
||||
handleSetMaxSwapSlippageBps,
|
||||
handleSetMaxPriceChangeBps,
|
||||
// P3
|
||||
handleSetPoolManagerGov,
|
||||
handleSetVaultGov,
|
||||
handleSetHandler,
|
||||
handleSetKeeper,
|
||||
handleSetSwapper,
|
||||
handleSetPoolManager,
|
||||
handleSetMinter,
|
||||
// 价格配置
|
||||
handleSetPrice,
|
||||
handleSetSpread,
|
||||
handleSetWhitelistedToken,
|
||||
handleClearWhitelistedToken,
|
||||
handleSetUsdcPriceSource,
|
||||
handleSetStableToken,
|
||||
handleSetEmergencyMode,
|
||||
handleSetSwapEnabled,
|
||||
handleUpdateYtPrice,
|
||||
// USDY 白名单
|
||||
handleAddUsdyVault,
|
||||
handleRemoveUsdyVault,
|
||||
}
|
||||
}
|
||||
449
frontend/src/components/LP/useLPPoolData.ts
Normal file
449
frontend/src/components/LP/useLPPoolData.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* LP 池数据读取 Hook
|
||||
* 集中管理所有池子状态的读取逻辑
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useAccount, useReadContract, useReadContracts } from 'wagmi'
|
||||
import {
|
||||
CONTRACTS,
|
||||
TOKEN_DECIMALS,
|
||||
YT_REWARD_ROUTER_ABI,
|
||||
YT_LP_TOKEN_ABI,
|
||||
YT_POOL_MANAGER_ABI,
|
||||
YT_VAULT_ABI,
|
||||
YT_PRICE_FEED_ABI,
|
||||
FACTORY_ABI,
|
||||
USDY_ABI,
|
||||
} from '../../config/contracts'
|
||||
import { ERC20_BASE_ABI, type PoolToken } from './types'
|
||||
|
||||
// 扩展的池子代币类型
|
||||
export interface ExtendedPoolToken extends PoolToken {
|
||||
balance?: bigint
|
||||
isWhitelisted?: boolean
|
||||
usdyAmount?: bigint
|
||||
isStable?: boolean
|
||||
weight?: bigint
|
||||
}
|
||||
|
||||
// 管理员配置用的代币类型
|
||||
export interface AvailableToken {
|
||||
address: string
|
||||
symbol: string
|
||||
name: string
|
||||
source: 'pool' | 'factory'
|
||||
isStable?: boolean
|
||||
isWhitelisted?: boolean
|
||||
}
|
||||
|
||||
export function useLPPoolData() {
|
||||
const { address, isConnected } = useAccount()
|
||||
|
||||
// ===== 动态获取 LP 池代币列表 =====
|
||||
const { data: rawPoolTokenAddresses } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'getAllPoolTokens',
|
||||
})
|
||||
|
||||
// ===== 从 Factory 获取所有创建的金库 =====
|
||||
const { data: factoryVaults } = useReadContract({
|
||||
address: CONTRACTS.FACTORY,
|
||||
abi: FACTORY_ABI,
|
||||
functionName: 'getAllVaults',
|
||||
})
|
||||
|
||||
// 去重处理
|
||||
const poolTokenAddresses = useMemo(() => {
|
||||
if (!rawPoolTokenAddresses || rawPoolTokenAddresses.length === 0) return []
|
||||
const seen = new Set<string>()
|
||||
return (rawPoolTokenAddresses as string[]).filter((addr: string) => {
|
||||
const lower = addr.toLowerCase()
|
||||
if (seen.has(lower)) return false
|
||||
seen.add(lower)
|
||||
return true
|
||||
})
|
||||
}, [rawPoolTokenAddresses])
|
||||
|
||||
// 批量读取代币信息
|
||||
const tokenInfoContracts = useMemo(() => {
|
||||
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
|
||||
return poolTokenAddresses.flatMap((addr: string) => [
|
||||
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'symbol' as const },
|
||||
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'name' as const },
|
||||
])
|
||||
}, [poolTokenAddresses])
|
||||
|
||||
const { data: tokenInfoResults } = useReadContracts({ contracts: tokenInfoContracts })
|
||||
|
||||
// 批量读取 Factory 金库信息
|
||||
const factoryVaultInfoContracts = useMemo(() => {
|
||||
if (!factoryVaults || factoryVaults.length === 0) return []
|
||||
return (factoryVaults as string[]).flatMap((addr: string) => [
|
||||
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'symbol' as const },
|
||||
{ address: addr as `0x${string}`, abi: ERC20_BASE_ABI, functionName: 'name' as const },
|
||||
])
|
||||
}, [factoryVaults])
|
||||
|
||||
const { data: factoryVaultInfoResults } = useReadContracts({ contracts: factoryVaultInfoContracts })
|
||||
|
||||
// 批量读取用户余额
|
||||
const balanceContracts = useMemo(() => {
|
||||
if (!poolTokenAddresses || poolTokenAddresses.length === 0 || !address) return []
|
||||
return poolTokenAddresses.map((addr: string) => ({
|
||||
address: addr as `0x${string}`,
|
||||
abi: ERC20_BASE_ABI,
|
||||
functionName: 'balanceOf' as const,
|
||||
args: [address] as const,
|
||||
}))
|
||||
}, [poolTokenAddresses, address])
|
||||
|
||||
const { data: balanceResults, refetch: refetchTokenBalances } = useReadContracts({ contracts: balanceContracts })
|
||||
|
||||
// 批量读取白名单状态
|
||||
const whitelistContracts = useMemo(() => {
|
||||
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
|
||||
return poolTokenAddresses.map((addr: string) => ({
|
||||
address: CONTRACTS.YT_VAULT as `0x${string}`,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'whitelistedTokens' as const,
|
||||
args: [addr] as const,
|
||||
}))
|
||||
}, [poolTokenAddresses])
|
||||
|
||||
const { data: whitelistResults, refetch: refetchVaultWhitelist } = useReadContracts({ contracts: whitelistContracts })
|
||||
|
||||
// 批量读取 usdyAmounts
|
||||
const usdyAmountsContracts = useMemo(() => {
|
||||
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
|
||||
return poolTokenAddresses.map((addr: string) => ({
|
||||
address: CONTRACTS.YT_VAULT as `0x${string}`,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'usdyAmounts' as const,
|
||||
args: [addr] as const,
|
||||
}))
|
||||
}, [poolTokenAddresses])
|
||||
|
||||
const { data: usdyAmountsResults, refetch: refetchUsdyAmounts } = useReadContracts({ contracts: usdyAmountsContracts })
|
||||
|
||||
// 批量读取稳定币状态
|
||||
const stableTokensContracts = useMemo(() => {
|
||||
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
|
||||
return poolTokenAddresses.map((addr: string) => ({
|
||||
address: CONTRACTS.YT_PRICE_FEED as `0x${string}`,
|
||||
abi: YT_PRICE_FEED_ABI,
|
||||
functionName: 'stableTokens' as const,
|
||||
args: [addr] as const,
|
||||
}))
|
||||
}, [poolTokenAddresses])
|
||||
|
||||
const { data: stableTokensResults } = useReadContracts({ contracts: stableTokensContracts })
|
||||
|
||||
// 批量读取代币权重
|
||||
const tokenWeightsContracts = useMemo(() => {
|
||||
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
|
||||
return poolTokenAddresses.map((addr: string) => ({
|
||||
address: CONTRACTS.YT_VAULT as `0x${string}`,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'tokenWeights' as const,
|
||||
args: [addr] as const,
|
||||
}))
|
||||
}, [poolTokenAddresses])
|
||||
|
||||
const { data: tokenWeightsResults, refetch: refetchTokenWeights } = useReadContracts({ contracts: tokenWeightsContracts })
|
||||
|
||||
// 构建代币列表
|
||||
const poolTokens = useMemo((): ExtendedPoolToken[] => {
|
||||
if (!poolTokenAddresses || poolTokenAddresses.length === 0) return []
|
||||
return poolTokenAddresses.map((addr: string, index: number) => {
|
||||
const symbol = tokenInfoResults?.[index * 2]?.result as string | undefined
|
||||
const name = tokenInfoResults?.[index * 2 + 1]?.result as string | undefined
|
||||
const balance = balanceResults?.[index]?.result as bigint | undefined
|
||||
const isWhitelisted = whitelistResults?.[index]?.result as boolean | undefined
|
||||
const usdyAmount = usdyAmountsResults?.[index]?.result as bigint | undefined
|
||||
const isStable = stableTokensResults?.[index]?.result as boolean | undefined
|
||||
const weight = tokenWeightsResults?.[index]?.result as bigint | undefined
|
||||
return {
|
||||
address: addr as `0x${string}`,
|
||||
symbol: symbol || `Token ${index}`,
|
||||
name: name || `Unknown Token ${index}`,
|
||||
decimals: TOKEN_DECIMALS.YT, // YT 代币默认 18 位
|
||||
balance,
|
||||
isWhitelisted,
|
||||
usdyAmount,
|
||||
isStable,
|
||||
weight,
|
||||
}
|
||||
})
|
||||
}, [poolTokenAddresses, tokenInfoResults, balanceResults, whitelistResults, usdyAmountsResults, stableTokensResults, tokenWeightsResults])
|
||||
|
||||
// 合并所有可用代币(用于管理员配置)
|
||||
const allAvailableTokens = useMemo((): AvailableToken[] => {
|
||||
const poolAddressSet = new Set(poolTokenAddresses?.map((a: string) => a.toLowerCase()) || [])
|
||||
|
||||
const factoryOnlyTokens: AvailableToken[] = []
|
||||
if (factoryVaults && factoryVaults.length > 0) {
|
||||
(factoryVaults as string[]).forEach((addr: string, index: number) => {
|
||||
if (!poolAddressSet.has(addr.toLowerCase())) {
|
||||
const symbol = factoryVaultInfoResults?.[index * 2]?.result as string | undefined
|
||||
const name = factoryVaultInfoResults?.[index * 2 + 1]?.result as string | undefined
|
||||
factoryOnlyTokens.push({
|
||||
address: addr,
|
||||
symbol: symbol || `Vault ${index}`,
|
||||
name: name || `Factory Vault ${index}`,
|
||||
source: 'factory',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const poolWithSource: AvailableToken[] = poolTokens.map(t => ({
|
||||
address: t.address,
|
||||
symbol: t.symbol,
|
||||
name: t.name,
|
||||
source: 'pool' as const,
|
||||
isStable: t.isStable,
|
||||
isWhitelisted: t.isWhitelisted,
|
||||
}))
|
||||
|
||||
const usdcInList = [...poolWithSource, ...factoryOnlyTokens].some(
|
||||
t => t.address.toLowerCase() === CONTRACTS.USDC.toLowerCase()
|
||||
)
|
||||
|
||||
const result: AvailableToken[] = [...poolWithSource, ...factoryOnlyTokens]
|
||||
if (!usdcInList) {
|
||||
result.push({
|
||||
address: CONTRACTS.USDC,
|
||||
symbol: 'USDC',
|
||||
name: 'USD Coin',
|
||||
source: 'pool',
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}, [poolTokens, poolTokenAddresses, factoryVaults, factoryVaultInfoResults])
|
||||
|
||||
// ===== ytLP 代币信息 =====
|
||||
const { data: ytLPBalance, refetch: refetchBalance } = useReadContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: YT_LP_TOKEN_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: address ? [address] : undefined,
|
||||
})
|
||||
|
||||
const { data: ytLPTotalSupply, refetch: refetchYtLPTotalSupply } = useReadContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: YT_LP_TOKEN_ABI,
|
||||
functionName: 'totalSupply',
|
||||
})
|
||||
|
||||
const { data: ytLPDecimals } = useReadContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: YT_LP_TOKEN_ABI,
|
||||
functionName: 'decimals',
|
||||
})
|
||||
|
||||
const { data: ytLPName } = useReadContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: YT_LP_TOKEN_ABI,
|
||||
functionName: 'name',
|
||||
})
|
||||
|
||||
const { data: ytLPSymbol } = useReadContract({
|
||||
address: CONTRACTS.YT_LP_TOKEN,
|
||||
abi: YT_LP_TOKEN_ABI,
|
||||
functionName: 'symbol',
|
||||
})
|
||||
|
||||
const { data: ytLPPrice, refetch: refetchYtLPPrice } = useReadContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'getYtLPPrice',
|
||||
})
|
||||
|
||||
// ===== 池子数据 =====
|
||||
const { data: aumInUsdy, refetch: refetchAumInUsdy } = useReadContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'getAumInUsdy',
|
||||
args: [true],
|
||||
})
|
||||
|
||||
const { data: cooldownDuration } = useReadContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'cooldownDuration',
|
||||
})
|
||||
|
||||
const { data: lastAddedAt } = useReadContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'lastAddedAt',
|
||||
args: address ? [address] : undefined,
|
||||
})
|
||||
|
||||
const { data: accountValue, refetch: refetchAccountValue } = useReadContract({
|
||||
address: CONTRACTS.YT_REWARD_ROUTER,
|
||||
abi: YT_REWARD_ROUTER_ABI,
|
||||
functionName: 'getAccountValue',
|
||||
args: address ? [address] : undefined,
|
||||
})
|
||||
|
||||
// ===== 管理状态 =====
|
||||
const { data: emergencyMode, refetch: refetchEmergencyMode } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'emergencyMode',
|
||||
})
|
||||
|
||||
const { data: swapEnabled, refetch: refetchSwapEnabled } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'isSwapEnabled',
|
||||
})
|
||||
|
||||
const { data: poolPaused } = useReadContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'paused',
|
||||
})
|
||||
|
||||
// ===== 手续费配置 =====
|
||||
const { data: swapFee } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'swapFee',
|
||||
})
|
||||
|
||||
const { data: stableSwapFee } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'stableSwapFee',
|
||||
})
|
||||
|
||||
const { data: taxBasisPoints } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'taxBasisPoints',
|
||||
})
|
||||
|
||||
const { data: stableTaxBasisPoints } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'stableTaxBasisPoints',
|
||||
})
|
||||
|
||||
const { data: dynamicFees } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'dynamicFees',
|
||||
})
|
||||
|
||||
// ===== 管理员地址 =====
|
||||
const { data: vaultGov } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'gov',
|
||||
})
|
||||
|
||||
const { data: poolManagerGov } = useReadContract({
|
||||
address: CONTRACTS.YT_POOL_MANAGER,
|
||||
abi: YT_POOL_MANAGER_ABI,
|
||||
functionName: 'gov',
|
||||
})
|
||||
|
||||
const { data: priceFeedGov } = useReadContract({
|
||||
address: CONTRACTS.YT_PRICE_FEED,
|
||||
abi: YT_PRICE_FEED_ABI,
|
||||
functionName: 'gov',
|
||||
})
|
||||
|
||||
const { data: usdcPriceSource } = useReadContract({
|
||||
address: CONTRACTS.YT_PRICE_FEED,
|
||||
abi: YT_PRICE_FEED_ABI,
|
||||
functionName: 'usdcPriceSource',
|
||||
})
|
||||
|
||||
// ===== USDY 信息 =====
|
||||
const { data: usdyTotalSupply, refetch: refetchUsdySupply } = useReadContract({
|
||||
address: CONTRACTS.USDY,
|
||||
abi: USDY_ABI,
|
||||
functionName: 'totalSupply',
|
||||
})
|
||||
|
||||
const { data: poolValue, refetch: refetchPoolValue } = useReadContract({
|
||||
address: CONTRACTS.YT_VAULT,
|
||||
abi: YT_VAULT_ABI,
|
||||
functionName: 'getPoolValue',
|
||||
})
|
||||
|
||||
// 辅助函数:根据地址获取代币符号
|
||||
const getTokenSymbol = (tokenAddress: string): string => {
|
||||
const token = poolTokens.find(t => t.address.toLowerCase() === tokenAddress.toLowerCase())
|
||||
return token?.symbol || 'Token'
|
||||
}
|
||||
|
||||
return {
|
||||
// 连接状态
|
||||
address,
|
||||
isConnected,
|
||||
|
||||
// 代币列表
|
||||
poolTokens,
|
||||
allAvailableTokens,
|
||||
poolTokenAddresses,
|
||||
|
||||
// ytLP 数据
|
||||
ytLPBalance,
|
||||
ytLPTotalSupply,
|
||||
ytLPDecimals,
|
||||
ytLPName,
|
||||
ytLPSymbol,
|
||||
ytLPPrice,
|
||||
|
||||
// 池子数据
|
||||
aumInUsdy,
|
||||
cooldownDuration,
|
||||
lastAddedAt,
|
||||
accountValue,
|
||||
poolValue,
|
||||
usdyTotalSupply,
|
||||
|
||||
// 管理状态
|
||||
emergencyMode,
|
||||
swapEnabled,
|
||||
poolPaused,
|
||||
|
||||
// 手续费
|
||||
swapFee,
|
||||
stableSwapFee,
|
||||
taxBasisPoints,
|
||||
stableTaxBasisPoints,
|
||||
dynamicFees,
|
||||
|
||||
// 管理员地址
|
||||
vaultGov,
|
||||
poolManagerGov,
|
||||
priceFeedGov,
|
||||
usdcPriceSource,
|
||||
|
||||
// 辅助函数
|
||||
getTokenSymbol,
|
||||
|
||||
// Refetch 函数
|
||||
refetchBalance,
|
||||
refetchTokenBalances,
|
||||
refetchYtLPTotalSupply,
|
||||
refetchYtLPPrice,
|
||||
refetchAumInUsdy,
|
||||
refetchAccountValue,
|
||||
refetchUsdyAmounts,
|
||||
refetchTokenWeights,
|
||||
refetchEmergencyMode,
|
||||
refetchSwapEnabled,
|
||||
refetchUsdySupply,
|
||||
refetchPoolValue,
|
||||
refetchVaultWhitelist,
|
||||
}
|
||||
}
|
||||
208
frontend/src/components/LP/useLPTransactions.ts
Normal file
208
frontend/src/components/LP/useLPTransactions.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* LP 交易处理 Hook
|
||||
* 集中管理所有交易逻辑
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
|
||||
import { useTransactions } from '../../context/TransactionContext'
|
||||
import type { TransactionType } from '../../context/TransactionContext'
|
||||
import { useToast } from '../Toast'
|
||||
|
||||
interface TransactionCallbacks {
|
||||
onSuccess?: () => void
|
||||
onError?: (error: string) => void
|
||||
}
|
||||
|
||||
export function useLPTransactions(callbacks?: TransactionCallbacks) {
|
||||
const { t } = useTranslation()
|
||||
const { addTransaction, updateTransaction } = useTransactions()
|
||||
const { showToast } = useToast()
|
||||
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
|
||||
|
||||
// 使用 ref 存储 callbacks,避免作为依赖项
|
||||
const callbacksRef = useRef(callbacks)
|
||||
callbacksRef.current = callbacks
|
||||
|
||||
const { writeContract, data: hash, isPending, error: writeError, reset, status: writeStatus } = useWriteContract()
|
||||
|
||||
const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({
|
||||
hash,
|
||||
})
|
||||
|
||||
// 使用 ref 追踪已处理的状态,防止重复触发
|
||||
const processedHashRef = useRef<string | null>(null)
|
||||
const successProcessedRef = useRef(false)
|
||||
const errorProcessedRef = useRef(false)
|
||||
const writeErrorProcessedRef = useRef<Error | null>(null)
|
||||
|
||||
// 超时自动重置机制
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout | null = null
|
||||
if (isPending && !hash) {
|
||||
timeoutId = setTimeout(() => {
|
||||
console.log('Transaction timeout, resetting state...')
|
||||
if (pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: 'Transaction timeout' })
|
||||
pendingTxRef.current = null
|
||||
}
|
||||
showToast('error', t('toast.txTimeout') || 'Transaction timeout')
|
||||
reset()
|
||||
}, 30000)
|
||||
}
|
||||
return () => {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
}
|
||||
}, [isPending, hash, t, showToast, reset, updateTransaction])
|
||||
|
||||
// 处理交易提交
|
||||
useEffect(() => {
|
||||
if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) {
|
||||
// 防止重复处理
|
||||
if (processedHashRef.current === hash) return
|
||||
processedHashRef.current = hash
|
||||
updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' })
|
||||
showToast('info', t('toast.txSubmitted'))
|
||||
}
|
||||
}, [hash, t, showToast, updateTransaction])
|
||||
|
||||
// 处理交易成功
|
||||
useEffect(() => {
|
||||
if (isSuccess && !successProcessedRef.current) {
|
||||
successProcessedRef.current = true
|
||||
if (pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'success' })
|
||||
showToast('success', t('toast.txSuccess'))
|
||||
pendingTxRef.current = null
|
||||
}
|
||||
callbacksRef.current?.onSuccess?.()
|
||||
reset()
|
||||
// 重置标记
|
||||
processedHashRef.current = null
|
||||
successProcessedRef.current = false
|
||||
}
|
||||
}, [isSuccess, t, showToast, updateTransaction, reset])
|
||||
|
||||
// 处理交易失败
|
||||
useEffect(() => {
|
||||
if (isError && pendingTxRef.current && !errorProcessedRef.current) {
|
||||
errorProcessedRef.current = true
|
||||
const errorMsg = typeof txError?.message === 'string' ? txError.message : 'Transaction failed'
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
|
||||
showToast('error', t('toast.txFailed'))
|
||||
pendingTxRef.current = null
|
||||
callbacksRef.current?.onError?.(errorMsg)
|
||||
reset()
|
||||
processedHashRef.current = null
|
||||
// 延迟重置
|
||||
setTimeout(() => { errorProcessedRef.current = false }, 100)
|
||||
}
|
||||
}, [isError, txError, t, showToast, updateTransaction, reset])
|
||||
|
||||
// 处理写入错误
|
||||
useEffect(() => {
|
||||
if (writeError && writeErrorProcessedRef.current !== writeError) {
|
||||
writeErrorProcessedRef.current = writeError
|
||||
const errorMsg = parseError(writeError, t)
|
||||
showToast('error', errorMsg)
|
||||
if (pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
|
||||
pendingTxRef.current = null
|
||||
}
|
||||
callbacksRef.current?.onError?.(errorMsg)
|
||||
reset()
|
||||
processedHashRef.current = null
|
||||
}
|
||||
}, [writeError, t, showToast, updateTransaction, reset])
|
||||
|
||||
// 错误解析
|
||||
const parseError = (error: any, t: any): string => {
|
||||
let msg = 'Unknown error'
|
||||
if (typeof error === 'string') {
|
||||
msg = error
|
||||
} else if (error?.shortMessage) {
|
||||
msg = error.shortMessage
|
||||
} else if (typeof error?.message === 'string') {
|
||||
msg = error.message
|
||||
} else if (error?.message) {
|
||||
msg = JSON.stringify(error.message)
|
||||
} else if (error) {
|
||||
try { msg = JSON.stringify(error) } catch { msg = String(error) }
|
||||
}
|
||||
|
||||
if (msg.includes('User rejected') || msg.includes('user rejected')) {
|
||||
return t('toast.userRejected')
|
||||
}
|
||||
if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance') || msg.includes('less than block base fee')) {
|
||||
return t('toast.insufficientBalance') + ' (Gas)'
|
||||
}
|
||||
if (msg.includes('CooldownNotPassed')) {
|
||||
return t('lp.cooldownNotPassed')
|
||||
}
|
||||
if (msg.includes('InsufficientOutput')) {
|
||||
return t('lp.insufficientOutput')
|
||||
}
|
||||
const match = msg.match(/error[:\s]+(\w+)/i)
|
||||
if (match) return match[1]
|
||||
return msg.slice(0, 100)
|
||||
}
|
||||
|
||||
// 记录交易
|
||||
const recordTx = (type: TransactionType, amount?: string, token?: string) => {
|
||||
const id = addTransaction({
|
||||
type,
|
||||
hash: '',
|
||||
status: 'pending',
|
||||
amount,
|
||||
token,
|
||||
})
|
||||
pendingTxRef.current = { id, type, amount }
|
||||
}
|
||||
|
||||
// 计算按钮是否应该禁用
|
||||
const isProcessing = (isPending || isConfirming) && writeStatus !== 'error'
|
||||
|
||||
return {
|
||||
writeContract,
|
||||
recordTx,
|
||||
isProcessing,
|
||||
isPending,
|
||||
isConfirming,
|
||||
hash,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
|
||||
export { parseError }
|
||||
|
||||
function parseError(error: any, t: any): string {
|
||||
let msg = 'Unknown error'
|
||||
if (typeof error === 'string') {
|
||||
msg = error
|
||||
} else if (error?.shortMessage) {
|
||||
msg = error.shortMessage
|
||||
} else if (typeof error?.message === 'string') {
|
||||
msg = error.message
|
||||
} else if (error?.message) {
|
||||
msg = JSON.stringify(error.message)
|
||||
} else if (error) {
|
||||
try { msg = JSON.stringify(error) } catch { msg = String(error) }
|
||||
}
|
||||
|
||||
if (msg.includes('User rejected') || msg.includes('user rejected')) {
|
||||
return t('toast.userRejected')
|
||||
}
|
||||
if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance') || msg.includes('less than block base fee')) {
|
||||
return t('toast.insufficientBalance') + ' (Gas)'
|
||||
}
|
||||
if (msg.includes('CooldownNotPassed')) {
|
||||
return t('lp.cooldownNotPassed')
|
||||
}
|
||||
if (msg.includes('InsufficientOutput')) {
|
||||
return t('lp.insufficientOutput')
|
||||
}
|
||||
const match = msg.match(/error[:\s]+(\w+)/i)
|
||||
if (match) return match[1]
|
||||
return msg.slice(0, 100)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1208
frontend/src/components/LendingAdminPanel.tsx
Normal file
1208
frontend/src/components/LendingAdminPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
313
frontend/src/components/LendingPanel.css
Normal file
313
frontend/src/components/LendingPanel.css
Normal file
@@ -0,0 +1,313 @@
|
||||
.lending-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lending-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.lending-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.contract-info {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.contract-info .label {
|
||||
font-weight: 500;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.contract-info .value {
|
||||
font-family: monospace;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 账户数据 */
|
||||
.account-data {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.account-data h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.data-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.data-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: var(--color-bg-section);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.data-item .label {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.data-item .value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.data-item .value.warning {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* 系统信息 */
|
||||
.system-info {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.system-info h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
background: var(--color-bg-section);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 操作区域 */
|
||||
.operations {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.operation-form {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.select-input,
|
||||
.amount-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.select-input:focus,
|
||||
.amount-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--color-primary-light);
|
||||
border: 1px solid var(--color-primary);
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.action-button:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 抵押品明细 */
|
||||
.collateral-details {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.collateral-details h3 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.details-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.details-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.details-table th {
|
||||
text-align: left;
|
||||
padding: 12px 16px;
|
||||
background: var(--color-bg-section);
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.details-table td {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.details-table tbody tr:hover {
|
||||
background: var(--color-bg-section);
|
||||
}
|
||||
|
||||
/* 连接提示 */
|
||||
.connect-prompt {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
background: var(--color-bg-card);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.connect-prompt p {
|
||||
font-size: 16px;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.lending-panel {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.data-grid,
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1 1 auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
2248
frontend/src/components/LendingPanel.tsx
Normal file
2248
frontend/src/components/LendingPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, createContext, useContext } from 'react'
|
||||
import { useState, useEffect, createContext, useContext, useCallback } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'info' | 'warning'
|
||||
@@ -19,14 +19,14 @@ const ToastContext = createContext<ToastContextType | null>(null)
|
||||
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
const [toasts, setToasts] = useState<Toast[]>([])
|
||||
|
||||
const showToast = (type: ToastType, message: string, duration = 4000) => {
|
||||
const showToast = useCallback((type: ToastType, message: string, duration = 4000) => {
|
||||
const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
setToasts(prev => [...prev, { id, type, message, duration }])
|
||||
}
|
||||
}, [])
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id))
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast }}>
|
||||
|
||||
@@ -23,8 +23,8 @@ export function TransactionHistory({ transactions, onClear }: Props) {
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
mint: 'Mint WUSD',
|
||||
burn: 'Burn WUSD',
|
||||
mint: 'Mint USDC',
|
||||
burn: 'Burn USDC',
|
||||
buy: 'Buy YT',
|
||||
sell: 'Sell YT',
|
||||
approve: 'Approve',
|
||||
|
||||
297
frontend/src/components/USDCPanel.tsx
Normal file
297
frontend/src/components/USDCPanel.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, useChainId } from 'wagmi'
|
||||
import { parseUnits, formatUnits } from 'viem'
|
||||
import { GAS_CONFIG, USDC_ABI, getContracts, getDecimals, getChainName } from '../config/contracts'
|
||||
import { useTransactions } from '../context/TransactionContext'
|
||||
import type { TransactionType } from '../context/TransactionContext'
|
||||
import { useToast } from './Toast'
|
||||
import { TransactionHistory } from './TransactionHistory'
|
||||
|
||||
// USDC 精度备用值 - 使用统一常量
|
||||
|
||||
export function USDCPanel() {
|
||||
const { t } = useTranslation()
|
||||
const { address, isConnected } = useAccount()
|
||||
// ===== 多链支持 =====
|
||||
const chainId = useChainId()
|
||||
const CONTRACTS = getContracts(chainId)
|
||||
const TOKEN_DECIMALS = getDecimals(chainId)
|
||||
const currentChainName = getChainName(chainId)
|
||||
const { transactions, addTransaction, updateTransaction, clearHistory } = useTransactions()
|
||||
const { showToast } = useToast()
|
||||
const [transferTo, setTransferTo] = useState('')
|
||||
const [transferAmount, setTransferAmount] = useState('')
|
||||
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
|
||||
|
||||
const { data: balance, refetch: refetchBalance } = useReadContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: address ? [address] : undefined,
|
||||
})
|
||||
|
||||
const { data: symbol } = useReadContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'symbol',
|
||||
})
|
||||
|
||||
const { data: decimals } = useReadContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'decimals',
|
||||
})
|
||||
|
||||
const { data: totalSupply, refetch: refetchTotalSupply } = useReadContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'totalSupply',
|
||||
})
|
||||
|
||||
const { data: name } = useReadContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'name',
|
||||
})
|
||||
|
||||
const { writeContract, data: hash, isPending, error: writeError, reset, status: writeStatus } = useWriteContract()
|
||||
|
||||
const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({
|
||||
hash,
|
||||
})
|
||||
|
||||
// 超时自动重置机制 - 解决WalletConnect通信超时问题
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout | null = null
|
||||
if (isPending && !hash) {
|
||||
// 如果30秒内没有响应,自动重置
|
||||
timeoutId = setTimeout(() => {
|
||||
console.log('Transaction timeout, resetting state...')
|
||||
if (pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: 'Transaction timeout' })
|
||||
pendingTxRef.current = null
|
||||
}
|
||||
showToast('error', t('toast.txTimeout') || 'Transaction timeout')
|
||||
reset()
|
||||
}, 30000)
|
||||
}
|
||||
return () => {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
}
|
||||
}, [isPending, hash])
|
||||
|
||||
// 处理交易提交
|
||||
useEffect(() => {
|
||||
// 验证 hash 是有效的交易哈希字符串
|
||||
if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' })
|
||||
showToast('info', t('toast.txSubmitted'))
|
||||
}
|
||||
}, [hash])
|
||||
|
||||
// 处理交易成功
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
if (pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'success' })
|
||||
showToast('success', t('toast.txSuccess'))
|
||||
pendingTxRef.current = null
|
||||
}
|
||||
refetchBalance()
|
||||
refetchTotalSupply()
|
||||
setTransferTo('')
|
||||
setTransferAmount('')
|
||||
}
|
||||
}, [isSuccess])
|
||||
|
||||
// 处理交易失败
|
||||
useEffect(() => {
|
||||
if (isError && pendingTxRef.current) {
|
||||
const errMsg = typeof txError?.message === 'string' ? txError.message : 'Transaction failed'
|
||||
updateTransaction(pendingTxRef.current.id, {
|
||||
status: 'failed',
|
||||
error: errMsg
|
||||
})
|
||||
showToast('error', t('toast.txFailed'))
|
||||
pendingTxRef.current = null
|
||||
// 重置状态,允许用户重新操作
|
||||
reset()
|
||||
}
|
||||
}, [isError])
|
||||
|
||||
// 处理写入错误
|
||||
useEffect(() => {
|
||||
if (writeError) {
|
||||
const errorMsg = parseError(writeError)
|
||||
showToast('error', errorMsg)
|
||||
if (pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
|
||||
pendingTxRef.current = null
|
||||
}
|
||||
// 重置状态,允许用户重新操作
|
||||
reset()
|
||||
}
|
||||
}, [writeError])
|
||||
|
||||
const parseError = (error: any): string => {
|
||||
// 确保获取字符串形式的错误信息
|
||||
let msg = 'Unknown error'
|
||||
if (typeof error === 'string') {
|
||||
msg = error
|
||||
} else if (error?.shortMessage) {
|
||||
msg = error.shortMessage
|
||||
} else if (typeof error?.message === 'string') {
|
||||
msg = error.message
|
||||
} else if (error?.message) {
|
||||
msg = JSON.stringify(error.message)
|
||||
} else if (error) {
|
||||
try { msg = JSON.stringify(error) } catch { msg = String(error) }
|
||||
}
|
||||
|
||||
if (msg.includes('User rejected') || msg.includes('user rejected')) {
|
||||
return t('toast.userRejected')
|
||||
}
|
||||
if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance') || msg.includes('less than block base fee')) {
|
||||
return t('toast.insufficientBalance') + ' (Gas)'
|
||||
}
|
||||
const match = msg.match(/error[:\s]+(\w+)/i)
|
||||
if (match) return match[1]
|
||||
return msg.slice(0, 100)
|
||||
}
|
||||
|
||||
const recordTx = (type: TransactionType, amount?: string, token?: string) => {
|
||||
const id = addTransaction({
|
||||
type,
|
||||
hash: '',
|
||||
status: 'pending',
|
||||
amount,
|
||||
token,
|
||||
})
|
||||
pendingTxRef.current = { id, type, amount }
|
||||
}
|
||||
|
||||
// 计算按钮是否应该禁用 - 排除错误状态
|
||||
const isProcessing = (isPending || isConfirming) && writeStatus !== 'error'
|
||||
|
||||
const handleTransfer = async () => {
|
||||
const tokenDecimals = decimals ?? TOKEN_DECIMALS.USDC
|
||||
if (!address || !transferTo || !transferAmount) return
|
||||
// 验证地址格式
|
||||
if (!transferTo.startsWith('0x') || transferTo.length !== 42) {
|
||||
showToast('error', t('usdc.invalidAddress'))
|
||||
return
|
||||
}
|
||||
recordTx('transfer', transferAmount, 'USDC')
|
||||
writeContract({
|
||||
address: CONTRACTS.USDC,
|
||||
abi: USDC_ABI,
|
||||
functionName: 'transfer',
|
||||
args: [transferTo as `0x${string}`, parseUnits(transferAmount, tokenDecimals)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="panel">
|
||||
<h2>USDC</h2>
|
||||
<p className="text-muted">{t('common.connectFirst')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const tokenDecimals = decimals ?? TOKEN_DECIMALS.USDC
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
<h2>{t('usdc.title')} <span style={{ fontSize: '12px', color: '#666', fontWeight: 'normal' }}>({currentChainName})</span></h2>
|
||||
|
||||
{/* 代币信息卡片 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '12px', marginBottom: '16px' }}>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="代币名称 Token Name - USDC 稳定币的完整名称">名称</div>
|
||||
<strong style={{ fontSize: '14px' }}>{name || 'USD Coin'}</strong>
|
||||
</div>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="代币符号 Token Symbol - USDC 稳定币的交易符号">符号</div>
|
||||
<strong style={{ fontSize: '14px' }}>{symbol || 'USDC'}</strong>
|
||||
</div>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title={`精度 Decimals - 当前链 ${currentChainName} 的 USDC 使用 ${tokenDecimals} 位小数精度`}>精度</div>
|
||||
<strong style={{ fontSize: '14px' }}>{tokenDecimals.toString()}</strong>
|
||||
</div>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px', gridColumn: 'span 2' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="合约地址 Contract Address - USDC 稳定币的 ERC20 合约地址">{t('common.contract')}</div>
|
||||
<code style={{ fontSize: '11px', wordBreak: 'break-all' }}>{CONTRACTS.USDC}</code>
|
||||
</div>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="总供应量 Total Supply - USDC 代币的全网发行总量">{t('usdc.totalSupply')}</div>
|
||||
<strong style={{ fontSize: '14px' }}>
|
||||
{totalSupply !== undefined
|
||||
? Number(formatUnits(totalSupply, tokenDecimals)).toLocaleString()
|
||||
: '0'}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 用户余额 */}
|
||||
<div style={{ padding: '16px', background: 'linear-gradient(135deg, #2775ca 0%, #1e5aa8 100%)', borderRadius: '12px', marginBottom: '16px', color: 'white' }}>
|
||||
<div style={{ fontSize: '12px', opacity: 0.9, marginBottom: '4px' }}>{t('common.balance')}</div>
|
||||
<strong style={{ fontSize: '24px' }}>
|
||||
{balance !== undefined
|
||||
? Number(formatUnits(balance, tokenDecimals)).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })
|
||||
: '0.00'} {symbol || 'USDC'}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<div style={{ padding: '12px', background: '#f5f5f5', borderRadius: '8px', marginBottom: '16px', fontSize: '13px', color: '#666' }}>
|
||||
<strong>提示:</strong> USDC 是 Circle 发行的标准稳定币,需要通过交易所或水龙头获取。
|
||||
</div>
|
||||
|
||||
{/* 转账功能 */}
|
||||
<div style={{ padding: '16px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', marginBottom: '12px' }}>
|
||||
<h4 style={{ margin: '0 0 12px 0', fontSize: '14px', color: '#333', cursor: 'help' }} title="转账 Transfer - 将 USDC 转移到其他钱包地址">{t('usdc.transfer')}</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 120px', gap: '12px', alignItems: 'end' }}>
|
||||
<div>
|
||||
<div className="form-group" style={{ marginBottom: '8px' }}>
|
||||
<label style={{ fontSize: '12px', color: '#666' }}>{t('usdc.toAddress')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={transferTo}
|
||||
onChange={(e) => setTransferTo(e.target.value)}
|
||||
placeholder="0x..."
|
||||
className="input"
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group" style={{ marginBottom: '0' }}>
|
||||
<label style={{ fontSize: '12px', color: '#666' }}>{t('usdc.transferAmount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={transferAmount}
|
||||
onChange={(e) => setTransferAmount(e.target.value)}
|
||||
placeholder={t('usdc.enterAmount')}
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleTransfer}
|
||||
disabled={isProcessing || !transferTo || !transferAmount}
|
||||
className="btn btn-primary"
|
||||
style={{ height: '40px' }}
|
||||
>
|
||||
{isProcessing ? '...' : t('usdc.transfer')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 交易历史 */}
|
||||
<TransactionHistory transactions={transactions} onClear={clearHistory} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,465 +0,0 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
|
||||
import { parseUnits, formatUnits } from 'viem'
|
||||
import { CONTRACTS, GAS_CONFIG, WUSD_ABI } from '../config/contracts'
|
||||
import { useTransactions } from '../context/TransactionContext'
|
||||
import type { TransactionType } from '../context/TransactionContext'
|
||||
import { useToast } from './Toast'
|
||||
import { TransactionHistory } from './TransactionHistory'
|
||||
|
||||
export function WUSDPanel() {
|
||||
const { t } = useTranslation()
|
||||
const { address, isConnected } = useAccount()
|
||||
const { transactions, addTransaction, updateTransaction, clearHistory } = useTransactions()
|
||||
const { showToast } = useToast()
|
||||
const [mintAmount, setMintAmount] = useState('')
|
||||
const [burnAmount, setBurnAmount] = useState('')
|
||||
const [transferTo, setTransferTo] = useState('')
|
||||
const [transferAmount, setTransferAmount] = useState('')
|
||||
const [showBoundaryTest, setShowBoundaryTest] = useState(false)
|
||||
const pendingTxRef = useRef<{ id: string; type: TransactionType; amount?: string } | null>(null)
|
||||
|
||||
const { data: balance, refetch: refetchBalance } = useReadContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'balanceOf',
|
||||
args: address ? [address] : undefined,
|
||||
})
|
||||
|
||||
const { data: symbol } = useReadContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'symbol',
|
||||
})
|
||||
|
||||
const { data: decimals } = useReadContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'decimals',
|
||||
})
|
||||
|
||||
const { data: totalSupply, refetch: refetchTotalSupply } = useReadContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'totalSupply',
|
||||
})
|
||||
|
||||
const { data: owner } = useReadContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'owner',
|
||||
})
|
||||
|
||||
const { data: name } = useReadContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'name',
|
||||
})
|
||||
|
||||
const { writeContract, data: hash, isPending, error: writeError, reset, status: writeStatus } = useWriteContract()
|
||||
|
||||
const { isLoading: isConfirming, isSuccess, isError, error: txError } = useWaitForTransactionReceipt({
|
||||
hash,
|
||||
})
|
||||
|
||||
// 超时自动重置机制 - 解决WalletConnect通信超时问题
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout | null = null
|
||||
if (isPending && !hash) {
|
||||
// 如果30秒内没有响应,自动重置
|
||||
timeoutId = setTimeout(() => {
|
||||
console.log('Transaction timeout, resetting state...')
|
||||
if (pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: 'Transaction timeout' })
|
||||
pendingTxRef.current = null
|
||||
}
|
||||
showToast('error', t('toast.txTimeout') || 'Transaction timeout')
|
||||
reset()
|
||||
}, 30000)
|
||||
}
|
||||
return () => {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
}
|
||||
}, [isPending, hash])
|
||||
|
||||
// 处理交易提交
|
||||
useEffect(() => {
|
||||
// 验证 hash 是有效的交易哈希字符串
|
||||
if (hash && typeof hash === 'string' && hash.startsWith('0x') && pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { hash, status: 'pending' })
|
||||
showToast('info', t('toast.txSubmitted'))
|
||||
}
|
||||
}, [hash])
|
||||
|
||||
// 处理交易成功
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
if (pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'success' })
|
||||
showToast('success', t('toast.txSuccess'))
|
||||
pendingTxRef.current = null
|
||||
}
|
||||
refetchBalance()
|
||||
refetchTotalSupply()
|
||||
setMintAmount('')
|
||||
setBurnAmount('')
|
||||
setTransferTo('')
|
||||
setTransferAmount('')
|
||||
}
|
||||
}, [isSuccess])
|
||||
|
||||
// 处理交易失败
|
||||
useEffect(() => {
|
||||
if (isError && pendingTxRef.current) {
|
||||
const errMsg = typeof txError?.message === 'string' ? txError.message : 'Transaction failed'
|
||||
updateTransaction(pendingTxRef.current.id, {
|
||||
status: 'failed',
|
||||
error: errMsg
|
||||
})
|
||||
showToast('error', t('toast.txFailed'))
|
||||
pendingTxRef.current = null
|
||||
// 重置状态,允许用户重新操作
|
||||
reset()
|
||||
}
|
||||
}, [isError])
|
||||
|
||||
// 处理写入错误
|
||||
useEffect(() => {
|
||||
if (writeError) {
|
||||
const errorMsg = parseError(writeError)
|
||||
showToast('error', errorMsg)
|
||||
if (pendingTxRef.current) {
|
||||
updateTransaction(pendingTxRef.current.id, { status: 'failed', error: errorMsg })
|
||||
pendingTxRef.current = null
|
||||
}
|
||||
// 重置状态,允许用户重新操作
|
||||
reset()
|
||||
}
|
||||
}, [writeError])
|
||||
|
||||
const parseError = (error: any): string => {
|
||||
// 确保获取字符串形式的错误信息
|
||||
let msg = 'Unknown error'
|
||||
if (typeof error === 'string') {
|
||||
msg = error
|
||||
} else if (error?.shortMessage) {
|
||||
msg = error.shortMessage
|
||||
} else if (typeof error?.message === 'string') {
|
||||
msg = error.message
|
||||
} else if (error?.message) {
|
||||
msg = JSON.stringify(error.message)
|
||||
} else if (error) {
|
||||
try { msg = JSON.stringify(error) } catch { msg = String(error) }
|
||||
}
|
||||
|
||||
if (msg.includes('User rejected') || msg.includes('user rejected')) {
|
||||
return t('toast.userRejected')
|
||||
}
|
||||
if (msg.includes('insufficient funds') || msg.includes('InsufficientBalance') || msg.includes('less than block base fee')) {
|
||||
return t('toast.insufficientBalance') + ' (Gas)'
|
||||
}
|
||||
const match = msg.match(/error[:\s]+(\w+)/i)
|
||||
if (match) return match[1]
|
||||
return msg.slice(0, 100)
|
||||
}
|
||||
|
||||
const recordTx = (type: TransactionType, amount?: string, token?: string) => {
|
||||
const id = addTransaction({
|
||||
type,
|
||||
hash: '',
|
||||
status: 'pending',
|
||||
amount,
|
||||
token,
|
||||
})
|
||||
pendingTxRef.current = { id, type, amount }
|
||||
}
|
||||
|
||||
// 计算按钮是否应该禁用 - 排除错误状态
|
||||
const isProcessing = (isPending || isConfirming) && writeStatus !== 'error'
|
||||
|
||||
const handleMint = async () => {
|
||||
if (!address || !mintAmount || !decimals) return
|
||||
recordTx('mint', mintAmount, 'WUSD')
|
||||
writeContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'mint',
|
||||
args: [address, parseUnits(mintAmount, decimals)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
const handleBurn = async () => {
|
||||
if (!address || !burnAmount || !decimals) return
|
||||
recordTx('burn', burnAmount, 'WUSD')
|
||||
writeContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'burn',
|
||||
args: [address, parseUnits(burnAmount, decimals)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
const handleTransfer = async () => {
|
||||
if (!address || !transferTo || !transferAmount || !decimals) return
|
||||
// 验证地址格式
|
||||
if (!transferTo.startsWith('0x') || transferTo.length !== 42) {
|
||||
showToast('error', t('wusd.invalidAddress'))
|
||||
return
|
||||
}
|
||||
recordTx('transfer', transferAmount, 'WUSD')
|
||||
writeContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'transfer',
|
||||
args: [transferTo as `0x${string}`, parseUnits(transferAmount, decimals)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
}
|
||||
|
||||
// 边界测试函数
|
||||
const runBoundaryTest = (testType: string) => {
|
||||
if (!address || !decimals) return
|
||||
recordTx('test', undefined, 'WUSD')
|
||||
switch (testType) {
|
||||
case 'mint_zero':
|
||||
writeContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'mint',
|
||||
args: [address, BigInt(0)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
case 'burn_exceed':
|
||||
const exceedAmount = balance ? balance + parseUnits('999999', decimals) : parseUnits('999999999', decimals)
|
||||
writeContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'burn',
|
||||
args: [address, exceedAmount],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
case 'mint_10000':
|
||||
pendingTxRef.current!.amount = '10000'
|
||||
writeContract({
|
||||
address: CONTRACTS.WUSD,
|
||||
abi: WUSD_ABI,
|
||||
functionName: 'mint',
|
||||
args: [address, parseUnits('10000', decimals)],
|
||||
gas: GAS_CONFIG.STANDARD,
|
||||
maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS,
|
||||
maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="panel">
|
||||
<h2>WUSD</h2>
|
||||
<p className="text-muted">{t('common.connectFirst')}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
<h2>{t('wusd.title')}</h2>
|
||||
|
||||
{/* 代币信息卡片 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '12px', marginBottom: '16px' }}>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="代币名称 Token Name - WUSD 稳定币的完整名称">名称</div>
|
||||
<strong style={{ fontSize: '14px' }}>{name || 'WUSD'}</strong>
|
||||
</div>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="代币符号 Token Symbol - WUSD 稳定币的交易符号">符号</div>
|
||||
<strong style={{ fontSize: '14px' }}>{symbol || 'WUSD'}</strong>
|
||||
</div>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="精度 Decimals - 代币小数位数">精度</div>
|
||||
<strong style={{ fontSize: '14px' }}>{decimals?.toString() || '18'}</strong>
|
||||
</div>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="合约地址 Contract Address - WUSD 稳定币的 ERC20 合约地址">{t('common.contract')}</div>
|
||||
<code style={{ fontSize: '11px', wordBreak: 'break-all' }}>{CONTRACTS.WUSD}</code>
|
||||
</div>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="所有者 Owner - WUSD 合约的所有者地址,拥有铸造和销毁权限">{t('wusd.owner')}</div>
|
||||
<code style={{ fontSize: '11px', wordBreak: 'break-all' }}>{owner ? `${(owner as string).slice(0, 10)}...${(owner as string).slice(-8)}` : '-'}</code>
|
||||
</div>
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="总供应量 Total Supply - WUSD 代币的全网发行总量">{t('wusd.totalSupply')}</div>
|
||||
<strong style={{ fontSize: '14px' }}>
|
||||
{totalSupply !== undefined && decimals !== undefined
|
||||
? Number(formatUnits(totalSupply, decimals)).toLocaleString()
|
||||
: '0'}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 用户余额 */}
|
||||
<div style={{ padding: '12px', background: '#f8f9fa', borderRadius: '8px', marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px', cursor: 'help' }} title="余额 Balance - 你钱包中持有的 WUSD 稳定币数量">{t('common.balance')}</div>
|
||||
<strong style={{ fontSize: '18px' }}>
|
||||
{balance !== undefined && decimals !== undefined
|
||||
? Number(formatUnits(balance, decimals)).toLocaleString()
|
||||
: '0'} {symbol || 'WUSD'}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
{/* 铸造和销毁 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
|
||||
<div style={{ padding: '12px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
|
||||
<div className="form-group" style={{ marginBottom: '8px' }}>
|
||||
<label style={{ fontSize: '13px', fontWeight: 500, cursor: 'help' }} title="铸造数量 Mint Amount - 输入要铸造的 WUSD 数量(仅 Owner 可用)">{t('wusd.mintAmount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={mintAmount}
|
||||
onChange={(e) => setMintAmount(e.target.value)}
|
||||
placeholder={t('wusd.enterAmount')}
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleMint}
|
||||
disabled={isProcessing || !mintAmount}
|
||||
className="btn btn-primary"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{isProcessing ? t('common.processing') : t('wusd.mint')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '12px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
|
||||
<div className="form-group" style={{ marginBottom: '8px' }}>
|
||||
<label style={{ fontSize: '13px', fontWeight: 500, cursor: 'help' }} title="销毁数量 Burn Amount - 输入要销毁的 WUSD 数量(仅 Owner 可用)">{t('wusd.burnAmount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={burnAmount}
|
||||
onChange={(e) => setBurnAmount(e.target.value)}
|
||||
placeholder={t('wusd.enterAmount')}
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleBurn}
|
||||
disabled={isProcessing || !burnAmount}
|
||||
className="btn btn-secondary"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{isProcessing ? t('common.processing') : t('wusd.burn')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 转账功能 */}
|
||||
<div style={{ padding: '12px', background: '#fff', border: '1px solid #e0e0e0', borderRadius: '8px', marginBottom: '12px' }}>
|
||||
<h4 style={{ margin: '0 0 12px 0', fontSize: '14px', color: '#333', cursor: 'help' }} title="转账 Transfer - 将 WUSD 转移到其他钱包地址">{t('wusd.transfer')}</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 120px', gap: '12px', alignItems: 'end' }}>
|
||||
<div>
|
||||
<div className="form-group" style={{ marginBottom: '8px' }}>
|
||||
<label style={{ fontSize: '12px', color: '#666' }}>{t('wusd.toAddress')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={transferTo}
|
||||
onChange={(e) => setTransferTo(e.target.value)}
|
||||
placeholder="0x..."
|
||||
className="input"
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group" style={{ marginBottom: '0' }}>
|
||||
<label style={{ fontSize: '12px', color: '#666' }}>{t('wusd.transferAmount')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={transferAmount}
|
||||
onChange={(e) => setTransferAmount(e.target.value)}
|
||||
placeholder={t('wusd.enterAmount')}
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleTransfer}
|
||||
disabled={isProcessing || !transferTo || !transferAmount}
|
||||
className="btn btn-primary"
|
||||
style={{ height: '40px' }}
|
||||
>
|
||||
{isProcessing ? '...' : t('wusd.transfer')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 边界测试区域 */}
|
||||
<div style={{ marginTop: '12px', padding: '8px 12px', background: '#f5f5f5', borderRadius: '8px' }}>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
|
||||
onClick={() => setShowBoundaryTest(!showBoundaryTest)}
|
||||
>
|
||||
<h4 style={{ margin: 0, color: '#666', fontSize: '13px' }} title="边界测试 Boundary Tests - 测试 WUSD 合约的边界条件和错误处理">{t('test.boundaryTests')}</h4>
|
||||
<span style={{ color: '#999', fontSize: '16px' }}>{showBoundaryTest ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
|
||||
{showBoundaryTest && (
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<div className="test-hint">{t('test.boundaryHint')}</div>
|
||||
|
||||
<div className="test-grid">
|
||||
<div className="test-card">
|
||||
<div className="test-card-left">
|
||||
<span className="test-name">{t('test.mintZero')}</span>
|
||||
<span className="test-error">MaySucceed</span>
|
||||
<p className="test-desc">{t('test.mintZeroDesc')}</p>
|
||||
</div>
|
||||
<div className="test-card-right">
|
||||
<button onClick={() => runBoundaryTest('mint_zero')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="test-card">
|
||||
<div className="test-card-left">
|
||||
<span className="test-name">{t('test.burnExceed')}</span>
|
||||
<span className="test-error">InsufficientBalance</span>
|
||||
<p className="test-desc">{t('test.burnExceedDesc')}</p>
|
||||
</div>
|
||||
<div className="test-card-right">
|
||||
<button onClick={() => runBoundaryTest('burn_exceed')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="test-card">
|
||||
<div className="test-card-left">
|
||||
<span className="test-name">{t('test.mint10000')}</span>
|
||||
<span className="test-error">Mint</span>
|
||||
<p className="test-desc">{t('test.mint10000Desc')}</p>
|
||||
</div>
|
||||
<div className="test-card-right">
|
||||
<button onClick={() => runBoundaryTest('mint_10000')} disabled={isProcessing} className="btn btn-secondary btn-sm">{t('test.run')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 交易历史 */}
|
||||
<TransactionHistory transactions={transactions} onClear={clearHistory} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
import { createWeb3Modal } from '@web3modal/wagmi/react'
|
||||
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'
|
||||
import { arbitrumSepolia } from 'wagmi/chains'
|
||||
import { http, createConfig, createStorage } from 'wagmi'
|
||||
import { arbitrumSepolia, bscTestnet } from 'wagmi/chains'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { injected, coinbaseWallet } from 'wagmi/connectors'
|
||||
|
||||
export const queryClient = new QueryClient()
|
||||
|
||||
@@ -15,27 +16,44 @@ const metadata = {
|
||||
icons: ['https://avatars.githubusercontent.com/u/37784886']
|
||||
}
|
||||
|
||||
const chains = [arbitrumSepolia] as const
|
||||
// 检查是否应该跳过自动重连
|
||||
const shouldSkipReconnect = () => {
|
||||
if (typeof window === 'undefined') return false
|
||||
const skip = sessionStorage.getItem('skipReconnect') === 'true'
|
||||
if (skip) {
|
||||
sessionStorage.removeItem('skipReconnect')
|
||||
}
|
||||
return skip
|
||||
}
|
||||
|
||||
export const config = defaultWagmiConfig({
|
||||
chains,
|
||||
projectId,
|
||||
metadata,
|
||||
// 启用coinbase钱包连接器
|
||||
enableCoinbase: true,
|
||||
// 启用injected钱包(MetaMask, TokenPocket等浏览器扩展)
|
||||
enableInjected: true,
|
||||
// 禁用WalletConnect(因为CSP问题)
|
||||
enableWalletConnect: false,
|
||||
// 禁用Email和社交登录(需要iframe)
|
||||
auth: {
|
||||
email: false,
|
||||
socials: [],
|
||||
showWallets: true,
|
||||
walletFeatures: true,
|
||||
// 自定义 storage,在需要时禁用重连
|
||||
const customStorage = createStorage({
|
||||
storage: {
|
||||
getItem: (key: string) => {
|
||||
// 如果设置了跳过重连,返回 null 来阻止重连
|
||||
if (shouldSkipReconnect() && key === 'wagmi.recentConnectorId') {
|
||||
return null
|
||||
}
|
||||
return localStorage.getItem(key)
|
||||
},
|
||||
setItem: (key: string, value: string) => localStorage.setItem(key, value),
|
||||
removeItem: (key: string) => localStorage.removeItem(key),
|
||||
},
|
||||
})
|
||||
|
||||
export const config = createConfig({
|
||||
chains: [arbitrumSepolia, bscTestnet], // 支持多链
|
||||
connectors: [
|
||||
injected(),
|
||||
coinbaseWallet({ appName: metadata.name }),
|
||||
],
|
||||
transports: {
|
||||
[arbitrumSepolia.id]: http(), // ARB Sepolia (421614)
|
||||
[bscTestnet.id]: http(), // BNB Testnet (97)
|
||||
},
|
||||
storage: customStorage,
|
||||
})
|
||||
|
||||
createWeb3Modal({
|
||||
wagmiConfig: config,
|
||||
projectId,
|
||||
|
||||
@@ -16,13 +16,17 @@
|
||||
"contract": "Contract",
|
||||
"address": "Address",
|
||||
"network": "Arbitrum Sepolia",
|
||||
"connectFirst": "Please connect wallet first"
|
||||
"connectFirst": "Please connect wallet first",
|
||||
"set": "Set"
|
||||
},
|
||||
"nav": {
|
||||
"wusd": "WUSD",
|
||||
"usdc": "USDC",
|
||||
"vaultTrading": "Vault Trading",
|
||||
"factory": "Factory",
|
||||
"lpPool": "LP Pool"
|
||||
"lpPool": "LP Pool",
|
||||
"holders": "Holders",
|
||||
"lending": "Lending",
|
||||
"autoTest": "Auto Test"
|
||||
},
|
||||
"header": {
|
||||
"title": "YT Asset Test"
|
||||
@@ -30,8 +34,8 @@
|
||||
"footer": {
|
||||
"description": "YT Asset Contract Testing Interface"
|
||||
},
|
||||
"wusd": {
|
||||
"title": "WUSD Token",
|
||||
"usdc": {
|
||||
"title": "USDC Token",
|
||||
"mintAmount": "Mint Amount",
|
||||
"burnAmount": "Burn Amount",
|
||||
"enterAmount": "Enter amount",
|
||||
@@ -55,18 +59,18 @@
|
||||
"idleAssets": "Idle Assets",
|
||||
"totalSupply": "Total Supply",
|
||||
"hardCap": "Hard Cap",
|
||||
"wusdPrice": "WUSD Price",
|
||||
"usdcPrice": "USDC Price",
|
||||
"ytPrice": "YT Price",
|
||||
"yourWusdBalance": "Your WUSD Balance",
|
||||
"yourUsdcBalance": "Your USDC Balance",
|
||||
"yourYtBalance": "Your YT Balance",
|
||||
"buyYt": "Buy YT",
|
||||
"sellYt": "Sell YT",
|
||||
"wusdAmount": "WUSD Amount",
|
||||
"usdcAmount": "USDC Amount",
|
||||
"ytAmount": "YT Amount",
|
||||
"enterWusdAmount": "Enter WUSD amount",
|
||||
"enterUsdcAmount": "Enter USDC amount",
|
||||
"enterYtAmount": "Enter YT amount",
|
||||
"youWillReceive": "You will receive",
|
||||
"approveWusd": "Approve WUSD",
|
||||
"approveUsdc": "Approve USDC",
|
||||
"buy": "Buy YT",
|
||||
"sell": "Sell YT",
|
||||
"redeemStatus": "Redeem Status",
|
||||
@@ -96,12 +100,12 @@
|
||||
"needApprove": "Need Approve",
|
||||
"vaultAddress": "Vault Address",
|
||||
"factoryAddress": "Factory Address",
|
||||
"wusdContract": "WUSD Contract",
|
||||
"usdcContract": "USDC Contract",
|
||||
"pricePrecision": "Price Precision",
|
||||
"transferYt": "Transfer YT",
|
||||
"transfer": "Transfer",
|
||||
"invalidAddress": "Invalid address format",
|
||||
"queueWithdrawDesc": "Selling YT creates a withdrawal request and joins the queue. WUSD will be sent after admin processes the request.",
|
||||
"queueWithdrawDesc": "Selling YT creates a withdrawal request and joins the queue. USDC will be sent after admin processes the request.",
|
||||
"queueTotal": "Total Requests",
|
||||
"queuePending": "Pending",
|
||||
"queueProcessed": "Processed",
|
||||
@@ -114,7 +118,7 @@
|
||||
"processBatchWithdrawals": "Batch Process Withdrawals",
|
||||
"batchSize": "Batch Size",
|
||||
"processBatch": "Process Batch",
|
||||
"processBatchHint": "Process withdrawal requests from queue in order, sending WUSD to users"
|
||||
"processBatchHint": "Process withdrawal requests from queue in order, sending USDC to users"
|
||||
},
|
||||
"factory": {
|
||||
"title": "Factory Management",
|
||||
@@ -133,13 +137,13 @@
|
||||
"symbol": "Symbol",
|
||||
"managerAddress": "Manager Address",
|
||||
"redemptionTime": "Redemption Time",
|
||||
"initialWusdPrice": "Initial WUSD Price",
|
||||
"initialUsdcPrice": "Initial USDC Price",
|
||||
"initialYtPrice": "Initial YT Price",
|
||||
"create": "Create Vault",
|
||||
"updatePrices": "Update Vault Prices",
|
||||
"vaultAddress": "Vault Address",
|
||||
"selectVault": "Select Vault",
|
||||
"newWusdPrice": "New WUSD Price",
|
||||
"newUsdcPrice": "New USDC Price",
|
||||
"newYtPrice": "New YT Price",
|
||||
"update": "Update Prices",
|
||||
"ownerConfig": "Owner Config",
|
||||
@@ -216,16 +220,16 @@
|
||||
"quickActions": "Quick Actions",
|
||||
"run": "Run",
|
||||
"running": "Running...",
|
||||
"mint10000": "Mint 10000 WUSD",
|
||||
"mint10000Desc": "Quick mint 10000 test WUSD",
|
||||
"maxApprove": "Max Approve WUSD",
|
||||
"mint10000": "Mint 10000 USDC",
|
||||
"mint10000Desc": "Quick mint 10000 test USDC",
|
||||
"maxApprove": "Max Approve USDC",
|
||||
"maxApproveDesc": "Approve max uint256 amount",
|
||||
"buyZero": "Buy Amount 0",
|
||||
"buyZeroDesc": "Test depositYT(0)",
|
||||
"sellZero": "Sell Amount 0",
|
||||
"sellZeroDesc": "Test withdrawYT(0)",
|
||||
"buyExceedBalance": "Buy Exceed Balance",
|
||||
"buyExceedBalanceDesc": "Buy more than WUSD balance",
|
||||
"buyExceedBalanceDesc": "Buy more than USDC balance",
|
||||
"sellExceedBalance": "Sell Exceed Balance",
|
||||
"sellExceedBalanceDesc": "Sell more than YT balance",
|
||||
"buyExceedHardcap": "Buy Exceed Hardcap",
|
||||
@@ -252,11 +256,11 @@
|
||||
"cooldownRemaining": "Cooldown Remaining",
|
||||
"noCooldown": "No cooldown",
|
||||
"addLiquidity": "Add Liquidity",
|
||||
"addLiquidityDesc": "Deposit YT tokens or WUSD to receive ytLP tokens",
|
||||
"addLiquidityDesc": "Deposit YT tokens or USDC to receive ytLP tokens",
|
||||
"removeLiquidity": "Remove Liquidity",
|
||||
"removeLiquidityDesc": "Burn ytLP to get tokens back",
|
||||
"swapTokens": "Swap Tokens",
|
||||
"swapDesc": "Swap between YT tokens and WUSD in the pool",
|
||||
"swapDesc": "Swap between YT tokens and USDC in the pool",
|
||||
"selectToken": "Select Token",
|
||||
"amount": "Amount",
|
||||
"slippage": "Slippage Tolerance",
|
||||
@@ -302,6 +306,132 @@
|
||||
"usdyAmount": "USDY Amount",
|
||||
"isStableToken": "Stable Token",
|
||||
"setStableToken": "Set Stable Token",
|
||||
"stableTokenHint": "Mark token as stable, affects price calculation"
|
||||
"stableTokenHint": "Mark token as stable, affects price calculation",
|
||||
"coreConfig": "Core Config",
|
||||
"cooldownDuration": "Cooldown Duration",
|
||||
"cooldownSeconds": "seconds",
|
||||
"swapFees": "Swap Fees",
|
||||
"setSwapFees": "Set Fees",
|
||||
"swapFeeLabel": "Swap Fee",
|
||||
"stableSwapFeeLabel": "Stable Swap Fee",
|
||||
"taxBasisPointsLabel": "Tax Basis Points",
|
||||
"stableTaxBasisPointsLabel": "Stable Tax Basis Points",
|
||||
"withdrawToken": "Emergency Withdraw Token",
|
||||
"withdrawTokenLabel": "Emergency Withdraw Token",
|
||||
"withdraw": "Withdraw",
|
||||
"receiver": "Receiver",
|
||||
"tradingLimits": "Trading Limits",
|
||||
"dynamicFees": "Dynamic Fees",
|
||||
"dynamicFeesLabel": "Dynamic Fees",
|
||||
"poolStatus": "Pool Status",
|
||||
"poolStatusLabel": "Pool Status",
|
||||
"running": "Running",
|
||||
"pausedStatus": "Paused",
|
||||
"pausePool": "Pause Pool",
|
||||
"unpausePool": "Unpause Pool",
|
||||
"maxSwapAmount": "Max Swap Amount",
|
||||
"maxSwapAmountLabel": "Max Swap Amount",
|
||||
"aumAdjustment": "AUM Adjustment",
|
||||
"addition": "Addition",
|
||||
"deduction": "Deduction",
|
||||
"deductionLabel": "Deduction",
|
||||
"maxSlippage": "Max Slippage",
|
||||
"maxSlippageLabel": "Max Slippage",
|
||||
"maxPriceChange": "Max Price Change",
|
||||
"maxPriceChangeLabel": "Max Price Change",
|
||||
"permissionManagement": "Permission Management",
|
||||
"setGov": "Set Gov",
|
||||
"setGovLabel": "Set Gov",
|
||||
"setHandler": "Set Handler",
|
||||
"setHandlerLabel": "Set Handler",
|
||||
"setKeeper": "Set Keeper",
|
||||
"setKeeperLabel": "Set Keeper",
|
||||
"setSwapper": "Set Swapper",
|
||||
"setSwapperLabel": "Set Swapper",
|
||||
"setPoolManager": "Set Pool Manager",
|
||||
"setPoolManagerLabel": "Set Pool Manager",
|
||||
"setMinterLabel": "Set Minter",
|
||||
"isActive": "Active",
|
||||
"adminConfig": "Admin Config",
|
||||
"debugInfo": "Debug Info",
|
||||
"sameTokenWarning": "Input and output tokens are the same",
|
||||
"withdrawWarning": "Warning: This will withdraw tokens from the pool",
|
||||
"permissionWarning": "Warning: Permission changes are high-risk operations"
|
||||
},
|
||||
"holders": {
|
||||
"title": "Holder Tracking",
|
||||
"updateNow": "Update Now",
|
||||
"lastUpdate": "Last Update",
|
||||
"holders": "Holders",
|
||||
"holdersList": "Holders List",
|
||||
"total": "Total",
|
||||
"noHolders": "No holder data available",
|
||||
"rank": "Rank",
|
||||
"address": "Address",
|
||||
"balance": "Balance",
|
||||
"holdingTime": "Holding Time",
|
||||
"lastUpdated": "Last Updated",
|
||||
"days": "d",
|
||||
"hours": "h",
|
||||
"minutes": "m"
|
||||
},
|
||||
"lending": {
|
||||
"title": "USDC Lending System",
|
||||
"contract": "Lending Contract",
|
||||
"yourAccount": "Your Account",
|
||||
"totalCollateral": "Total Collateral Value",
|
||||
"totalBorrow": "Total Borrowed",
|
||||
"availableToBorrow": "Available to Borrow",
|
||||
"healthFactor": "Health Factor",
|
||||
"systemInfo": "System Information",
|
||||
"totalLiquidity": "Total Liquidity",
|
||||
"totalBorrows": "Total Borrows",
|
||||
"utilizationRate": "Utilization Rate",
|
||||
"borrowAPY": "Borrow APY",
|
||||
"depositCollateral": "Deposit Collateral",
|
||||
"borrow": "Borrow",
|
||||
"repay": "Repay",
|
||||
"withdrawCollateral": "Withdraw Collateral",
|
||||
"selectCollateral": "Select Collateral",
|
||||
"borrowAmount": "Borrow Amount",
|
||||
"repayAmount": "Repay Amount",
|
||||
"yourBorrow": "Your Borrow",
|
||||
"usdcBalance": "USDC Balance",
|
||||
"approve": "Approve Collateral",
|
||||
"approveUSDC": "Approve USDC",
|
||||
"deposit": "Deposit",
|
||||
"withdraw": "Withdraw",
|
||||
"collateralDetails": "Collateral Details",
|
||||
"asset": "Asset",
|
||||
"walletBalance": "Wallet Balance",
|
||||
"depositedAmount": "Deposited Amount",
|
||||
"admin": {
|
||||
"show": "Show Admin Config",
|
||||
"hide": "Hide Admin Config",
|
||||
"title": "Lending System Admin",
|
||||
"notOwner": "Admin permission required",
|
||||
"noPermission": "You are not an admin, view only",
|
||||
"systemStatus": "System Status",
|
||||
"systemPaused": "System Paused",
|
||||
"pause": "Pause System",
|
||||
"unpause": "Unpause System",
|
||||
"pauseSent": "Pause transaction submitted",
|
||||
"unpauseSent": "Unpause transaction submitted",
|
||||
"collateralConfig": "Collateral Configuration",
|
||||
"selectAsset": "Select Asset",
|
||||
"currentConfig": "Current Configuration",
|
||||
"isActive": "Is Active",
|
||||
"collateralFactor": "Collateral Factor",
|
||||
"liquidationThreshold": "Liquidation Threshold",
|
||||
"liquidationBonus": "Liquidation Bonus",
|
||||
"loadToForm": "Load to Form",
|
||||
"setConfig": "Set Configuration",
|
||||
"activate": "Activate Collateral",
|
||||
"deactivate": "Deactivate Collateral",
|
||||
"configSent": "Config transaction submitted",
|
||||
"activateSent": "Activate transaction submitted",
|
||||
"deactivateSent": "Deactivate transaction submitted",
|
||||
"contracts": "Contract Addresses"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,17 @@
|
||||
"contract": "合约",
|
||||
"address": "地址",
|
||||
"network": "Arbitrum Sepolia",
|
||||
"connectFirst": "请先连接钱包"
|
||||
"connectFirst": "请先连接钱包",
|
||||
"set": "设置"
|
||||
},
|
||||
"nav": {
|
||||
"wusd": "WUSD",
|
||||
"usdc": "USDC",
|
||||
"vaultTrading": "金库交易",
|
||||
"factory": "工厂管理",
|
||||
"lpPool": "LP 流动池"
|
||||
"lpPool": "LP 流动池",
|
||||
"holders": "持有者",
|
||||
"lending": "借贷",
|
||||
"autoTest": "自动测试"
|
||||
},
|
||||
"header": {
|
||||
"title": "YT 资产测试"
|
||||
@@ -30,8 +34,8 @@
|
||||
"footer": {
|
||||
"description": "YT 资产合约测试界面"
|
||||
},
|
||||
"wusd": {
|
||||
"title": "WUSD 代币",
|
||||
"usdc": {
|
||||
"title": "USDC 代币",
|
||||
"mintAmount": "铸造数量",
|
||||
"burnAmount": "销毁数量",
|
||||
"enterAmount": "输入数量",
|
||||
@@ -55,18 +59,18 @@
|
||||
"idleAssets": "闲置资产",
|
||||
"totalSupply": "总供应量",
|
||||
"hardCap": "硬顶",
|
||||
"wusdPrice": "WUSD 价格",
|
||||
"usdcPrice": "USDC 价格",
|
||||
"ytPrice": "YT 价格",
|
||||
"yourWusdBalance": "你的 WUSD 余额",
|
||||
"yourUsdcBalance": "你的 USDC 余额",
|
||||
"yourYtBalance": "你的 YT 余额",
|
||||
"buyYt": "买入 YT",
|
||||
"sellYt": "卖出 YT",
|
||||
"wusdAmount": "WUSD 数量",
|
||||
"usdcAmount": "USDC 数量",
|
||||
"ytAmount": "YT 数量",
|
||||
"enterWusdAmount": "输入 WUSD 数量",
|
||||
"enterUsdcAmount": "输入 USDC 数量",
|
||||
"enterYtAmount": "输入 YT 数量",
|
||||
"youWillReceive": "你将收到",
|
||||
"approveWusd": "授权 WUSD",
|
||||
"approveUsdc": "授权 USDC",
|
||||
"buy": "买入 YT",
|
||||
"sell": "卖出 YT",
|
||||
"redeemStatus": "赎回状态",
|
||||
@@ -96,12 +100,12 @@
|
||||
"needApprove": "需要授权",
|
||||
"vaultAddress": "金库地址",
|
||||
"factoryAddress": "工厂地址",
|
||||
"wusdContract": "WUSD 合约",
|
||||
"usdcContract": "USDC 合约",
|
||||
"pricePrecision": "价格精度",
|
||||
"transferYt": "转账 YT",
|
||||
"transfer": "转账",
|
||||
"invalidAddress": "无效的地址格式",
|
||||
"queueWithdrawDesc": "卖出 YT 将创建退出请求并加入队列,等待管理员处理后发放 WUSD。",
|
||||
"queueWithdrawDesc": "卖出 YT 将创建退出请求并加入队列,等待管理员处理后发放 USDC。",
|
||||
"queueTotal": "总请求数",
|
||||
"queuePending": "待处理",
|
||||
"queueProcessed": "已处理",
|
||||
@@ -114,7 +118,7 @@
|
||||
"processBatchWithdrawals": "批量处理退出",
|
||||
"batchSize": "批量大小",
|
||||
"processBatch": "处理退出",
|
||||
"processBatchHint": "从队列中按顺序处理指定数量的退出请求,向用户发送 WUSD"
|
||||
"processBatchHint": "从队列中按顺序处理指定数量的退出请求,向用户发送 USDC"
|
||||
},
|
||||
"factory": {
|
||||
"title": "工厂管理",
|
||||
@@ -133,13 +137,13 @@
|
||||
"symbol": "符号",
|
||||
"managerAddress": "管理员地址",
|
||||
"redemptionTime": "赎回时间",
|
||||
"initialWusdPrice": "初始 WUSD 价格",
|
||||
"initialUsdcPrice": "初始 USDC 价格",
|
||||
"initialYtPrice": "初始 YT 价格",
|
||||
"create": "创建金库",
|
||||
"updatePrices": "更新金库价格",
|
||||
"vaultAddress": "金库地址",
|
||||
"selectVault": "选择金库",
|
||||
"newWusdPrice": "新 WUSD 价格",
|
||||
"newUsdcPrice": "新 USDC 价格",
|
||||
"newYtPrice": "新 YT 价格",
|
||||
"update": "更新价格",
|
||||
"ownerConfig": "Owner 配置",
|
||||
@@ -216,16 +220,16 @@
|
||||
"quickActions": "快速操作",
|
||||
"run": "执行",
|
||||
"running": "执行中...",
|
||||
"mint10000": "铸造 10000 WUSD",
|
||||
"mint10000Desc": "快速铸造10000个测试WUSD",
|
||||
"maxApprove": "最大授权 WUSD",
|
||||
"mint10000": "铸造 10000 USDC",
|
||||
"mint10000Desc": "快速铸造10000个测试USDC",
|
||||
"maxApprove": "最大授权 USDC",
|
||||
"maxApproveDesc": "授权最大uint256额度",
|
||||
"buyZero": "买入金额为0",
|
||||
"buyZeroDesc": "测试 depositYT(0)",
|
||||
"sellZero": "卖出金额为0",
|
||||
"sellZeroDesc": "测试 withdrawYT(0)",
|
||||
"buyExceedBalance": "买入超过余额",
|
||||
"buyExceedBalanceDesc": "买入金额超过 WUSD 余额",
|
||||
"buyExceedBalanceDesc": "买入金额超过 USDC 余额",
|
||||
"sellExceedBalance": "卖出超过余额",
|
||||
"sellExceedBalanceDesc": "卖出金额超过 YT 余额",
|
||||
"buyExceedHardcap": "买入超过硬顶",
|
||||
@@ -252,11 +256,11 @@
|
||||
"cooldownRemaining": "冷却时间剩余",
|
||||
"noCooldown": "无冷却",
|
||||
"addLiquidity": "添加流动性",
|
||||
"addLiquidityDesc": "存入 YT 代币或 WUSD 获得 ytLP 凭证",
|
||||
"addLiquidityDesc": "存入 YT 代币或 USDC 获得 ytLP 凭证",
|
||||
"removeLiquidity": "移除流动性",
|
||||
"removeLiquidityDesc": "销毁 ytLP 获取代币",
|
||||
"swapTokens": "代币互换",
|
||||
"swapDesc": "在池内交换 YT 代币和 WUSD",
|
||||
"swapDesc": "在池内交换 YT 代币和 USDC",
|
||||
"selectToken": "选择代币",
|
||||
"amount": "数量",
|
||||
"slippage": "滑点容忍度",
|
||||
@@ -302,6 +306,132 @@
|
||||
"usdyAmount": "USDY 数量",
|
||||
"isStableToken": "稳定币",
|
||||
"setStableToken": "设置稳定币",
|
||||
"stableTokenHint": "将代币标记为稳定币,影响价格计算方式"
|
||||
"stableTokenHint": "将代币标记为稳定币,影响价格计算方式",
|
||||
"coreConfig": "核心配置",
|
||||
"cooldownDuration": "冷却时间",
|
||||
"cooldownSeconds": "秒",
|
||||
"swapFees": "交换手续费",
|
||||
"setSwapFees": "设置手续费",
|
||||
"swapFeeLabel": "Swap手续费",
|
||||
"stableSwapFeeLabel": "稳定币Swap手续费",
|
||||
"taxBasisPointsLabel": "税基点",
|
||||
"stableTaxBasisPointsLabel": "稳定币税基点",
|
||||
"withdrawToken": "紧急提取代币",
|
||||
"withdrawTokenLabel": "紧急提取代币",
|
||||
"withdraw": "提取",
|
||||
"receiver": "接收者",
|
||||
"tradingLimits": "交易限制",
|
||||
"dynamicFees": "动态手续费",
|
||||
"dynamicFeesLabel": "动态手续费",
|
||||
"poolStatus": "池子状态",
|
||||
"poolStatusLabel": "池子状态",
|
||||
"running": "运行中",
|
||||
"pausedStatus": "已暂停",
|
||||
"pausePool": "暂停池子",
|
||||
"unpausePool": "恢复池子",
|
||||
"maxSwapAmount": "最大交换金额",
|
||||
"maxSwapAmountLabel": "最大交换金额",
|
||||
"aumAdjustment": "AUM 调整",
|
||||
"addition": "增加",
|
||||
"deduction": "减少",
|
||||
"deductionLabel": "减少",
|
||||
"maxSlippage": "最大滑点",
|
||||
"maxSlippageLabel": "最大滑点",
|
||||
"maxPriceChange": "最大价格变化",
|
||||
"maxPriceChangeLabel": "最大价格变化",
|
||||
"permissionManagement": "权限管理",
|
||||
"setGov": "设置 Gov",
|
||||
"setGovLabel": "设置 Gov",
|
||||
"setHandler": "设置 Handler",
|
||||
"setHandlerLabel": "设置 Handler",
|
||||
"setKeeper": "设置 Keeper",
|
||||
"setKeeperLabel": "设置 Keeper",
|
||||
"setSwapper": "设置 Swapper",
|
||||
"setSwapperLabel": "设置 Swapper",
|
||||
"setPoolManager": "设置 PoolManager",
|
||||
"setPoolManagerLabel": "设置 PoolManager",
|
||||
"setMinterLabel": "设置 Minter",
|
||||
"isActive": "激活",
|
||||
"adminConfig": "管理员配置",
|
||||
"debugInfo": "调试信息",
|
||||
"sameTokenWarning": "输入和输出代币相同",
|
||||
"withdrawWarning": "警告: 此操作将从池子中提取代币",
|
||||
"permissionWarning": "警告: 权限变更是高风险操作,请确保了解影响"
|
||||
},
|
||||
"holders": {
|
||||
"title": "持有者追踪",
|
||||
"updateNow": "立即更新",
|
||||
"lastUpdate": "最后更新",
|
||||
"holders": "持有者",
|
||||
"holdersList": "持有者列表",
|
||||
"total": "总计",
|
||||
"noHolders": "暂无持有者数据",
|
||||
"rank": "排名",
|
||||
"address": "地址",
|
||||
"balance": "余额",
|
||||
"holdingTime": "持有时长",
|
||||
"lastUpdated": "最后更新",
|
||||
"days": "天",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟"
|
||||
},
|
||||
"lending": {
|
||||
"title": "USDC 借贷系统",
|
||||
"contract": "借贷合约",
|
||||
"yourAccount": "你的账户",
|
||||
"totalCollateral": "总抵押价值",
|
||||
"totalBorrow": "总借款",
|
||||
"availableToBorrow": "可借额度",
|
||||
"healthFactor": "健康因子",
|
||||
"systemInfo": "系统信息",
|
||||
"totalLiquidity": "总流动性",
|
||||
"totalBorrows": "总借款量",
|
||||
"utilizationRate": "资金利用率",
|
||||
"borrowAPY": "借款年化",
|
||||
"depositCollateral": "存入抵押品",
|
||||
"borrow": "借出",
|
||||
"repay": "归还",
|
||||
"withdrawCollateral": "提取抵押品",
|
||||
"selectCollateral": "选择抵押品",
|
||||
"borrowAmount": "借款金额",
|
||||
"repayAmount": "归还金额",
|
||||
"yourBorrow": "你的借款",
|
||||
"usdcBalance": "USDC 余额",
|
||||
"approve": "授权抵押品",
|
||||
"approveUSDC": "授权 USDC",
|
||||
"deposit": "存入",
|
||||
"withdraw": "提取",
|
||||
"collateralDetails": "抵押品明细",
|
||||
"asset": "资产",
|
||||
"walletBalance": "钱包余额",
|
||||
"depositedAmount": "已存入数量",
|
||||
"admin": {
|
||||
"show": "显示管理员配置",
|
||||
"hide": "隐藏管理员配置",
|
||||
"title": "借贷系统管理",
|
||||
"notOwner": "需要管理员权限",
|
||||
"noPermission": "你不是管理员,只能查看配置",
|
||||
"systemStatus": "系统状态",
|
||||
"systemPaused": "系统暂停",
|
||||
"pause": "暂停系统",
|
||||
"unpause": "恢复系统",
|
||||
"pauseSent": "暂停交易已提交",
|
||||
"unpauseSent": "恢复交易已提交",
|
||||
"collateralConfig": "抵押品配置",
|
||||
"selectAsset": "选择抵押品",
|
||||
"currentConfig": "当前配置",
|
||||
"isActive": "是否激活",
|
||||
"collateralFactor": "抵押率",
|
||||
"liquidationThreshold": "清算阈值",
|
||||
"liquidationBonus": "清算奖励",
|
||||
"loadToForm": "加载到表单",
|
||||
"setConfig": "设置配置",
|
||||
"activate": "激活抵押品",
|
||||
"deactivate": "停用抵押品",
|
||||
"configSent": "配置交易已提交",
|
||||
"activateSent": "激活交易已提交",
|
||||
"deactivateSent": "停用交易已提交",
|
||||
"contracts": "合约地址"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,13 @@ export default defineConfig({
|
||||
host: '0.0.0.0',
|
||||
strictPort: true,
|
||||
allowedHosts: ['maxfight.vip', 'localhost', '127.0.0.1'],
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3003',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api')
|
||||
}
|
||||
}
|
||||
},
|
||||
// 优化依赖预构建
|
||||
optimizeDeps: {
|
||||
|
||||
Reference in New Issue
Block a user