diff --git a/.gitignore b/.gitignore index c6c2cbf..8548cee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,15 @@ # Markdown files *.md +# Documentation directories +docs/ +document/ + +# Temp files +*.txt +*.jpg +*.backup* + # Node modules node_modules/ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4d1be0f..014908e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index a9ab51a..d15ad08 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" diff --git a/frontend/scripts/batch-buy-simulation.ts b/frontend/scripts/batch-buy-simulation.ts new file mode 100644 index 0000000..d645070 --- /dev/null +++ b/frontend/scripts/batch-buy-simulation.ts @@ -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 { + 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) diff --git a/frontend/scripts/batch-sell-simulation.ts b/frontend/scripts/batch-sell-simulation.ts new file mode 100644 index 0000000..735bc9d --- /dev/null +++ b/frontend/scripts/batch-sell-simulation.ts @@ -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 { + 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) diff --git a/frontend/scripts/check-account-status.js b/frontend/scripts/check-account-status.js new file mode 100644 index 0000000..7937488 --- /dev/null +++ b/frontend/scripts/check-account-status.js @@ -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() diff --git a/frontend/scripts/check-all-balances.ts b/frontend/scripts/check-all-balances.ts new file mode 100644 index 0000000..627d3fb --- /dev/null +++ b/frontend/scripts/check-all-balances.ts @@ -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() diff --git a/frontend/scripts/check-balance.ts b/frontend/scripts/check-balance.ts new file mode 100644 index 0000000..2ad22be --- /dev/null +++ b/frontend/scripts/check-balance.ts @@ -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() diff --git a/frontend/scripts/check-borrow-history.js b/frontend/scripts/check-borrow-history.js new file mode 100644 index 0000000..cb43ce6 --- /dev/null +++ b/frontend/scripts/check-borrow-history.js @@ -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() diff --git a/frontend/scripts/check-collateral-balance.js b/frontend/scripts/check-collateral-balance.js new file mode 100644 index 0000000..a904b4a --- /dev/null +++ b/frontend/scripts/check-collateral-balance.js @@ -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() diff --git a/frontend/scripts/check-compound-v3-storage.js b/frontend/scripts/check-compound-v3-storage.js new file mode 100644 index 0000000..0b45efd --- /dev/null +++ b/frontend/scripts/check-compound-v3-storage.js @@ -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() diff --git a/frontend/scripts/check-configurator-setup.js b/frontend/scripts/check-configurator-setup.js new file mode 100644 index 0000000..adc43bd --- /dev/null +++ b/frontend/scripts/check-configurator-setup.js @@ -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(); diff --git a/frontend/scripts/check-function-selector.js b/frontend/scripts/check-function-selector.js new file mode 100644 index 0000000..2c96f2c --- /dev/null +++ b/frontend/scripts/check-function-selector.js @@ -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') diff --git a/frontend/scripts/check-interest-rates.js b/frontend/scripts/check-interest-rates.js new file mode 100644 index 0000000..4359b2c --- /dev/null +++ b/frontend/scripts/check-interest-rates.js @@ -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() diff --git a/frontend/scripts/check-lending-config.js b/frontend/scripts/check-lending-config.js new file mode 100644 index 0000000..3e12afd --- /dev/null +++ b/frontend/scripts/check-lending-config.js @@ -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() diff --git a/frontend/scripts/check-lending-setup.js b/frontend/scripts/check-lending-setup.js new file mode 100644 index 0000000..a0b4737 --- /dev/null +++ b/frontend/scripts/check-lending-setup.js @@ -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(); diff --git a/frontend/scripts/check-owner.js b/frontend/scripts/check-owner.js new file mode 100644 index 0000000..dda74b8 --- /dev/null +++ b/frontend/scripts/check-owner.js @@ -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() diff --git a/frontend/scripts/check-price-decimals.js b/frontend/scripts/check-price-decimals.js new file mode 100644 index 0000000..a269eaa --- /dev/null +++ b/frontend/scripts/check-price-decimals.js @@ -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() diff --git a/frontend/scripts/check-price-oracle.js b/frontend/scripts/check-price-oracle.js new file mode 100644 index 0000000..97d8100 --- /dev/null +++ b/frontend/scripts/check-price-oracle.js @@ -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() diff --git a/frontend/scripts/check-price.js b/frontend/scripts/check-price.js new file mode 100644 index 0000000..dea7557 --- /dev/null +++ b/frontend/scripts/check-price.js @@ -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() diff --git a/frontend/scripts/check-recent-events.js b/frontend/scripts/check-recent-events.js new file mode 100644 index 0000000..1b4f374 --- /dev/null +++ b/frontend/scripts/check-recent-events.js @@ -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() diff --git a/frontend/scripts/check-token-transfers.js b/frontend/scripts/check-token-transfers.js new file mode 100644 index 0000000..d8537d8 --- /dev/null +++ b/frontend/scripts/check-token-transfers.js @@ -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() diff --git a/frontend/scripts/check-transaction-details.js b/frontend/scripts/check-transaction-details.js new file mode 100644 index 0000000..58ebeea --- /dev/null +++ b/frontend/scripts/check-transaction-details.js @@ -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() diff --git a/frontend/scripts/check-usdc-supply-history.js b/frontend/scripts/check-usdc-supply-history.js new file mode 100644 index 0000000..450ed0d --- /dev/null +++ b/frontend/scripts/check-usdc-supply-history.js @@ -0,0 +1,158 @@ +import { createPublicClient, http, getAddress } from 'viem' +import { arbitrumSepolia } from 'viem/chains' + +const client = createPublicClient({ + chain: arbitrumSepolia, + transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07') +}) + +const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D') +const USDC = getAddress('0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d') +const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d') + +const TRANSFER_EVENT = { + type: 'event', + name: 'Transfer', + inputs: [ + { indexed: true, name: 'from', type: 'address' }, + { indexed: true, name: 'to', type: 'address' }, + { indexed: false, name: 'value', type: 'uint256' } + ] +} + +async function main() { + console.log('\n检查 USDC 供应/提取历史...\n') + console.log('用户:', USER) + console.log('Lending 合约:', LENDING_PROXY) + console.log() + + const latestBlock = await client.getBlockNumber() + console.log('最新区块:', latestBlock) + console.log('查询范围: 最近 10000 个区块\n') + + try { + // 1. 检查用户转入 Lending 的 USDC(supply 操作) + console.log('=== 1. USDC 转入 Lending 合约(Supply)===') + const logsToLending = await client.getLogs({ + address: USDC, + event: TRANSFER_EVENT, + args: { + from: USER, + to: LENDING_PROXY + }, + fromBlock: latestBlock - 10000n, + toBlock: latestBlock + }) + + if (logsToLending.length > 0) { + console.log(`找到 ${logsToLending.length} 笔转入记录:\n`) + let totalSupplied = 0n + logsToLending.forEach((log, i) => { + const { value } = log.args + totalSupplied += value + console.log(`${i + 1}. 区块 ${log.blockNumber}`) + console.log(` 交易: ${log.transactionHash}`) + console.log(` 数量: ${Number(value) / 1e6} USDC`) + console.log() + }) + console.log(`总存入: ${Number(totalSupplied) / 1e6} USDC\n`) + } else { + console.log('未找到转入记录\n') + } + + // 2. 检查 Lending 转给用户的 USDC(withdraw 操作) + console.log('=== 2. USDC 从 Lending 转出(Withdraw)===') + const logsFromLending = await client.getLogs({ + address: USDC, + event: TRANSFER_EVENT, + args: { + from: LENDING_PROXY, + to: USER + }, + fromBlock: latestBlock - 10000n, + toBlock: latestBlock + }) + + if (logsFromLending.length > 0) { + console.log(`找到 ${logsFromLending.length} 笔转出记录:\n`) + let totalWithdrawn = 0n + logsFromLending.forEach((log, i) => { + const { value } = log.args + totalWithdrawn += value + console.log(`${i + 1}. 区块 ${log.blockNumber}`) + console.log(` 交易: ${log.transactionHash}`) + console.log(` 数量: ${Number(value) / 1e6} USDC`) + console.log() + }) + console.log(`总提取: ${Number(totalWithdrawn) / 1e6} USDC\n`) + } else { + console.log('未找到转出记录\n') + } + + // 3. 获取当前余额 + console.log('=== 3. 当前状态 ===') + const LENDING_ABI = [ + { + inputs: [{ name: 'account', type: 'address' }], + name: 'getBalance', + outputs: [{ name: '', type: 'int256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ name: 'account', type: 'address' }], + name: 'borrowBalanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } + ] + + const balance = await client.readContract({ + address: LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getBalance', + args: [USER] + }) + + const borrowBalance = await client.readContract({ + address: LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'borrowBalanceOf', + args: [USER] + }) + + console.log('USDC 余额(在 Lending 中):', Number(balance) / 1e6, 'USDC') + console.log(' 原始值:', balance.toString()) + console.log(' 正数 = 存款,负数 = 借款') + console.log() + console.log('借款余额:', Number(borrowBalance) / 1e6, 'USDC') + console.log() + + // 4. 分析 + console.log('=== 分析 ===') + if (balance > 0) { + console.log('⚠️ 你当前有存款!') + console.log(` 存款金额: ${Number(balance) / 1e6} USDC`) + console.log() + console.log('这意味着:') + console.log(` - 如果你借款 ≤ ${Number(balance) / 1e6} USDC,只是提取存款`) + console.log(` - 只有借款 > ${Number(balance) / 1e6} USDC,才会产生债务`) + console.log() + console.log('建议:') + console.log(' 1. 如果要真正借款,请借款金额大于当前存款') + console.log(` 例如:借款 ${Number(balance) / 1e6 + 100} USDC`) + console.log(' 2. 或者先提取所有存款,再借款') + } else if (balance < 0) { + console.log('✓ 你有借款!') + console.log(` 借款金额: ${-Number(balance) / 1e6} USDC`) + } else { + console.log('✓ 你没有存款也没有借款') + } + + } catch (error) { + console.error('查询失败:', error.message) + } +} + +main() diff --git a/frontend/scripts/check-user-account-data.js b/frontend/scripts/check-user-account-data.js new file mode 100644 index 0000000..7dd9451 --- /dev/null +++ b/frontend/scripts/check-user-account-data.js @@ -0,0 +1,100 @@ +import { createPublicClient, http, getAddress } from 'viem' +import { arbitrumSepolia } from 'viem/chains' + +const client = createPublicClient({ + chain: arbitrumSepolia, + transport: http('https://api.zan.top/node/v1/arb/sepolia/baf84c429d284bb5b676cb8c9ca21c07') +}) + +const LENDING_PROXY = getAddress('0xCb4E7B1069F6C26A1c27523ce4c8dfD884552d1D') +const USER = getAddress('0xa013422A5918CD099C63c8CC35283EACa99a705d') +const YT_A = getAddress('0x97204190B35D9895a7a47aa7BaC61ac08De3cF05') + +const LENDING_ABI = [ + { + inputs: [{ internalType: 'address', name: '_user', type: 'address' }], + name: 'getUserAccountData', + outputs: [ + { internalType: 'uint256', name: 'totalCollateralValue', type: 'uint256' }, + { internalType: 'uint256', name: 'totalBorrowValue', type: 'uint256' }, + { internalType: 'uint256', name: 'availableToBorrow', type: 'uint256' }, + { internalType: 'uint256', name: 'healthFactor', type: 'uint256' } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { internalType: 'address', name: '_user', type: 'address' }, + { internalType: 'address', name: '_collateralAsset', type: 'address' } + ], + name: 'getUserCollateral', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +] + +async function main() { + console.log('\n检查用户账户数据...\n') + console.log('用户地址:', USER) + console.log('Lending 合约:', LENDING_PROXY) + + try { + // 1. 检查抵押品余额 + console.log('\n--- 1. 检查 getUserCollateral (YT-A) ---') + const collateral = await client.readContract({ + address: LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getUserCollateral', + args: [USER, YT_A] + }) + console.log('✓ getUserCollateral:', Number(collateral) / 1e18, 'YT-A') + console.log(' 原始值:', collateral.toString()) + + // 2. 检查账户数据 + console.log('\n--- 2. 检查 getUserAccountData ---') + const accountData = await client.readContract({ + address: LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getUserAccountData', + args: [USER] + }) + + console.log('✓ getUserAccountData 返回:') + console.log(' totalCollateralValue:', Number(accountData[0]) / 1e6, 'USD') + console.log(' totalBorrowValue:', Number(accountData[1]) / 1e6, 'USD') + console.log(' availableToBorrow:', Number(accountData[2]) / 1e6, 'USD') + console.log(' healthFactor:', Number(accountData[3]) / 1e4, '%') + + console.log('\n 原始值:') + console.log(' [0]:', accountData[0].toString()) + console.log(' [1]:', accountData[1].toString()) + console.log(' [2]:', accountData[2].toString()) + console.log(' [3]:', accountData[3].toString()) + + // 3. 诊断 + console.log('\n--- 诊断 ---') + if (collateral > 0n && accountData[0] === 0n) { + console.log('⚠️ 问题发现:') + console.log(' - getUserCollateral 返回有值(', Number(collateral) / 1e18, 'YT-A)') + console.log(' - 但 getUserAccountData 返回总抵押价值为 0') + console.log(' 可能原因:') + console.log(' 1. 价格预言机返回 0(YT-A 价格未设置)') + console.log(' 2. getUserAccountData 函数逻辑错误') + console.log(' 3. 抵押品资产未在配置中激活') + } else if (collateral === 0n) { + console.log('✓ 抵押品余额为 0,这是正常的(如果之前交易失败)') + } else if (accountData[0] > 0n) { + console.log('✓ 一切正常,抵押品价值已正确计算') + } + + } catch (error) { + console.error('\n✗ 调用失败:', error.message) + if (error.message.includes('Contract function')) { + console.log('\n可能原因:getUserAccountData 函数不存在或签名不匹配') + } + } +} + +main() diff --git a/frontend/scripts/check-withdraw-transactions.js b/frontend/scripts/check-withdraw-transactions.js new file mode 100644 index 0000000..e5ca393 --- /dev/null +++ b/frontend/scripts/check-withdraw-transactions.js @@ -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) diff --git a/frontend/scripts/configure-collateral.js b/frontend/scripts/configure-collateral.js new file mode 100644 index 0000000..66491ec --- /dev/null +++ b/frontend/scripts/configure-collateral.js @@ -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) +}) diff --git a/frontend/scripts/debug-contract-view-functions.js b/frontend/scripts/debug-contract-view-functions.js new file mode 100644 index 0000000..6aba395 --- /dev/null +++ b/frontend/scripts/debug-contract-view-functions.js @@ -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() diff --git a/frontend/scripts/debug-events.js b/frontend/scripts/debug-events.js new file mode 100644 index 0000000..4e7ab5a --- /dev/null +++ b/frontend/scripts/debug-events.js @@ -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) diff --git a/frontend/scripts/debug-lending-functions.js b/frontend/scripts/debug-lending-functions.js new file mode 100644 index 0000000..c3c03fb --- /dev/null +++ b/frontend/scripts/debug-lending-functions.js @@ -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() diff --git a/frontend/scripts/decode-tx-data.js b/frontend/scripts/decode-tx-data.js new file mode 100644 index 0000000..bed24c6 --- /dev/null +++ b/frontend/scripts/decode-tx-data.js @@ -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); +} diff --git a/frontend/scripts/diagnose-configurator.js b/frontend/scripts/diagnose-configurator.js new file mode 100644 index 0000000..fd1b74d --- /dev/null +++ b/frontend/scripts/diagnose-configurator.js @@ -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(); diff --git a/frontend/scripts/find-missing-tokens.js b/frontend/scripts/find-missing-tokens.js new file mode 100644 index 0000000..1480a53 --- /dev/null +++ b/frontend/scripts/find-missing-tokens.js @@ -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() diff --git a/frontend/scripts/get-proxy-implementation.js b/frontend/scripts/get-proxy-implementation.js new file mode 100644 index 0000000..3bba03c --- /dev/null +++ b/frontend/scripts/get-proxy-implementation.js @@ -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(); diff --git a/frontend/scripts/init-lending-config.js b/frontend/scripts/init-lending-config.js new file mode 100644 index 0000000..c19eb0c --- /dev/null +++ b/frontend/scripts/init-lending-config.js @@ -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 ✅ +`) diff --git a/frontend/scripts/simulate-deposit.js b/frontend/scripts/simulate-deposit.js new file mode 100644 index 0000000..a16957f --- /dev/null +++ b/frontend/scripts/simulate-deposit.js @@ -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) diff --git a/frontend/scripts/simulate-supply.js b/frontend/scripts/simulate-supply.js new file mode 100644 index 0000000..5f7daf7 --- /dev/null +++ b/frontend/scripts/simulate-supply.js @@ -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() diff --git a/frontend/scripts/single-buy-test.ts b/frontend/scripts/single-buy-test.ts new file mode 100644 index 0000000..2302d80 --- /dev/null +++ b/frontend/scripts/single-buy-test.ts @@ -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) diff --git a/frontend/scripts/test-calculations.js b/frontend/scripts/test-calculations.js new file mode 100644 index 0000000..553ec39 --- /dev/null +++ b/frontend/scripts/test-calculations.js @@ -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() diff --git a/frontend/scripts/test-collateral-functions.js b/frontend/scripts/test-collateral-functions.js new file mode 100644 index 0000000..cbc7ee5 --- /dev/null +++ b/frontend/scripts/test-collateral-functions.js @@ -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() diff --git a/frontend/scripts/test-config-read.js b/frontend/scripts/test-config-read.js new file mode 100644 index 0000000..2834f04 --- /dev/null +++ b/frontend/scripts/test-config-read.js @@ -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() diff --git a/frontend/scripts/test-correct-function-names.js b/frontend/scripts/test-correct-function-names.js new file mode 100644 index 0000000..143574b --- /dev/null +++ b/frontend/scripts/test-correct-function-names.js @@ -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() diff --git a/frontend/scripts/test-supply-with-trace.js b/frontend/scripts/test-supply-with-trace.js new file mode 100644 index 0000000..6eb1046 --- /dev/null +++ b/frontend/scripts/test-supply-with-trace.js @@ -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() diff --git a/frontend/scripts/test-total-functions.js b/frontend/scripts/test-total-functions.js new file mode 100644 index 0000000..eb5c837 --- /dev/null +++ b/frontend/scripts/test-total-functions.js @@ -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); diff --git a/frontend/scripts/verify-all-lending-functions.js b/frontend/scripts/verify-all-lending-functions.js new file mode 100644 index 0000000..6d92319 --- /dev/null +++ b/frontend/scripts/verify-all-lending-functions.js @@ -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); diff --git a/frontend/scripts/verify-apy-calculation.js b/frontend/scripts/verify-apy-calculation.js new file mode 100644 index 0000000..7486de7 --- /dev/null +++ b/frontend/scripts/verify-apy-calculation.js @@ -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() diff --git a/frontend/scripts/verify-current-borrow.js b/frontend/scripts/verify-current-borrow.js new file mode 100644 index 0000000..71b70b7 --- /dev/null +++ b/frontend/scripts/verify-current-borrow.js @@ -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() diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 395f4a6..90a4e35 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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('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 (
@@ -26,17 +60,51 @@ function AppContent() {

{t('header.title')}

- {t('common.network')} + + {/* 网络切换器 */} +
+ + {!isSupported && ( + + ⚠️ 不支持的网络 + + )} +
+ + {currentChainName}
- {activeTab === 'wusd' && } + {activeTab === 'usdc' && } {activeTab === 'vault' && } {activeTab === 'factory' && } {activeTab === 'lp' && } + {activeTab === 'lending' && } + {activeTab === 'holders' && }
diff --git a/frontend/src/components/AutoTestPanel.tsx b/frontend/src/components/AutoTestPanel.tsx new file mode 100644 index 0000000..5dcb4f8 --- /dev/null +++ b/frontend/src/components/AutoTestPanel.tsx @@ -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 { + 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([]) + const [logs, setLogs] = useState([]) + const [selectedTests, setSelectedTests] = useState([ + 'check_balance', + 'usdc_mint', + 'vault_buy', + 'vault_sell', + ]) + + const abortRef = useRef(false) + const logsEndRef = useRef(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 ( +
+

{t('nav.autoTest')}

+ + {/* 模式选择 */} +
+
+ +
+ + +
+
+ + {mode === 'privateKey' && ( +
+ + setPrivateKey(e.target.value)} + disabled={isRunning} + /> +
+ )} + + {mode === 'wallet' && ( +
+ {isConnected + ? `[OK] ${isZh ? '已连接' : 'Connected'}: ${connectedAddress?.slice(0, 6)}...${connectedAddress?.slice(-4)}` + : `[!] ${isZh ? '请先连接钱包' : 'Please connect wallet first'}` + } +
+ )} +
+ + {/* 测试项选择 */} +
+
+

{isZh ? '选择测试项' : 'Select Tests'}

+ +
+ +
+ {testItems.map(test => ( +
!isRunning && toggleTest(test.id)} + > +
+
+ toggleTest(test.id)} + disabled={isRunning} + style={{ width: '14px', height: '14px' }} + onClick={(e) => e.stopPropagation()} + /> + {test.name} +
+

{test.description}

+
+
+ ))} +
+
+ + {/* 操作按钮 */} +
+ {!isRunning ? ( + + ) : ( + + )} + +
+ + {/* 测试结果 */} + {results.length > 0 && ( +
+
+

{isZh ? '测试结果' : 'Test Results'}

+
+ {isZh ? '通过' : 'Pass'}: {stats.success} | + {isZh ? ' 失败' : ' Fail'}: {stats.failed} | + {isZh ? ' 等待' : ' Pending'}: {stats.pending} +
+
+ +
+ {results.map((result, idx) => ( +
+ + + {result.status === 'pending' && '...'} + {result.status === 'running' && 'RUN'} + {result.status === 'success' && 'OK'} + {result.status === 'failed' && 'FAIL'} + + {result.name} + + + {result.message} + {result.duration && ` (${result.duration}ms)`} + +
+ ))} +
+
+ )} + + {/* 日志输出 */} + {logs.length > 0 && ( +
+

{isZh ? '执行日志' : 'Execution Log'}

+
+ {logs.map((log, idx) => ( +
>>') ? '#9e9e9e' : + '#e0e0e0' + }} + > + {log} +
+ ))} +
+
+
+ )} +
+ ) +} diff --git a/frontend/src/components/ConnectButton.tsx b/frontend/src/components/ConnectButton.tsx index 592a3ff..74073b4 100644 --- a/frontend/src/components/ConnectButton.tsx +++ b/frontend/src/components/ConnectButton.tsx @@ -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 } + 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 (
- {formatAddress(address)} - +
) diff --git a/frontend/src/components/FactoryPanel.tsx b/frontend/src/components/FactoryPanel.tsx index 26ce51c..f60126d 100644 --- a/frontend/src/components/FactoryPanel.tsx +++ b/frontend/src/components/FactoryPanel.tsx @@ -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([]) 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() { {vaultImplementation || '-'}
- {t('factory.defaultHardCap')}: - {defaultHardCap ? formatUnits(defaultHardCap, 18) : '0'} + {t('factory.defaultHardCap')}: + {defaultHardCap ? formatUnits(defaultHardCap, TOKEN_DECIMALS.YT) : '0'}
{t('factory.totalVaults')}: @@ -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')} @@ -573,16 +656,6 @@ export function FactoryPanel() { className="input" />
-
- - setCreateForm({ ...createForm, initialWusdPrice: e.target.value })} - placeholder="1" - className="input" - /> -
-
+
-
- - updateBatchCreateRow(index, 'wusdPrice', e.target.value)} - placeholder="1" - className="input" - style={{ fontSize: '12px' }} - /> -
-

