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:
2026-01-26 23:32:29 +08:00
parent c8427caa01
commit 5cb64f881e
80 changed files with 18342 additions and 1117 deletions

9
.gitignore vendored
View File

@@ -1,6 +1,15 @@
# Markdown files
*.md
# Documentation directories
docs/
document/
# Temp files
*.txt
*.jpg
*.backup*
# Node modules
node_modules/

View File

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

View File

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

View 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)

View 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)

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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();

View 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')

View 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()

View 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()

View 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();

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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 的 USDCsupply 操作)
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 转给用户的 USDCwithdraw 操作)
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()

View 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. 价格预言机返回 0YT-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()

View 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)

View 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)
})

View 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()

View 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)

View 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()

View 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);
}

View 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();

View 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()

View 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();

View 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 ✅
`)

View 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)

View 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()

View 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)

View 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()

View 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()

View 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()

View 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()

View 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()

View 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);

View 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);

View 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()

View 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()

View File

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

View 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>
)
}

View File

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

View File

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

View 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;
}
}

View 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>
)
}

File diff suppressed because it is too large Load Diff

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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'

View 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

View 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,
}
}

View 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,
}
}

View 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

File diff suppressed because it is too large Load Diff

View 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;
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

@@ -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": "合约地址"
}
}
}

View File

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