{t('factory.updatePrices')}

+

{t('factory.updatePrices')}

@@ -750,16 +812,6 @@ export function FactoryPanel() { ))}
-
- - setPriceForm({ ...priceForm, wusdPrice: e.target.value })} - placeholder="e.g. 1.05" - className="input" - /> -
+ {/* 单金库管理配置 */} +
+
单金库管理配置
+ + {/* 选择金库 */} +
+ + +
+ + {selectedVaultForManage && ( + <> + {/* 更新金库YT价格(USDC价格来自Chainlink) */} +
+ +

+ USDC 价格由 Chainlink 预言机自动提供,无需手动更新 +

+
+
+ + setSingleVaultPriceForm({ ytPrice: e.target.value })} + placeholder="1" + className="input" + style={{ fontSize: '13px' }} + step="0.01" + /> +
+ +
+
+ + {/* 设置硬顶 */} +
+ +
+ setSingleVaultHardCapForm(e.target.value)} + placeholder="100000" + className="input" + style={{ flex: 1, fontSize: '13px' }} + /> + +
+
+ + {/* 设置赎回时间 */} +
+ +
+ setSingleVaultRedemptionTime(e.target.value)} + className="input" + style={{ flex: 1, fontSize: '13px' }} + /> + +
+
+ + {/* 设置管理员 */} +
+ +
+ setSingleVaultManager(e.target.value)} + placeholder="0x..." + className="input" + style={{ flex: 1, fontSize: '13px' }} + /> + +
+
+ + )} +
+ {/* 设置默认硬顶 */}
@@ -2522,11 +3107,285 @@ export function LPPanel() { 配置步骤: 1. 设置价格 → 2. 设置价差(可选) → 3. 添加白名单
+ + {/* ===== P1: 核心配置 ===== */} +
+

P1: 核心配置

+ + {/* 冷却时间设置 */} +
+ +
+ setCooldownForm({ duration: e.target.value })} + placeholder="3600" + className="input" + style={{ fontSize: '12px', width: '120px' }} + /> + 当前: {cooldownDuration?.toString() || '...'} 秒 + +
+
+ + {/* 手续费配置 */} +
+ +
+ 当前: swapFee={swapFee?.toString() || '...'}, stableSwapFee={stableSwapFee?.toString() || '...'}, taxBps={taxBasisPoints?.toString() || '...'}, stableTaxBps={stableTaxBasisPoints?.toString() || '...'} +
+
+
+ + setSwapFeesForm({ ...swapFeesForm, swapFee: e.target.value })} className="input" style={{ fontSize: '11px' }} /> +
+
+ + setSwapFeesForm({ ...swapFeesForm, stableSwapFee: e.target.value })} className="input" style={{ fontSize: '11px' }} /> +
+
+ + setSwapFeesForm({ ...swapFeesForm, taxBasisPoints: e.target.value })} className="input" style={{ fontSize: '11px' }} /> +
+
+ + setSwapFeesForm({ ...swapFeesForm, stableTaxBasisPoints: e.target.value })} className="input" style={{ fontSize: '11px' }} /> +
+
+ +
+ + {/* 紧急提取代币 */} +
+ +
警告: 此操作将从池子中提取代币
+
+
+ + +
+
+ + setWithdrawForm({ ...withdrawForm, receiver: e.target.value })} placeholder="0x..." className="input" style={{ fontSize: '11px' }} /> +
+
+ + setWithdrawForm({ ...withdrawForm, amount: e.target.value })} placeholder="0" className="input" style={{ fontSize: '11px' }} /> +
+
+ +
+
+ + {/* ===== P2: 交易限制 ===== */} +
+

P2: 交易限制

+ + {/* 池子状态 + 动态手续费 */} +
+
+ +
+ + {poolPaused ? '已暂停' : '运行中'} + + +
+
+
+ +
+ + {dynamicFees ? '开启' : '关闭'} + + +
+
+
+ + {/* AUM 调整 */} +
+ +
+ 当前: +{aumAddition ? formatUnits(aumAddition, TOKEN_DECIMALS.USDY) : '0'} / -{aumDeduction ? formatUnits(aumDeduction, TOKEN_DECIMALS.USDY) : '0'} +
+
+ setAumAdjustForm({ ...aumAdjustForm, addition: e.target.value })} placeholder="增加" className="input" style={{ fontSize: '11px', width: '100px' }} /> + / + setAumAdjustForm({ ...aumAdjustForm, deduction: e.target.value })} placeholder="减少" className="input" style={{ fontSize: '11px', width: '100px' }} /> + +
+
+ + {/* 最大交换金额 */} +
+ +
+ + setMaxSwapForm({ ...maxSwapForm, amount: e.target.value })} placeholder="金额" className="input" style={{ fontSize: '11px', width: '120px' }} /> + +
+
+ + {/* 滑点和价格变化限制 */} +
+
+ +
当前: {maxSwapSlippageBps?.toString() || '...'}
+
+ setLimitsForm({ ...limitsForm, maxSwapSlippageBps: e.target.value })} className="input" style={{ fontSize: '11px', flex: 1 }} /> + +
+
+
+ +
当前: {vaultMaxPriceChangeBps?.toString() || '...'}
+
+ setLimitsForm({ ...limitsForm, maxPriceChangeBps: e.target.value })} className="input" style={{ fontSize: '11px', flex: 1 }} /> + +
+
+
+
+ + {/* ===== P3: 权限管理 ===== */} +
+

P3: 权限管理

+
警告: 权限变更是高风险操作,请确保了解影响
+ + {/* 当前权限状态 */} +
+ 当前权限: +
+ PoolManager Gov: {poolManagerGov ? `${(poolManagerGov as string).slice(0, 6)}...${(poolManagerGov as string).slice(-4)}` : '...'} + YTVault Gov: {vaultGov ? `${(vaultGov as string).slice(0, 6)}...${(vaultGov as string).slice(-4)}` : '...'} + YTVault Owner: {vaultOwner ? `${(vaultOwner as string).slice(0, 6)}...${(vaultOwner as string).slice(-4)}` : '...'} + PoolManager: {vaultPoolManager ? `${(vaultPoolManager as string).slice(0, 6)}...${(vaultPoolManager as string).slice(-4)}` : '...'} + ytLP Gov: {ytLPGov ? `${(ytLPGov as string).slice(0, 6)}...${(ytLPGov as string).slice(-4)}` : '...'} + ytLP Owner: {ytLPOwner ? `${(ytLPOwner as string).slice(0, 6)}...${(ytLPOwner as string).slice(-4)}` : '...'} +
+
+ + {/* 设置 Gov */} +
+ +
+ setPermissionForm({ ...permissionForm, govAddress: e.target.value })} placeholder="0x..." className="input" style={{ fontSize: '11px', flex: 1, minWidth: '200px' }} /> + + +
+
+ + {/* 设置 Handler */} +
+ +
+ setPermissionForm({ ...permissionForm, handlerAddress: e.target.value })} placeholder="0x..." className="input" style={{ fontSize: '11px', flex: 1 }} /> + + +
+
+ + {/* 设置 Keeper */} +
+ +
+ setPermissionForm({ ...permissionForm, keeperAddress: e.target.value })} placeholder="0x..." className="input" style={{ fontSize: '11px', flex: 1 }} /> + + +
+
+ + {/* 设置 Swapper */} +
+ +
+ setPermissionForm({ ...permissionForm, swapperAddress: e.target.value })} placeholder="0x..." className="input" style={{ fontSize: '11px', flex: 1 }} /> + + +
+
+ + {/* 设置 PoolManager */} +
+ +
+ setPermissionForm({ ...permissionForm, poolManagerAddress: e.target.value })} placeholder="0x..." className="input" style={{ fontSize: '11px', flex: 1 }} /> + +
+
+ + {/* 设置 Minter */} +
+ +
+ setPermissionForm({ ...permissionForm, minterAddress: e.target.value })} placeholder="0x..." className="input" style={{ fontSize: '11px', flex: 1 }} /> + + +
+
+
)}
- {/* USDY 内部计价代币信息 */} + {/* USDY 内部计价代币信息 - 已隐藏 */} + {false && (
- {usdyTotalSupply ? Number(formatUnits(usdyTotalSupply, 18)).toLocaleString(undefined, { maximumFractionDigits: 2 }) : '0'} USDY + {usdyTotalSupply ? Number(formatUnits(usdyTotalSupply, TOKEN_DECIMALS.USDY)).toLocaleString(undefined, { maximumFractionDigits: 2 }) : '0'} USDY
@@ -2578,7 +3437,7 @@ export function LPPanel() { PoolManager 余额:
- {poolManagerUsdyBalance ? Number(formatUnits(poolManagerUsdyBalance, 18)).toLocaleString(undefined, { maximumFractionDigits: 2 }) : '0'} USDY + {poolManagerUsdyBalance ? Number(formatUnits(poolManagerUsdyBalance, TOKEN_DECIMALS.USDY)).toLocaleString(undefined, { maximumFractionDigits: 2 }) : '0'} USDY
@@ -2689,6 +3548,7 @@ export function LPPanel() {
)}
+ )} {/* Transaction History */} diff --git a/frontend/src/components/LendingAdminPanel.tsx b/frontend/src/components/LendingAdminPanel.tsx new file mode 100644 index 0000000..8107d6b --- /dev/null +++ b/frontend/src/components/LendingAdminPanel.tsx @@ -0,0 +1,1208 @@ +import { useState, useEffect, useRef, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt, useChainId } from 'wagmi' +import { GAS_CONFIG, YT_VAULT_ABI, LENDING_ABI, getContracts, getChainName } from '../config/contracts' +import { useToast } from './Toast' + +// ERC20 Base ABI for reading token info +const ERC20_BASE_ABI = [ + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function' + } +] as const + +// 正确的 Configurator ABI (Compound V3 风格) +const CONFIGURATOR_ABI = [ + { + inputs: [ + { internalType: 'address', name: 'lendingProxy', type: 'address' }, + { + 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: 'assetConfig', + type: 'tuple' + } + ], + name: 'addAsset', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { internalType: 'address', name: 'lendingProxy', type: 'address' }, + { + 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: 'assetConfig', + type: 'tuple' + } + ], + name: 'updateAsset', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + 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' + }, + { + inputs: [ + { internalType: 'address', name: 'lendingProxy', type: 'address' }, + { internalType: 'address', name: 'asset', type: 'address' } + ], + name: 'getAssetIndex', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { internalType: 'address', name: 'lendingProxy', type: 'address' }, + { + 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: 'configuration', + type: 'tuple' + } + ], + name: 'setConfiguration', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function' + } +] as const + +interface LendingAdminPanelProps { + isOpen: boolean + onToggle: () => void +} + +export function LendingAdminPanel({ isOpen, onToggle }: LendingAdminPanelProps) { + const { t } = useTranslation() + const { address } = useAccount() + const { showToast } = useToast() + + // ===== 多链支持 ===== + const chainId = useChainId() + const CONTRACTS = getContracts(chainId) + const currentChainName = getChainName(chainId) + + // ===== 动态获取 LP 池代币列表(用作抵押品选项)===== + const { data: rawPoolTokenAddresses } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getAllPoolTokens' + }) + + // 去重处理 + const poolTokenAddresses = useMemo(() => { + if (!rawPoolTokenAddresses || rawPoolTokenAddresses.length === 0) return [] + const seen = new Set() + return (rawPoolTokenAddresses as string[]).filter((addr: string) => { + const lower = addr.toLowerCase() + if (seen.has(lower)) return false + seen.add(lower) + return true + }) + }, [rawPoolTokenAddresses]) + + // 批量读取代币 symbol 和 decimals + 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: 'decimals' as const } + ]) + }, [poolTokenAddresses]) + + const { data: tokenInfoResults } = useReadContracts({ + contracts: tokenInfoContracts + }) + + // 批量读取白名单状态 + 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 } = useReadContracts({ + contracts: whitelistContracts + }) + + // 构建动态代币列表 + const COLLATERAL_ASSETS = useMemo(() => { + if (!poolTokenAddresses || poolTokenAddresses.length === 0) return [] + return poolTokenAddresses.map((addr: string, index: number) => { + const symbol = tokenInfoResults?.[index * 2]?.result as string | undefined + const decimals = tokenInfoResults?.[index * 2 + 1]?.result as number | undefined + const isWhitelisted = whitelistResults?.[index]?.result as boolean | undefined + return { + address: addr as `0x${string}`, + name: symbol || `Token-${index + 1}`, + decimals: decimals || 18, + isWhitelisted: isWhitelisted || false + } + }) + }, [poolTokenAddresses, tokenInfoResults, whitelistResults]) + + const [selectedAsset, setSelectedAsset] = useState('') + // 使用用户友好的格式存储(百分比和代币数量) + const [borrowCollateralFactor, setBorrowCollateralFactor] = useState('75') // 75% + const [liquidateCollateralFactor, setLiquidateCollateralFactor] = useState('85') // 85% + const [liquidationFactor, setLiquidationFactor] = useState('105') // 105% + const [supplyCap, setSupplyCap] = useState('1000000') // 1M tokens + const [newTargetReserves, setNewTargetReserves] = useState('') // 新的目标储备金(USDC) + + const { writeContract, data: hash, isPending, error: writeError } = useWriteContract() + const { isLoading: isConfirming, isSuccess: isConfirmed, isError: isTxError, error: txError } = useWaitForTransactionReceipt({ hash }) + + // 用于跟踪已处理的交易,避免重复提示 + const processedHashRef = useRef(null) + const processedErrorRef = useRef(false) + + // 读取Configurator owner + const { data: configuratorOwner } = useReadContract({ + address: CONTRACTS.LENDING_CONFIGURATOR, + abi: CONFIGURATOR_ABI, + functionName: 'owner' + }) + + // 读取Lending owner + const { data: lendingOwner } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'owner' + }) + + // 读取暂停状态 + const { data: isPaused, refetch: refetchPaused } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'paused' + }) + + // 读取完整配置 + const { data: configuration, refetch: refetchConfig } = useReadContract({ + address: CONTRACTS.LENDING_CONFIGURATOR, + abi: CONFIGURATOR_ABI, + functionName: 'getConfiguration', + args: [CONTRACTS.LENDING_PROXY as `0x${string}`] + }) + + // 读取利率配置(从 lendingProxy 合约直接读取) + const { data: borrowKinkValue } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'borrowKink' + }) + + const { data: supplyKinkValue } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'supplyKink' + }) + + const { data: borrowRateBase } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'borrowPerSecondInterestRateBase' + }) + + const { data: borrowRateSlopeLow } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'borrowPerSecondInterestRateSlopeLow' + }) + + const { data: borrowRateSlopeHigh } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'borrowPerSecondInterestRateSlopeHigh' + }) + + const { data: supplyRateBase } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'supplyPerSecondInterestRateBase' + }) + + const { data: supplyRateSlopeLow } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'supplyPerSecondInterestRateSlopeLow' + }) + + const { data: supplyRateSlopeHigh } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'supplyPerSecondInterestRateSlopeHigh' + }) + + // 初始化选中的资产(当代币列表加载完成后) + useEffect(() => { + if (COLLATERAL_ASSETS.length > 0 && !selectedAsset) { + setSelectedAsset(COLLATERAL_ASSETS[0].address) + } + }, [COLLATERAL_ASSETS, selectedAsset]) + + // 检查权限 + const isConfiguratorOwner = address && configuratorOwner && + address.toLowerCase() === configuratorOwner.toLowerCase() + const isLendingOwner = address && lendingOwner && + address.toLowerCase() === lendingOwner.toLowerCase() + + // 当新交易开始时重置错误标志 + useEffect(() => { + if (hash && hash !== processedHashRef.current) { + processedErrorRef.current = false + } + }, [hash]) + + // 选中资产变化时重新加载配置 + useEffect(() => { + refetchConfig() + }, [selectedAsset, refetchConfig]) + + // 交易确认成功后自动刷新配置 + useEffect(() => { + if (isConfirmed && hash && hash !== processedHashRef.current) { + processedHashRef.current = hash + showToast('success', '交易确认成功!正在刷新配置...') + setTimeout(() => { + refetchConfig() + refetchPaused() + }, 2000) + } + }, [isConfirmed, hash]) + + // 交易失败处理 + useEffect(() => { + if (isTxError && !processedErrorRef.current) { + processedErrorRef.current = true + console.error('Transaction failed:', txError) + const errorMsg = txError?.message || '交易失败' + showToast('error', `交易失败: ${errorMsg}`) + } + }, [isTxError, txError]) + + // 写入合约错误处理 + useEffect(() => { + if (writeError && !processedErrorRef.current) { + processedErrorRef.current = true + console.error('Write contract error:', writeError) + const errorMsg = writeError?.message || '操作失败' + showToast('error', `操作失败: ${errorMsg}`) + } + }, [writeError]) + + // 获取当前选中资产的配置 + const selectedAssetConfig = configuration?.assetConfigs?.find( + (cfg: any) => cfg.asset.toLowerCase() === selectedAsset.toLowerCase() + ) + + // 获取所有资产的配置映射(用于显示) + const assetConfigsMap = useMemo(() => { + const map = new Map() + configuration?.assetConfigs?.forEach((cfg: any) => { + map.set(cfg.asset.toLowerCase(), cfg) + }) + return map + }, [configuration]) + + // 添加或更新抵押品配置 + const handleSetConfig = async () => { + if (!address || !isConfiguratorOwner) { + showToast('error', t('lending.admin.notOwner')) + return + } + + const selectedAssetInfo = COLLATERAL_ASSETS.find(a => a.address === selectedAsset) + if (!selectedAssetInfo) return + + try { + // 将百分比转换为 1e18 格式(75% -> 0.75e18) + const borrowFactorWei = BigInt(Math.floor(parseFloat(borrowCollateralFactor) * 1e16)) // 75 -> 75e16 = 0.75e18 + const liquidateFactorWei = BigInt(Math.floor(parseFloat(liquidateCollateralFactor) * 1e16)) + const liquidationFactorWei = BigInt(Math.floor(parseFloat(liquidationFactor) * 1e16)) + // 将代币数量转换为 wei(18位精度) + const supplyCapWei = BigInt(Math.floor(parseFloat(supplyCap) * 1e18)) + + console.log('Setting asset config:', { + asset: selectedAsset, + decimals: selectedAssetInfo.decimals, + borrowCollateralFactor: borrowCollateralFactor + '% -> ' + borrowFactorWei.toString(), + liquidateCollateralFactor: liquidateCollateralFactor + '% -> ' + liquidateFactorWei.toString(), + liquidationFactor: liquidationFactor + '% -> ' + liquidationFactorWei.toString(), + supplyCap: supplyCap + ' tokens -> ' + supplyCapWei.toString() + }) + + const assetConfig = { + asset: selectedAsset as `0x${string}`, + decimals: selectedAssetInfo.decimals, + borrowCollateralFactor: borrowFactorWei, + liquidateCollateralFactor: liquidateFactorWei, + liquidationFactor: liquidationFactorWei, + supplyCap: supplyCapWei + } + + // 检查资产是否已存在 + const functionName = selectedAssetConfig ? 'updateAsset' : 'addAsset' + + await writeContract({ + address: CONTRACTS.LENDING_CONFIGURATOR, + abi: CONFIGURATOR_ABI, + functionName, + args: [CONTRACTS.LENDING_PROXY as `0x${string}`, assetConfig], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS + }) + showToast('success', `${selectedAssetConfig ? '更新' : '添加'}配置交易已发送`) + } catch (error) { + console.error('Set config error:', error) + showToast('error', t('toast.txFailed')) + } + } + + // 暂停/恢复系统 + const handleTogglePause = async (pause: boolean) => { + if (!address || !isLendingOwner) { + showToast('error', t('lending.admin.notOwner')) + return + } + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: pause ? 'pause' : 'unpause', + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS + }) + showToast('success', pause ? t('lending.admin.pauseSent') : t('lending.admin.unpauseSent')) + } catch (error) { + console.error('Toggle pause error:', error) + showToast('error', t('toast.txFailed')) + } + } + + // 初始化基础配置(设置 USDC 和价格源) + const handleInitBaseConfig = async () => { + if (!address || !isConfiguratorOwner) { + showToast('error', t('lending.admin.notOwner')) + return + } + + try { + // 构建完整的配置结构,保留现有的抵押品配置 + const newConfig = { + baseToken: CONTRACTS.USDC, // 设置 USDC 地址 + lendingPriceSource: CONTRACTS.LENDING_PRICE_FEED, // 设置价格源 + supplyKink: BigInt(8e17), // 80% + supplyPerYearInterestRateSlopeLow: BigInt(5e16), // 5% + supplyPerYearInterestRateSlopeHigh: BigInt(2e17), // 20% + supplyPerYearInterestRateBase: BigInt(0), + borrowKink: BigInt(8e17), // 80% + borrowPerYearInterestRateSlopeLow: BigInt(8e16), // 8% + borrowPerYearInterestRateSlopeHigh: BigInt(4e17), // 40% + borrowPerYearInterestRateBase: BigInt(2e16), // 2% + storeFrontPriceFactor: BigInt(1e18), + trackingIndexScale: BigInt(1e15), + baseBorrowMin: BigInt(1e6), // 1 USDC (6 decimals) + targetReserves: BigInt(1e10), // 10,000 USDC + assetConfigs: configuration?.assetConfigs || [] // 保留现有抵押品配置 + } + + await writeContract({ + address: CONTRACTS.LENDING_CONFIGURATOR, + abi: CONFIGURATOR_ABI, + functionName: 'setConfiguration', + args: [CONTRACTS.LENDING_PROXY, newConfig], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS + }) + showToast('success', '正在初始化基础配置...') + } catch (error) { + console.error('Init base config error:', error) + showToast('error', '初始化失败: ' + (error as Error).message) + } + } + + // 设置目标储备金 + const handleSetTargetReserves = async () => { + if (!address || !isConfiguratorOwner) { + showToast('error', t('lending.admin.notOwner')) + return + } + + if (!newTargetReserves || !configuration) { + showToast('error', '请输入目标储备金数量') + return + } + + try { + const targetReservesValue = BigInt(Number(newTargetReserves) * 1e6) // USDC 6位精度 + + // 保留现有配置,只修改 targetReserves + const updatedConfig = { + baseToken: configuration.baseToken, + lendingPriceSource: configuration.lendingPriceSource, + supplyKink: configuration.supplyKink, + supplyPerYearInterestRateSlopeLow: configuration.supplyPerYearInterestRateSlopeLow, + supplyPerYearInterestRateSlopeHigh: configuration.supplyPerYearInterestRateSlopeHigh, + supplyPerYearInterestRateBase: configuration.supplyPerYearInterestRateBase, + borrowKink: configuration.borrowKink, + borrowPerYearInterestRateSlopeLow: configuration.borrowPerYearInterestRateSlopeLow, + borrowPerYearInterestRateSlopeHigh: configuration.borrowPerYearInterestRateSlopeHigh, + borrowPerYearInterestRateBase: configuration.borrowPerYearInterestRateBase, + storeFrontPriceFactor: configuration.storeFrontPriceFactor, + trackingIndexScale: configuration.trackingIndexScale, + baseBorrowMin: configuration.baseBorrowMin, + targetReserves: targetReservesValue, // 更新目标储备金 + assetConfigs: configuration.assetConfigs || [] + } + + await writeContract({ + address: CONTRACTS.LENDING_CONFIGURATOR, + abi: CONFIGURATOR_ABI, + functionName: 'setConfiguration', + args: [CONTRACTS.LENDING_PROXY, updatedConfig], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS + }) + showToast('success', `正在设置目标储备金为 ${newTargetReserves} USDC...`) + setNewTargetReserves('') // 清空输入 + } catch (error) { + console.error('Set target reserves error:', error) + showToast('error', '设置失败: ' + (error as Error).message) + } + } + + // 从配置加载到表单(从 wei 转换为用户友好格式) + const loadConfig = () => { + if (selectedAssetConfig) { + // 从 wei 转换为百分比(0.75e18 -> 75) + setBorrowCollateralFactor((Number(selectedAssetConfig.borrowCollateralFactor) / 1e16).toString()) + setLiquidateCollateralFactor((Number(selectedAssetConfig.liquidateCollateralFactor) / 1e16).toString()) + setLiquidationFactor((Number(selectedAssetConfig.liquidationFactor) / 1e16).toString()) + // 从 wei 转换为代币数量(1e24 -> 1000000) + setSupplyCap((Number(selectedAssetConfig.supplyCap) / 1e18).toString()) + } + } + + // 转换显示值(从 1e18 格式到百分比) + const toPercentage = (value: bigint | undefined) => { + if (!value) return '0' + return (Number(value) / 1e16).toFixed(2) // 1e18 * 0.75 = 0.75e18, /1e16 = 75 + } + + return ( +
+
+
+

+ {t('lending.admin.title')} +

+ {/* 折叠状态下显示摘要信息 */} + {!isOpen && ( +
+
+ 系统: + + {isPaused ? '已暂停' : '运行中'} + +
+ {COLLATERAL_ASSETS.slice(0, 3).map((asset) => { + const config = assetConfigsMap.get(asset.address.toLowerCase()) + return ( +
+ {asset.name}: + + {config ? '已配置' : '未配置'} + + {config && ( + + ({toPercentage(config.borrowCollateralFactor)}%) + + )} +
+ ) + })} +
+ )} +
+ {isOpen ? '▼' : '▶'} +
+ + {isOpen && ( +
+ {/* 管理员信息 */} +
+

Configurator Owner: {configuratorOwner || 'Loading...'}

+

Lending Owner: {lendingOwner || 'Loading...'}

+

+ {isConfiguratorOwner ? 'You are Configurator Owner' : isLendingOwner ? 'You are Lending Owner' : 'You are not an admin'} +

+
+ + {/* 所有抵押品状态概览 */} +

+ 所有抵押品状态 +

+
+
+ {COLLATERAL_ASSETS.map((asset) => { + const config = assetConfigsMap.get(asset.address.toLowerCase()) + return ( +
+
+ {asset.name} {config ? '(已配置)' : '(未配置)'} +
+ {config ? ( +
+
借款抵押率: {toPercentage(config.borrowCollateralFactor)}%
+
清算抵押率: {toPercentage(config.liquidateCollateralFactor)}%
+
清算奖励: {toPercentage(config.liquidationFactor)}%
+
供应上限: {(Number(config.supplyCap) / (10 ** asset.decimals)).toLocaleString()}
+
+ ) : ( +
未配置
+ )} +
+ ) + })} +
+
+ + {/* 基础配置状态 */} +

+ 基础配置 +

+
+
+
+ USDC 地址: + + {configuration?.baseToken && configuration?.baseToken !== '0x0000000000000000000000000000000000000000' + ? `${configuration.baseToken.slice(0, 6)}...${configuration.baseToken.slice(-4)}` + : '未设置'} + +
+
+ 价格源: + + {configuration?.lendingPriceSource && configuration?.lendingPriceSource !== '0x0000000000000000000000000000000000000000' + ? `${configuration.lendingPriceSource.slice(0, 6)}...${configuration.lendingPriceSource.slice(-4)}` + : '未设置'} + +
+
+ + {/* 利率配置显示 */} +
+
利率配置参数
+
+ {/* Kink 拐点 */} +
+
拐点 (Kink):
+
+ 供应拐点: + + {supplyKinkValue ? `${(Number(supplyKinkValue) / 1e18 * 100).toFixed(1)}%` : 'Loading...'} + +
+
+ 借款拐点: + + {borrowKinkValue ? `${(Number(borrowKinkValue) / 1e18 * 100).toFixed(1)}%` : 'Loading...'} + +
+
+ + {/* 供应利率 */} +
+
供应利率 (APY):
+
+ 基础利率: + + {supplyRateBase ? `${(Number(supplyRateBase) * 31536000 / 1e18 * 100).toFixed(2)}%` : 'Loading...'} + +
+
+ 拐点前斜率: + + {supplyRateSlopeLow ? `${(Number(supplyRateSlopeLow) * 31536000 / 1e18 * 100).toFixed(2)}%` : 'Loading...'} + +
+
+ 拐点后斜率: + + {supplyRateSlopeHigh ? `${(Number(supplyRateSlopeHigh) * 31536000 / 1e18 * 100).toFixed(2)}%` : 'Loading...'} + +
+
+ + {/* 借款利率 */} +
+
借款利率 (APY):
+
+ 基础利率: + + {borrowRateBase ? `${(Number(borrowRateBase) * 31536000 / 1e18 * 100).toFixed(2)}%` : 'Loading...'} + +
+
+ 拐点前斜率: + + {borrowRateSlopeLow ? `${(Number(borrowRateSlopeLow) * 31536000 / 1e18 * 100).toFixed(2)}%` : 'Loading...'} + +
+
+ 拐点后斜率: + + {borrowRateSlopeHigh ? `${(Number(borrowRateSlopeHigh) * 31536000 / 1e18 * 100).toFixed(2)}%` : 'Loading...'} + +
+
+
+
+ {isConfiguratorOwner && (!configuration?.baseToken || configuration?.baseToken === '0x0000000000000000000000000000000000000000') && ( + + )} + {(!configuration?.baseToken || configuration?.baseToken === '0x0000000000000000000000000000000000000000') && ( +
+ 注意: 基础配置未初始化,存入抵押品功能将无法使用。请点击上方按钮初始化。 +
+ )} +
+ + {/* 系统状态 */} +

+ {t('lending.admin.systemStatus')} +

+
+
+ {t('lending.admin.systemPaused')}: + + {isPaused ? t('common.yes') : t('common.no')} + +
+ {isLendingOwner && ( + + )} +
+ + {/* 目标储备金设置 */} +

+ 目标储备金设置 +

+
+
+
+ 当前目标储备金: + + {configuration?.targetReserves + ? `${(Number(configuration.targetReserves) / 1e6).toLocaleString()} USDC` + : 'N/A'} + +
+
+ 提示: 当储备金低于目标值时,用户可以购买清算抵押品 +
+
+ {isConfiguratorOwner && configuration?.baseToken && ( +
+ +
+ setNewTargetReserves(e.target.value)} + placeholder="例如: 10000" + className="input" + style={{ flex: 1, fontSize: '12px', padding: '6px' }} + min="0" + step="100" + /> + +
+
+ )} +
+ + {/* 抵押品配置 */} +

+ {t('lending.admin.collateralConfig')} +

+ + {/* 选择资产 */} +
+ +
+ + +
+
+ + {/* 当前链上配置 */} +
+
+
+ {COLLATERAL_ASSETS.find(a => a.address === selectedAsset)?.name} 当前链上配置 +
+ {selectedAssetConfig && ( + + 已配置 + + )} +
+ + {selectedAssetConfig ? ( + <> +
+
+
借款抵押率
+ + {toPercentage(selectedAssetConfig.borrowCollateralFactor)}% + +
+
+
清算抵押率
+ + {toPercentage(selectedAssetConfig.liquidateCollateralFactor)}% + +
+
+
清算奖励
+ + {toPercentage(selectedAssetConfig.liquidationFactor)}% + +
+
+
供应上限
+ + {(Number(selectedAssetConfig.supplyCap) / 1e18).toLocaleString()} + +
+
+ + + ) : ( +
+ 未配置此资产
+ + 请在下方设置参数后点击"添加配置" + +
+ )} +
+ + {/* 配置表单 */} + {isConfiguratorOwner && ( + <> +
+
参数说明:
+
+ • 借款抵押率:用户可按抵押品价值的百分比借款(输入 75 表示 75%)
+ • 清算抵押率:低于此比例时可被清算(输入 85 表示 85%)
+ • 清算奖励:清算人获得的奖励(输入 105 表示 105%)
+ • 供应上限:最大可存入的代币数量(输入代币数量,如 1000000 = 100万代币) +
+
+ +
+
+ + setBorrowCollateralFactor(e.target.value)} + placeholder="75" + className="input" + style={{ fontSize: '13px', width: '100%' }} + /> +
+ 例如:75 表示 75% +
+
+
+ + setLiquidateCollateralFactor(e.target.value)} + placeholder="85" + className="input" + style={{ fontSize: '13px', width: '100%' }} + /> +
+ 例如:85 表示 85% +
+
+
+ + setLiquidationFactor(e.target.value)} + placeholder="105" + className="input" + style={{ fontSize: '13px', width: '100%' }} + /> +
+ 例如:105 表示 105% +
+
+
+ + setSupplyCap(e.target.value)} + placeholder="1000000" + className="input" + style={{ fontSize: '13px', width: '100%' }} + /> +
+ 例如:1000000 表示 100万 代币 +
+
+
+ + + + {/* 交易状态提示 */} + {(isPending || isConfirming) && ( +
+
+ {isPending && '等待钱包确认...'} + {isConfirming && '交易确认中...'} +
+ {hash && ( +
+ 交易哈希: + {hash.slice(0, 10)}...{hash.slice(-8)} + +
+ 点击查看区块链浏览器 +
+ )} +
+ )} + + {/* 交易失败提示 */} + {isTxError && ( +
+
+ 交易失败 +
+
+ {txError?.message || '未知错误'} +
+ {hash && ( + + )} +
+ )} + + )} + + {/* 调试信息 */} +
+
调试信息
+
+
+ 当前选中资产:{' '} + {COLLATERAL_ASSETS.find(a => a.address === selectedAsset)?.name} +
+
+ 资产地址:{' '} + {selectedAsset} +
+
+ 配置状态:{' '} + + {selectedAssetConfig ? '已配置' : '未配置'} + +
+ {selectedAssetConfig && ( + <> +
+ 借款抵押率(wei):{' '} + {selectedAssetConfig.borrowCollateralFactor.toString()} +
+
+ 清算抵押率(wei):{' '} + {selectedAssetConfig.liquidateCollateralFactor.toString()} +
+ + )} +
+ Lending Proxy:{' '} + {CONTRACTS.LENDING_PROXY} +
+
+ Configurator:{' '} + {CONTRACTS.LENDING_CONFIGURATOR} +
+ {hash && ( +
+ 最近交易:{' '} + + {hash} + +
+ )} +
+
+
+ )} +
+ ) +} diff --git a/frontend/src/components/LendingPanel.css b/frontend/src/components/LendingPanel.css new file mode 100644 index 0000000..2e2d786 --- /dev/null +++ b/frontend/src/components/LendingPanel.css @@ -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; + } +} diff --git a/frontend/src/components/LendingPanel.tsx b/frontend/src/components/LendingPanel.tsx new file mode 100644 index 0000000..7ef4836 --- /dev/null +++ b/frontend/src/components/LendingPanel.tsx @@ -0,0 +1,2248 @@ +import { useState, useEffect, useRef, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt, usePublicClient, useChainId } from 'wagmi' +import { parseUnits, formatUnits } from 'viem' +import { + GAS_CONFIG, + LENDING_ABI, + USDC_ABI, + YT_TOKEN_ABI, + YT_VAULT_ABI, + getContracts, + getDecimals, + getChainName, + getUSDCDecimals +} from '../config/contracts' + +// ERC20 Base ABI +const ERC20_BASE_ABI = [ + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function' + } +] as const +import { useTransactions } from '../context/TransactionContext' +import type { TransactionType } from '../context/TransactionContext' +import { useToast } from './Toast' +import { TransactionHistory } from './TransactionHistory' +import { LendingAdminPanel } from './LendingAdminPanel' +import './LendingPanel.css' + +// Configurator ABI - 使用正确的 getConfiguration 函数 +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' + } +] as const + +// Price Feed ABI - 价格预言机 +const PRICE_FEED_ABI = [ + { + inputs: [{ internalType: 'address', name: 'asset', type: 'address' }], + name: 'getPrice', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + } +] as const + +export function LendingPanel() { + const { t } = useTranslation() + const { address, isConnected } = useAccount() + const { transactions, addTransaction, updateTransaction, clearHistory } = useTransactions() + const { showToast } = useToast() + const pendingTxRef = useRef<{ id?: string; type: TransactionType; amount?: string; token?: string } | null>(null) + + // ===== 多链支持 ===== + const chainId = useChainId() + const CONTRACTS = getContracts(chainId) + const TOKEN_DECIMALS = getDecimals(chainId) + const currentChainName = getChainName(chainId) + + // 表单状态 + const [activeTab, setActiveTab] = useState<'supplyUSDC' | 'withdrawUSDC' | 'deposit' | 'borrow' | 'repay' | 'withdraw'>('supplyUSDC') + const [selectedCollateral, setSelectedCollateral] = useState('') + const [depositAmount, setDepositAmount] = useState('') + const [borrowAmount, setBorrowAmount] = useState('') + const [repayAmount, setRepayAmount] = useState('') + const [withdrawAmount, setWithdrawAmount] = useState('') + const [supplyUSDCAmount, setSupplyUSDCAmount] = useState('') + const [withdrawUSDCAmount, setWithdrawUSDCAmount] = useState('') + const [lastError, setLastError] = useState('') + const [showAdminConfig, setShowAdminConfig] = useState(false) + + // 清算和储备管理功能状态 (隐藏功能) + const [liquidateBorrower, setLiquidateBorrower] = useState('') + const [batchLiquidateAddresses, setBatchLiquidateAddresses] = useState('') // 批量清算地址(逗号分隔) + const [buyCollateralAsset, setBuyCollateralAsset] = useState('') + const [buyCollateralBaseAmount, setBuyCollateralBaseAmount] = useState('') + const [withdrawReservesTo, setWithdrawReservesTo] = useState('') + const [withdrawReservesAmount, setWithdrawReservesAmount] = useState('') + const SHOW_ADVANCED_FEATURES = true // 控制是否显示高级功能 + const LIQUIDATION_THRESHOLD = 10n * 10n ** 30n // $10 清算阈值(30位精度) + + // 清算监控状态 + const [liquidationHistory, setLiquidationHistory] = useState([]) + const [isLoadingHistory, setIsLoadingHistory] = useState(false) + const [showLiquidationMonitor, setShowLiquidationMonitor] = useState(false) + + const { writeContract, data: hash, isPending } = useWriteContract() + const publicClient = usePublicClient() + + // 读取完整配置(包含所有抵押品)- 需要先读取才能生成动态抵押品列表 + const { data: lendingConfig } = useReadContract({ + address: CONTRACTS.LENDING_CONFIGURATOR, + abi: CONFIGURATOR_ABI, + functionName: 'getConfiguration', + args: [CONTRACTS.LENDING_PROXY] + }) + + // ===== 动态获取 LP 池代币列表(用于系统配置状态显示)===== + const { data: rawPoolTokenAddresses } = useReadContract({ + address: CONTRACTS.YT_VAULT, + abi: YT_VAULT_ABI, + functionName: 'getAllPoolTokens' + }) + + // 去重处理 + const poolTokenAddresses = useMemo(() => { + if (!rawPoolTokenAddresses || rawPoolTokenAddresses.length === 0) return [] + const seen = new Set() + return (rawPoolTokenAddresses as string[]).filter((addr: string) => { + const lower = addr.toLowerCase() + if (seen.has(lower)) return false + seen.add(lower) + return true + }) + }, [rawPoolTokenAddresses]) + + // 批量读取代币 symbol 和 decimals + const poolTokenInfoContracts = 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: 'decimals' as const } + ]) + }, [poolTokenAddresses]) + + const { data: poolTokenInfoResults } = useReadContracts({ + contracts: poolTokenInfoContracts + }) + + // 批量读取白名单状态 + const poolWhitelistContracts = 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: poolWhitelistResults } = useReadContracts({ + contracts: poolWhitelistContracts + }) + + // 构建所有白名单代币列表 + const ALL_WHITELISTED_TOKENS = useMemo(() => { + if (!poolTokenAddresses || poolTokenAddresses.length === 0) return [] + return poolTokenAddresses.map((addr: string, index: number) => { + const symbol = poolTokenInfoResults?.[index * 2]?.result as string | undefined + const decimals = poolTokenInfoResults?.[index * 2 + 1]?.result as number | undefined + const isWhitelisted = poolWhitelistResults?.[index]?.result as boolean | undefined + return { + address: addr as `0x${string}`, + name: symbol || `Token-${index + 1}`, + decimals: decimals || 18, + isWhitelisted: isWhitelisted || false + } + }) + }, [poolTokenAddresses, poolTokenInfoResults, poolWhitelistResults]) + + // 生成配置映射(用于快速查找配置) + const assetConfigsMap = useMemo(() => { + const map = new Map() + lendingConfig?.assetConfigs?.forEach((cfg: any) => { + map.set(cfg.asset.toLowerCase(), cfg) + }) + return map + }, [lendingConfig]) + + // 动态生成抵押品列表(从合约配置中获取 - 仅包含已配置的) + const COLLATERAL_ASSETS = useMemo(() => { + if (!lendingConfig?.assetConfigs || lendingConfig.assetConfigs.length === 0) { + return [] + } + + return lendingConfig.assetConfigs.map((config: any, index: number) => ({ + name: `Asset-${index + 1}`, // 临时名称,后续会从代币合约读取symbol + address: config.asset as `0x${string}`, + decimals: config.decimals, + borrowCollateralFactor: config.borrowCollateralFactor, + liquidateCollateralFactor: config.liquidateCollateralFactor, + })) + }, [lendingConfig]) + + // 读取用户借款余额 + const { data: borrowBalance, refetch: refetchBorrowBalance } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'borrowBalanceOf', + args: address ? [address] : undefined, + query: { enabled: !!address } + }) + + // 读取用户principal(用于计算USDC存款余额) + const { data: userPrincipal, refetch: refetchUserPrincipal } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'userBasic', + args: address ? [address] : undefined, + query: { enabled: !!address } + }) + + // 读取supplyIndex(用于计算含利息的余额) + const { data: supplyIndex } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'supplyIndex', + query: { enabled: true } + }) + + // 读取borrowIndex + const { data: borrowIndexData } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'borrowIndex', + query: { enabled: true } + }) + + // 读取用户USDC余额(在Lending合约中的余额) + const { refetch: refetchLendingBalance } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getBalance', + args: address ? [address] : undefined, + query: { enabled: !!address } + }) + + // 读取用户USDC钱包余额 + const { data: usdcBalance, refetch: refetchUsdcBalance } = useReadContract({ + address: CONTRACTS.USDC, + abi: USDC_ABI, + functionName: 'balanceOf', + args: address ? [address] : undefined, + query: { enabled: !!address } + }) + + // 读取USDC授权额度 + const { data: usdcAllowance, refetch: refetchUsdcAllowance } = useReadContract({ + address: CONTRACTS.USDC, + abi: USDC_ABI, + functionName: 'allowance', + args: address ? [address, CONTRACTS.LENDING_PROXY] : undefined, + query: { enabled: !!address } + }) + + // 读取各个抵押品余额和授权 + const collateralContracts = COLLATERAL_ASSETS.flatMap(asset => [ + { + address: asset.address as `0x${string}`, + abi: YT_TOKEN_ABI, + functionName: 'balanceOf' as const, + args: address ? [address] : undefined, + }, + { + address: asset.address as `0x${string}`, + abi: YT_TOKEN_ABI, + functionName: 'allowance' as const, + args: address ? [address, CONTRACTS.LENDING_PROXY] : undefined, + }, + { + address: CONTRACTS.LENDING_PROXY as `0x${string}`, + abi: LENDING_ABI, + functionName: 'getCollateral' as const, + args: address ? [address, asset.address] : undefined, + } + ]) + + const { data: collateralData, refetch: refetchCollateralData } = useReadContracts({ + contracts: collateralContracts, + query: { enabled: !!address } + }) + + // 读取系统信息 + const { data: systemInfoData, refetch: refetchSystemInfo } = useReadContracts({ + contracts: [ + { + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getTotalSupply' + }, + { + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getTotalBorrow' + }, + { + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getUtilization' + }, + { + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getBorrowRate' + }, + { + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getReserves' + } + ] + }) + + // 读取系统暂停状态 + const { data: isPaused } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'paused' + }) + + // 读取储备金数据 (高级功能) + const { data: currentReserves } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getReserves', + query: { enabled: SHOW_ADVANCED_FEATURES } + }) + + const { data: targetReservesData } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'targetReserves', + query: { enabled: SHOW_ADVANCED_FEATURES } + }) + + // 读取清算状态 (当输入借款人地址时查询) + // 查询借款人是否可清算 + const { data: isLiquidatableData } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'isLiquidatable', + args: liquidateBorrower ? [liquidateBorrower as `0x${string}`] : undefined, + query: { enabled: SHOW_ADVANCED_FEATURES && !!liquidateBorrower } + }) + + // 查询借款人的债务余额 + const { data: borrowerDebtBalance } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'borrowBalanceOf', + args: liquidateBorrower ? [liquidateBorrower as `0x${string}`] : undefined, + query: { enabled: SHOW_ADVANCED_FEATURES && !!liquidateBorrower } + }) + + // 读取 base token 地址(USDC) + const baseToken = useMemo(() => { + return lendingConfig?.baseToken as `0x${string}` | undefined + }, [lendingConfig]) + + // 读取 base token 价格 + const { data: baseTokenPrice } = useReadContract({ + address: CONTRACTS.LENDING_PRICE_FEED as `0x${string}`, + abi: PRICE_FEED_ABI, + functionName: 'getPrice', + args: baseToken ? [baseToken] : undefined, + query: { enabled: SHOW_ADVANCED_FEATURES && !!baseToken } + }) + + // 计算借款人的债务价值(USD) + const borrowerDebtValue = useMemo(() => { + if (!borrowerDebtBalance || !baseTokenPrice) return 0n + // debtValue = borrowBalance (6 decimals) × basePrice (30 decimals) / 10^6 + return (borrowerDebtBalance * baseTokenPrice) / 10n ** 6n + }, [borrowerDebtBalance, baseTokenPrice]) + + // 检查债务是否超过清算阈值 + const isAboveThreshold = useMemo(() => { + return borrowerDebtValue >= LIQUIDATION_THRESHOLD + }, [borrowerDebtValue]) + + // 读取抵押品储备(当选择了资产时) + const { data: collateralReserveData } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'getCollateralReserves', + args: buyCollateralAsset ? [buyCollateralAsset as `0x${string}`] : undefined, + query: { enabled: SHOW_ADVANCED_FEATURES && !!buyCollateralAsset } + }) + + // 读取可购买抵押品数量 (当输入资产和金额时查询) + const { data: quoteCollateralData } = useReadContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'quoteCollateral', + args: buyCollateralAsset && buyCollateralBaseAmount + ? [buyCollateralAsset as `0x${string}`, parseUnits(buyCollateralBaseAmount, TOKEN_DECIMALS.USDC)] + : undefined, + query: { enabled: SHOW_ADVANCED_FEATURES && !!buyCollateralAsset && !!buyCollateralBaseAmount } + }) + + // 读取抵押品的代币符号(用于显示名称) + const { data: tokenSymbolsData } = useReadContracts({ + contracts: COLLATERAL_ASSETS.map(asset => ({ + address: asset.address, + abi: YT_TOKEN_ABI, + functionName: 'symbol' + })) + }) + + // 读取抵押品价格 + const { data: pricesData } = useReadContracts({ + contracts: COLLATERAL_ASSETS.map(asset => ({ + address: CONTRACTS.LENDING_PRICE_FEED as `0x${string}`, + abi: PRICE_FEED_ABI, + functionName: 'getPrice', + args: [asset.address as `0x${string}`] + })) + }) + + // 为抵押品添加实际的代币名称 + const COLLATERAL_ASSETS_WITH_NAMES = useMemo(() => { + return COLLATERAL_ASSETS.map((asset, index) => ({ + ...asset, + name: tokenSymbolsData?.[index]?.result as string || asset.name + })) + }, [COLLATERAL_ASSETS, tokenSymbolsData]) + + // 初始化选中的抵押品 + useEffect(() => { + if (COLLATERAL_ASSETS_WITH_NAMES.length > 0 && !selectedCollateral) { + setSelectedCollateral(COLLATERAL_ASSETS_WITH_NAMES[0].address) + } + }, [COLLATERAL_ASSETS_WITH_NAMES, selectedCollateral]) + + // 监听交易状态 + const { isLoading: isConfirming, isSuccess: isConfirmed, isError: isTxError, error: txError } = useWaitForTransactionReceipt({ + hash, + }) + + // 当获取到交易哈希后,添加到历史记录 + useEffect(() => { + if (hash && pendingTxRef.current && !pendingTxRef.current.id) { + const { type, amount, token } = pendingTxRef.current + const txId = addTransaction({ + type, + hash, + status: 'pending', + amount, + token + }) + pendingTxRef.current = { ...pendingTxRef.current, id: txId } + } + }, [hash]) + + // 交易确认后更新 + useEffect(() => { + if (isConfirmed && pendingTxRef.current && pendingTxRef.current.id) { + const { id, type } = pendingTxRef.current + updateTransaction(id, { status: 'success' }) + showToast('success', `${type} ${t('toast.txSuccess')}`) + + // 刷新数据 + refetchBorrowBalance() + refetchUserPrincipal() + refetchLendingBalance() + refetchUsdcAllowance() + refetchUsdcBalance() + refetchCollateralData() + refetchSystemInfo() + + pendingTxRef.current = null + + // 只在非授权操作成功后清空表单 + if (type !== 'approve') { + setDepositAmount('') + setBorrowAmount('') + setRepayAmount('') + setWithdrawAmount('') + setSupplyUSDCAmount('') + setWithdrawUSDCAmount('') + } + } + }, [isConfirmed]) + + // 处理交易失败 + useEffect(() => { + if (isTxError && pendingTxRef.current && pendingTxRef.current.id) { + const { id } = pendingTxRef.current + const errorMsg = txError?.message || 'Transaction failed' + setLastError(`Transaction Error: ${errorMsg}`) + updateTransaction(id, { status: 'failed' }) + showToast('error', t('toast.txFailed')) + pendingTxRef.current = null + } + }, [isTxError, txError]) + + // 查询清算历史(仅在显示监控面板时查询) + useEffect(() => { + if (!publicClient || !showLiquidationMonitor) return + + const fetchLiquidationHistory = async () => { + setIsLoadingHistory(true) + try { + const currentBlock = await publicClient.getBlockNumber() + const fromBlock = currentBlock - 50000n // 查询最近 50000 个区块(约14小时,Arbitrum) + + // 查询 AbsorbDebt 事件(Compound V3 清算事件) + // event AbsorbDebt(address indexed absorber, address indexed borrower, uint256 basePaidOut, uint256 usdValue) + const logs = await publicClient.getLogs({ + address: CONTRACTS.LENDING_PROXY as `0x${string}`, + event: { + type: 'event', + name: 'AbsorbDebt', + inputs: [ + { type: 'address', indexed: true, name: 'absorber' }, + { type: 'address', indexed: true, name: 'borrower' }, + { type: 'uint256', indexed: false, name: 'basePaidOut' }, + { type: 'uint256', indexed: false, name: 'usdValue' } + ] + }, + fromBlock, + toBlock: currentBlock + }) + + // 格式化日志数据 + const formattedHistory = logs.map((log: any) => ({ + absorber: log.args.absorber, + borrower: log.args.borrower, + basePaidOut: log.args.basePaidOut, + usdValue: log.args.usdValue, + blockNumber: log.blockNumber, + transactionHash: log.transactionHash, + timestamp: Date.now() // 实际应该从区块获取 + })) + + setLiquidationHistory(formattedHistory) + } catch (error) { + console.error('Failed to fetch liquidation history:', error) + setLiquidationHistory([]) + } finally { + setIsLoadingHistory(false) + } + } + + fetchLiquidationHistory() + }, [publicClient, showLiquidationMonitor]) + + // 格式化数据 + const formatBalance = (balance: bigint | undefined, decimals: number = TOKEN_DECIMALS.USDC) => { + if (!balance) return '0.00' + const formatted = formatUnits(balance, decimals) + return parseFloat(formatted).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 6 + }) + } + + const formatPercentage = (value: bigint | undefined, divisor: number = 10000) => { + if (!value) return '0.00' + return (Number(value) / divisor).toFixed(2) + } + + // 获取抵押品数据 + const getCollateralInfo = (assetIndex: number) => { + if (!collateralData) return { balance: 0n, allowance: 0n, deposited: 0n } + + const baseIndex = assetIndex * 3 + return { + balance: (collateralData[baseIndex]?.result as bigint) || 0n, + allowance: (collateralData[baseIndex + 1]?.result as bigint) || 0n, + deposited: (collateralData[baseIndex + 2]?.result as bigint) || 0n, + } + } + + // 处理授权 + const handleApprove = async (tokenAddress: string, amount: bigint, isUSDC: boolean = false) => { + if (!address) return + + // 保存交易信息,等待 hash 返回后再添加到历史记录 + pendingTxRef.current = { + type: 'approve', + token: tokenAddress + } + + try { + await writeContract({ + address: tokenAddress as `0x${string}`, + abi: isUSDC ? USDC_ABI : YT_TOKEN_ABI, + functionName: 'approve', + args: [CONTRACTS.LENDING_PROXY, amount], + gas: GAS_CONFIG.SIMPLE, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Approve error:', error) + showToast('error', '授权失败') + pendingTxRef.current = null + } + } + + // 存入抵押品 + const handleDeposit = async () => { + if (!address || !depositAmount) return + + // ✅ 动态获取抵押品精度(从合约配置中读取) + const selectedAsset = COLLATERAL_ASSETS.find( + a => a.address.toLowerCase() === selectedCollateral.toLowerCase() + ) + const collateralDecimals = selectedAsset?.decimals || 18 // fallback 到 18 + if (!selectedAsset) { + console.warn('⚠️ 未找到抵押品配置,使用默认精度 18') + } + + const amount = parseUnits(depositAmount, collateralDecimals) // ✅ 使用动态精度 + // 保存交易信息,等待 hash 返回后再添加到历史记录 + pendingTxRef.current = { + type: 'transfer', + amount: depositAmount, + token: selectedCollateral + } + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'supplyCollateral', + args: [selectedCollateral as `0x${string}`, amount], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Deposit error:', error) + const errorMsg = error instanceof Error ? error.message : String(error) + setLastError(`Deposit Error: ${errorMsg}`) + showToast('error', '存入失败: ' + errorMsg) + pendingTxRef.current = null + } + } + + // 存入USDC赚取利息 (流动性提供者 - 文档流程1) + const handleSupplyUSDC = async () => { + if (!address || !supplyUSDCAmount) return + + const amount = parseUnits(supplyUSDCAmount, TOKEN_DECIMALS.USDC) + pendingTxRef.current = { + type: 'transfer', + amount: supplyUSDCAmount, + token: CONTRACTS.USDC + } + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'supply', + args: [amount], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Supply USDC error:', error) + showToast('error', '存入USDC失败') + pendingTxRef.current = null + } + } + + // 提取USDC存款 (流动性提供者 - 文档流程2) + const handleWithdrawUSDC = async () => { + if (!address || !withdrawUSDCAmount) return + + const amount = parseUnits(withdrawUSDCAmount, TOKEN_DECIMALS.USDC) + pendingTxRef.current = { + type: 'transfer', + amount: withdrawUSDCAmount, + token: CONTRACTS.USDC + } + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'withdraw', + args: [amount], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Withdraw USDC error:', error) + showToast('error', '提取USDC失败') + pendingTxRef.current = null + } + } + + // 借出USDC (通过 withdraw 实现 - Compound V3 模式) + const handleBorrow = async () => { + if (!address || !borrowAmount) return + + const amount = parseUnits(borrowAmount, TOKEN_DECIMALS.USDC) + // 保存交易信息,等待 hash 返回后再添加到历史记录 + pendingTxRef.current = { + type: 'transfer', + amount: borrowAmount, + token: CONTRACTS.USDC + } + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'borrow', + args: [amount], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Borrow error:', error) + showToast('error', '借款失败') + pendingTxRef.current = null + } + } + + // 归还USDC (通过 supply 实现 - Compound V3 模式) + const handleRepay = async () => { + if (!address || !repayAmount) return + + const amount = parseUnits(repayAmount, TOKEN_DECIMALS.USDC) + // 保存交易信息,等待 hash 返回后再添加到历史记录 + pendingTxRef.current = { + type: 'transfer', + amount: repayAmount, + token: CONTRACTS.USDC + } + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'supply', + args: [amount], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Repay error:', error) + showToast('error', '归还失败') + pendingTxRef.current = null + } + } + + // 提取抵押品 + const handleWithdraw = async () => { + if (!address || !withdrawAmount) return + + // ✅ 动态获取抵押品精度(从合约配置中读取) + const selectedAsset = COLLATERAL_ASSETS.find( + a => a.address.toLowerCase() === selectedCollateral.toLowerCase() + ) + const collateralDecimals = selectedAsset?.decimals || 18 + if (!selectedAsset) { + console.warn('⚠️ 未找到抵押品配置,使用默认精度 18') + } + const amount = parseUnits(withdrawAmount, collateralDecimals) + // 保存交易信息,等待 hash 返回后再添加到历史记录 + pendingTxRef.current = { + type: 'transfer', + amount: withdrawAmount, + token: selectedCollateral + } + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'withdrawCollateral', + args: [selectedCollateral as `0x${string}`, amount], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Withdraw error:', error) + showToast('error', '提取失败') + pendingTxRef.current = null + } + } + + // === 高级功能:清算和储备管理 (隐藏功能) === + + // 清算不健康借款人 (文档流程5) + // 单个清算 (absorb) + const handleAbsorb = async () => { + if (!address || !liquidateBorrower) return + + // 检查是否可清算 + if (!isLiquidatableData) { + showToast('该地址不可清算', 'warning') + return + } + + // 检查清算阈值 + if (!isAboveThreshold) { + const debtInUSD = Number(borrowerDebtValue) / Number(10n ** 30n) + showToast(`债务金额 $${debtInUSD.toFixed(2)} 低于清算阈值 $10`, 'warning') + return + } + + pendingTxRef.current = { + type: 'transfer', + token: CONTRACTS.LENDING_PROXY + } + + recordTx('absorb', liquidateBorrower, 'Absorb') + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'absorb', + args: [liquidateBorrower as `0x${string}`], // 只传 borrower,msg.sender 自动作为 absorber + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Absorb error:', error) + showToast('清算失败', 'error') + pendingTxRef.current = null + } + } + + // 批量清算 (absorbMultiple) + const handleAbsorbMultiple = async () => { + if (!address || !batchLiquidateAddresses) return + + // 解析地址列表(逗号或空格分隔) + const addresses = batchLiquidateAddresses + .split(/[,\s]+/) + .map(addr => addr.trim()) + .filter(addr => addr.match(/^0x[a-fA-F0-9]{40}$/)) + + if (addresses.length === 0) { + showToast('请输入有效的地址列表', 'warning') + return + } + + pendingTxRef.current = { + type: 'transfer', + token: CONTRACTS.LENDING_PROXY + } + + recordTx('absorbMultiple', `${addresses.length} 个地址`, 'AbsorbMultiple') + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'absorbMultiple', + args: [address, addresses as `0x${string}`[]], // absorber, accounts[] + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('AbsorbMultiple error:', error) + showToast('批量清算失败', 'error') + pendingTxRef.current = null + } + } + + // 购买清算抵押品 (文档流程6 - 改进版本) + const handleBuyCollateral = async () => { + if (!address || !buyCollateralAsset || !buyCollateralBaseAmount) return + + // 1. 检查储备金状态(文档要求:reserves < targetReserves) + if (currentReserves && targetReservesData) { + if (currentReserves >= targetReservesData) { + showToast('error', '储备金充足,当前无法购买抵押品') + return + } + } + + // 2. 检查抵押品储备(文档要求:collateralReserve > 0) + if (!collateralReserveData || collateralReserveData === 0n) { + showToast('error', '该抵押品储备为空,无法购买') + return + } + + const baseAmount = parseUnits(buyCollateralBaseAmount, TOKEN_DECIMALS.USDC) + + // 3. 改进滑点计算(文档逻辑:考虑储备限制) + const expectedAmount = quoteCollateralData as bigint || 0n + const availableReserve = collateralReserveData as bigint + const actualAmount = expectedAmount < availableReserve ? expectedAmount : availableReserve + + // 4. 应用 2% 滑点保护(默认值) + const slippageTolerance = 0.02 // 2% + const slippageMultiplier = BigInt(Math.floor((1 - slippageTolerance) * 1e18)) + const minAmount = (actualAmount * slippageMultiplier) / BigInt(1e18) + + // 5. 如果储备不足,提示用户 + if (actualAmount < expectedAmount) { + const actualBaseAmount = (baseAmount * actualAmount) / expectedAmount + console.log(`储备不足调整: 原计划支付 ${formatUnits(baseAmount, TOKEN_DECIMALS.USDC)} USDC, 实际约支付 ${formatUnits(actualBaseAmount, TOKEN_DECIMALS.USDC)} USDC`) + } + + pendingTxRef.current = { + type: 'transfer', + amount: buyCollateralBaseAmount, + token: CONTRACTS.USDC + } + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'buyCollateral', + args: [ + buyCollateralAsset as `0x${string}`, + minAmount, + baseAmount, + address + ], + gas: GAS_CONFIG.COMPLEX, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Buy collateral error:', error) + showToast('error', '购买抵押品失败') + pendingTxRef.current = null + } + } + + // 提取储备金 (文档流程9 - 仅管理员) + const handleWithdrawReserves = async () => { + if (!address || !withdrawReservesTo || !withdrawReservesAmount) return + + const amount = parseUnits(withdrawReservesAmount, TOKEN_DECIMALS.USDC) + pendingTxRef.current = { + type: 'transfer', + amount: withdrawReservesAmount, + token: CONTRACTS.USDC + } + + try { + await writeContract({ + address: CONTRACTS.LENDING_PROXY, + abi: LENDING_ABI, + functionName: 'withdrawReserves', + args: [withdrawReservesTo as `0x${string}`, amount], + gas: GAS_CONFIG.STANDARD, + maxFeePerGas: GAS_CONFIG.MAX_FEE_PER_GAS, + maxPriorityFeePerGas: GAS_CONFIG.MAX_PRIORITY_FEE_PER_GAS, + }) + } catch (error) { + console.error('Withdraw reserves error:', error) + showToast('error', '提取储备金失败') + pendingTxRef.current = null + } + } + + if (!isConnected) { + return ( +
+
+

{t('common.connectFirst')}

+
+
+ ) + } + + // 用户数据(使用正确的函数) + const totalBorrow = borrowBalance || 0n + + // 计算总抵押价值 + // collateralData 结构:[balance, allowance, collateral] for each asset + // pricesData 结构:[price] for each asset + let totalCollateralValue = 0n + let totalCollateralInUSD = 0n + let weightedCollateralFactor = 0n + + COLLATERAL_ASSETS_WITH_NAMES.forEach((asset, index) => { + const collateralIndex = index * 3 + 2 // 每个资产占3个位置,第3个是 getCollateral 的结果 + const collateralBalance = collateralData?.[collateralIndex]?.result as bigint || 0n + const price = pricesData?.[index]?.result as bigint || 0n + const config = lendingConfig?.assetConfigs?.find( + (cfg: any) => cfg.asset.toLowerCase() === asset.address.toLowerCase() + ) + + if (collateralBalance > 0n && price > 0n) { + // Compound V3 价格格式:1e30 规模 + // 抵押品:18位精度,USDC:6位精度 + // 价值(USDC 6位精度)= (balance * price) / (1e18 * 1e30 / 1e6) = (balance * price) / 1e42 + const valueInUSD = (collateralBalance * price) / BigInt(10 ** 42) + totalCollateralInUSD += valueInUSD + + // 计算加权抵押率(用于计算可借额度) + if (config?.borrowCollateralFactor) { + const collateralFactor = BigInt(config.borrowCollateralFactor) + weightedCollateralFactor += valueInUSD * collateralFactor + } + } + }) + + totalCollateralValue = totalCollateralInUSD + + // 计算可借额度 + // 可借额度 = 总抵押价值 × 平均抵押率 - 已借款 + let availableToBorrow = 0n + if (totalCollateralInUSD > 0n && weightedCollateralFactor > 0n) { + // borrowCollateralFactor 是 1e18 精度 + const maxBorrow = (weightedCollateralFactor / BigInt(10 ** 18)) + availableToBorrow = maxBorrow > totalBorrow ? maxBorrow - totalBorrow : 0n + } + + // 计算健康因子 + // 健康因子 = (总抵押价值 × 清算阈值) / 总借款 + // 为了显示方便,乘以 10000(表示百分比,100% = 10000) + let healthFactor = 0n + if (totalBorrow > 0n && totalCollateralInUSD > 0n) { + let weightedLiquidationThreshold = 0n + COLLATERAL_ASSETS_WITH_NAMES.forEach((asset, index) => { + const collateralIndex = index * 3 + 2 + const collateralBalance = collateralData?.[collateralIndex]?.result as bigint || 0n + const price = pricesData?.[index]?.result as bigint || 0n + const config = lendingConfig?.assetConfigs?.find( + (cfg: any) => cfg.asset.toLowerCase() === asset.address.toLowerCase() + ) + + if (collateralBalance > 0n && price > 0n && config?.liquidateCollateralFactor) { + const valueInUSD = (collateralBalance * price) / BigInt(10 ** 42) + const liquidationFactor = BigInt(config.liquidateCollateralFactor) + weightedLiquidationThreshold += valueInUSD * liquidationFactor + } + }) + + if (weightedLiquidationThreshold > 0n) { + // healthFactor 以 10000 为基数(100% = 10000) + healthFactor = (weightedLiquidationThreshold * BigInt(10000)) / (totalBorrow * BigInt(10 ** 18)) + } + } else if (totalBorrow === 0n && totalCollateralInUSD > 0n) { + // 如果没有借款但有抵押品,健康因子设为最大值 + healthFactor = BigInt(10 ** 10) // 非常大的值表示安全 + } + + const totalCollateral = totalCollateralValue + + const totalLiquidity = systemInfoData?.[0]?.result as bigint || 0n + const totalBorrows = systemInfoData?.[1]?.result as bigint || 0n + const utilizationRate = systemInfoData?.[2]?.result as bigint || 0n + const borrowRatePerSecond = systemInfoData?.[3]?.result as bigint || 0n + const reserves = systemInfoData?.[4]?.result as bigint || 0n + + // getBorrowRate 返回的是年化利率,精度 1e18 + // getUtilization 返回的是使用率,精度 1e18 + // 在显示时直接转换为百分比: value / 1e18 × 100 + + return ( +
+
+
+

{t('lending.title')}

+
+ {t('lending.contract')}: + {CONTRACTS.LENDING_PROXY} +
+
+
+ + {/* 用户账户数据 */} +
+

+ {t('lending.yourAccount')} +

+
+
+ + {t('lending.totalCollateral')} + + + ${formatBalance(totalCollateral)} + +
+
+ + {t('lending.totalBorrow')} + + + ${formatBalance(totalBorrow)} + +
+
+ + {t('lending.availableToBorrow')} + + + ${formatBalance(availableToBorrow)} + +
+
+ + {t('lending.healthFactor')} + + + {formatPercentage(healthFactor, 10000)} + +
+
+
+ + {/* 配置状态概览 */} +
+

+ 系统配置状态 +

+
+
+ 系统状态: + + {isPaused ? '已暂停' : '运行中'} + +
+ + {ALL_WHITELISTED_TOKENS.map((token) => { + const config = assetConfigsMap.get(token.address.toLowerCase()) + return ( +
+ {token.name}: + + {config ? '✓ 激活' : '✗ 未激活'} + + {config && ( + + ({Number(config.borrowCollateralFactor) / 1e16}%) + + )} +
+ ) + })} +
+
+ + {/* 系统信息 */} +
+

{t('lending.systemInfo')}

+
+
+ {t('lending.totalLiquidity')} + ${formatBalance(totalLiquidity)} +
+
+ {t('lending.totalBorrows')} + ${formatBalance(totalBorrows)} +
+
+ 储备金量 + + {reserves >= 0n ? `$${formatBalance(reserves)}` : `-$${formatBalance(-reserves)}`} + +
+
+ {t('lending.utilizationRate')} + {(Number(utilizationRate) / 1e18 * 100).toFixed(2)}% +
+
+ {t('lending.borrowAPY')} + {(Number(borrowRatePerSecond) / 1e18 * 100).toFixed(2)}% +
+
+
+ + {/* 操作标签页 */} +
+
+ {/* 流动性提供者功能 */} + + + + {/* 借款人功能 */} + + + + +
+ +
+ {/* 存入USDC赚取利息 (流动性提供者) */} + {activeTab === 'supplyUSDC' && ( +
+
+
+ 流动性提供者 - 存入USDC赚取利息 +
+
+ 存入USDC到借贷池,自动赚取利息收益(来自借款人支付的利息) +
+
+ +
+

您的USDC余额: {usdcBalance ? formatBalance(usdcBalance) : '0.00'} USDC

+

当前供应APY: {(Number(borrowRatePerSecond) / 1e18 * Number(utilizationRate) / 1e18 * 100).toFixed(2)}%

+
+ +
+ + setSupplyUSDCAmount(e.target.value)} + placeholder="0.00" + className="amount-input" + /> +
+ + {(() => { + const amount = parseUnits(supplyUSDCAmount || '0', TOKEN_DECIMALS.USDC) + const needsApproval = (usdcAllowance || 0n) < amount + + if (needsApproval) { + return ( + + ) + } + + return ( + + ) + })()} +
+ )} + + {/* 提取USDC存款 (流动性提供者) */} + {activeTab === 'withdrawUSDC' && ( +
+
+
+ 流动性提供者 - 提取USDC存款 +
+
+ 提取之前存入的USDC(包含累积的利息收益) +
+
+ + {(() => { + // 计算用户的USDC存款余额(含利息) + // principal > 0 表示有存款,实际余额 = principal × supplyIndex / 1e18 + let supplyBalance = 0n + if (userPrincipal && supplyIndex) { + const principal = BigInt(userPrincipal) + if (principal > 0n) { + // 用户有存款,计算含利息的余额 + supplyBalance = (principal * supplyIndex) / BigInt(10 ** 18) + } + } + + return ( + <> +
+
+
+ 您的USDC存款余额(含利息): +
+
+ {formatBalance(supplyBalance)} USDC +
+
+

+ 系统总流动性: ${formatBalance(totalLiquidity)} +

+

+ 注意:提取金额不能超过您的存款余额 +

+
+ + ) + })()} + +
+ + setWithdrawUSDCAmount(e.target.value)} + placeholder="0.00" + className="amount-input" + /> +
+ + +
+ )} + + {/* 存入抵押品 */} + {activeTab === 'deposit' && ( +
+
+ + +
+ +
+ + setDepositAmount(e.target.value)} + placeholder="0.00" + className="amount-input" + /> +
+ + {(() => { + const index = COLLATERAL_ASSETS_WITH_NAMES.findIndex(a => a.address === selectedCollateral) + const info = getCollateralInfo(index) + // ✅ 动态获取抵押品精度(从合约配置中读取) + const selectedAsset = COLLATERAL_ASSETS.find( + a => a.address.toLowerCase() === selectedCollateral.toLowerCase() + ) + const collateralDecimals = selectedAsset?.decimals || 18 + const amount = parseUnits(depositAmount || '0', collateralDecimals) + const needsApproval = info.allowance < amount + + return ( + <> + {needsApproval ? ( + + ) : ( + + )} + + {/* Debug Info */} +
+
Debug Info:
+
Selected Collateral: {selectedCollateral}
+
Wallet Balance: {formatBalance(info.balance, 18)}
+
Approved Amount: {formatBalance(info.allowance, 18)}
+
Input Amount: {depositAmount || '0'}
+
Amount (wei): {amount.toString()}
+
Needs Approval: {needsApproval ? 'Yes' : 'No'}
+
Can Deposit: {!needsApproval && depositAmount ? 'Yes' : 'No'}
+ {lastError && ( +
+ Error: {lastError} +
+ )} +
+ + ) + })()} +
+ )} + + {/* 借出USDC */} + {activeTab === 'borrow' && ( +
+
+

{t('lending.availableToBorrow')}: ${formatBalance(availableToBorrow)}

+
+ +
+ + setBorrowAmount(e.target.value)} + placeholder="0.00" + className="amount-input" + /> +
+ + + + {/* Debug Info */} +
+
Debug Info:
+
Available to Borrow: ${formatBalance(availableToBorrow)}
+
Input Amount: ${borrowAmount || '0'}
+
Total Liquidity: ${formatBalance(totalLiquidity)}
+
Current Health Factor: {formatBalance(healthFactor, 18)}
+
Can Borrow: {borrowAmount && parseFloat(borrowAmount) > 0 && parseUnits(borrowAmount, TOKEN_DECIMALS.USDC) <= (availableToBorrow || 0n) ? 'Yes' : 'No'}
+ {lastError && ( +
+ Error: {lastError} +
+ )} +
+
+ )} + + {/* 归还USDC */} + {activeTab === 'repay' && ( +
+
+

{t('lending.yourBorrow')}: ${formatBalance(totalBorrow)}

+

{t('lending.usdcBalance')}: {formatBalance(usdcBalance)}

+
+ +
+ + setRepayAmount(e.target.value)} + placeholder="0.00" + className="amount-input" + /> +
+ + {(() => { + const amount = parseUnits(repayAmount || '0', TOKEN_DECIMALS.USDC) + const needsApproval = (usdcAllowance || 0n) < amount + + return ( + <> + {needsApproval ? ( + + ) : ( + + )} + + {/* Debug Info */} +
+
Debug Info:
+
Current Borrow Balance: ${formatBalance(totalBorrow)}
+
USDC Wallet Balance: {formatBalance(usdcBalance)}
+
USDC Approved Amount: {formatBalance(usdcAllowance)}
+
Input Amount: ${repayAmount || '0'}
+
Amount (wei): {amount.toString()}
+
Needs Approval: {needsApproval ? 'Yes' : 'No'}
+
Can Repay: {!needsApproval && repayAmount ? 'Yes' : 'No'}
+ {lastError && ( +
+ Error: {lastError} +
+ )} +
+ + ) + })()} +
+ )} + + {/* 提取抵押品 */} + {activeTab === 'withdraw' && ( +
+
+ + +
+ +
+ + setWithdrawAmount(e.target.value)} + placeholder="0.00" + className="amount-input" + /> +
+ + + + {/* Debug Info */} + {(() => { + const index = COLLATERAL_ASSETS_WITH_NAMES.findIndex(a => a.address === selectedCollateral) + const info = getCollateralInfo(index) + // ✅ 动态获取抵押品精度(从合约配置中读取) + const selectedAsset = COLLATERAL_ASSETS.find( + a => a.address.toLowerCase() === selectedCollateral.toLowerCase() + ) + const collateralDecimals = selectedAsset?.decimals || 18 + const amount = parseUnits(withdrawAmount || '0', collateralDecimals) + const canWithdraw = amount > 0n && amount <= info.deposited + + return ( +
+
Debug Info:
+
Selected Collateral: {selectedCollateral}
+
Deposited Amount: {formatBalance(info.deposited, 18)}
+
Input Amount: {withdrawAmount || '0'}
+
Amount (wei): {amount.toString()}
+
Current Borrow Balance: ${formatBalance(totalBorrow)}
+
Current Health Factor: {formatBalance(healthFactor, 18)}
+
Can Withdraw: {canWithdraw ? 'Yes' : 'No'}
+ {lastError && ( +
+ Error: {lastError} +
+ )} +
+ ) + })()} +
+ )} +
+
+ + {/* === 高级功能区域 (隐藏) === */} + {SHOW_ADVANCED_FEATURES && ( + <> + {/* 清算功能区 */} +
+

+ 清算功能 (Liquidation) +

+ + {/* 单个清算 - 已隐藏 */} + {false && ( +
+

单个清算 (Absorb)

+
+ + setLiquidateBorrower(e.target.value)} + placeholder="0x..." + className="amount-input" + /> +
+ + {/* 清算状态信息 */} + {liquidateBorrower && ( +
+
+ 清算状态:{' '} + + {isLiquidatableData === undefined ? '查询中...' : (isLiquidatableData ? '✓ 可清算' : '✗ 不可清算')} + +
+ {borrowerDebtBalance !== undefined && ( +
+ 债务余额: {formatBalance(borrowerDebtBalance)} USDC +
+ )} + {borrowerDebtValue > 0n && ( +
+ 债务价值: ${(Number(borrowerDebtValue) / Number(10n ** 30n)).toFixed(2)} +
+ )} +
+ 清算阈值: $10.00 +
+ {borrowerDebtValue > 0n && ( +
+ {isAboveThreshold ? '✓ 超过清算阈值' : '⚠ 低于清算阈值(不建议清算)'} +
+ )} +
+ )} + + +
+ )} + + {/* 批量清算 - 已隐藏 */} + {false && ( +
+

批量清算 (AbsorbMultiple)

+
+ +