Files
Heyue_test/frontend/scripts/batch-sell-simulation.ts
Sofio 5cb64f881e feat: 添加多链支持和 Lending 借贷系统
- 新增 ARB Sepolia + BNB Testnet 多链支持
- 添加 LendingPanel 借贷系统组件
- 添加 LendingAdminPanel 管理面板
- 添加 USDCPanel USDC 操作组件
- 添加 HoldersPanel 持有人信息组件
- 添加 AutoTestPanel 自动化测试组件
- 重构 LP 组件为模块化结构 (LP/)
- 添加多个调试和测试脚本
- 修复 USDC 精度动态配置
- 优化合约配置支持多链切换

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 23:32:29 +08:00

344 lines
9.9 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 批量用户卖出模拟脚本
*
* 功能:
* 1. 读取测试账户的 YT 余额
* 2. 批量执行 withdrawYT 卖出交易(进入排队)
* 3. 可选:管理员批量处理队列
*
* 使用方法:
* npx tsx scripts/batch-sell-simulation.ts
*/
import {
createPublicClient,
createWalletClient,
http,
parseUnits,
formatUnits,
type Address,
type Hex
} from 'viem'
import { arbitrumSepolia } from 'viem/chains'
import { privateKeyToAccount, mnemonicToAccount } from 'viem/accounts'
// ============ 配置 ============
const CONTRACTS = {
WUSD: '0x6d2bf81a631dFE19B2f348aE92cF6Ef41ca2DF98' as Address,
FACTORY: '0x982716f32F10BCB5B5944c1473a8992354bF632b' as Address,
VAULTS: {
YT_A: '0x0cA35994F033685E7a57ef9bc5d00dd3cf927330' as Address,
YT_B: '0x333805C9EE75f59Aa2Cc79DfDe2499F920c7b408' as Address,
YT_C: '0x6DF0ED6f0345F601A206974973dE9fC970598587' as Address,
}
}
const CONFIG = {
// 主账户私钥 (Owner用于处理队列)
MAIN_PRIVATE_KEY: '0xa082a7037105ebd606bee80906687e400d89899bbb6ba0273a61528c2f5fab89' as Hex,
// HD 钱包助记词 (与买入脚本相同)
TEST_MNEMONIC: 'test test test test test test test test test test test junk',
// 模拟用户数量
USER_COUNT: 10,
// 卖出比例 (0.5 = 卖出 50% 的 YT)
SELL_RATIO: 0.5,
// 目标金库
TARGET_VAULT: CONTRACTS.VAULTS.YT_A,
// 交易间隔 (毫秒)
TX_INTERVAL: 2000,
// 是否自动处理队列
AUTO_PROCESS_QUEUE: true,
// 批量处理大小
BATCH_PROCESS_SIZE: 10,
// RPC URL
RPC_URL: 'https://sepolia-rollup.arbitrum.io/rpc',
}
// ============ ABI ============
const VAULT_ABI = [
{
inputs: [{ name: '_ytAmount', type: 'uint256' }],
name: 'withdrawYT',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ name: '_ytAmount', type: 'uint256' }],
name: 'previewSell',
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: 'account', type: 'address' }],
name: 'balanceOf',
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'symbol',
outputs: [{ type: 'string' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'canRedeemNow',
outputs: [{ type: 'bool' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'getQueueProgress',
outputs: [
{ name: 'currentIndex', type: 'uint256' },
{ name: 'totalRequests', type: 'uint256' },
{ name: 'pendingCount', type: 'uint256' },
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: '_batchSize', type: 'uint256' }],
name: 'processBatchWithdrawals',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
] as const
// ============ 工具函数 ============
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
// ============ 主逻辑 ============
async function main() {
console.log('🚀 批量用户卖出模拟开始\n')
const publicClient = createPublicClient({
chain: arbitrumSepolia,
transport: http(CONFIG.RPC_URL),
})
// 主账户 (Owner)
const mainAccount = privateKeyToAccount(CONFIG.MAIN_PRIVATE_KEY)
const mainWalletClient = createWalletClient({
account: mainAccount,
chain: arbitrumSepolia,
transport: http(CONFIG.RPC_URL),
})
console.log(`📍 Owner 账户: ${mainAccount.address}`)
// 获取金库信息
const vaultSymbol = await publicClient.readContract({
address: CONFIG.TARGET_VAULT,
abi: VAULT_ABI,
functionName: 'symbol',
})
console.log(`🏦 目标金库: ${vaultSymbol} (${CONFIG.TARGET_VAULT})`)
// 检查赎回状态
const canRedeem = await publicClient.readContract({
address: CONFIG.TARGET_VAULT,
abi: VAULT_ABI,
functionName: 'canRedeemNow',
})
console.log(`📅 赎回状态: ${canRedeem ? '✅ 可赎回' : '❌ 锁定中'}`)
if (!canRedeem) {
console.error('\n⚠ 当前不在赎回期,无法执行卖出操作')
console.log('请先通过管理员设置赎回时间: setVaultNextRedemptionTime')
return
}
// 获取队列状态
const queueProgress = await publicClient.readContract({
address: CONFIG.TARGET_VAULT,
abi: VAULT_ABI,
functionName: 'getQueueProgress',
})
console.log(`📋 队列状态: 已处理 ${queueProgress[0]}, 总请求 ${queueProgress[1]}, 待处理 ${queueProgress[2]}`)
// 获取测试账户
console.log(`\n👥 检查 ${CONFIG.USER_COUNT} 个测试账户的 YT 余额...\n`)
const testAccounts = []
for (let i = 0; i < CONFIG.USER_COUNT; i++) {
const account = mnemonicToAccount(CONFIG.TEST_MNEMONIC, { addressIndex: i })
const ytBalance = await publicClient.readContract({
address: CONFIG.TARGET_VAULT,
abi: VAULT_ABI,
functionName: 'balanceOf',
args: [account.address],
})
if (ytBalance > 0n) {
const sellAmount = (ytBalance * BigInt(Math.floor(CONFIG.SELL_RATIO * 100))) / 100n
testAccounts.push({ account, ytBalance, sellAmount })
console.log(` 账户 ${i + 1}: ${account.address} | YT余额: ${formatUnits(ytBalance, 18)} | 计划卖出: ${formatUnits(sellAmount, 18)}`)
} else {
console.log(` 账户 ${i + 1}: ${account.address} | YT余额: 0 (跳过)`)
}
}
if (testAccounts.length === 0) {
console.log('\n⚠ 没有账户有 YT 余额,请先运行 npm run sim:buy')
return
}
// 执行卖出
console.log('\n💸 步骤 1: 执行批量卖出 (进入排队)...\n')
const results: { address: string; sellAmount: string; expectedWusd: string; status: string; hash?: string }[] = []
for (let i = 0; i < testAccounts.length; i++) {
const { account, sellAmount } = testAccounts[i]
console.log(`\n[${i + 1}/${testAccounts.length}] 账户: ${account.address}`)
console.log(` 卖出金额: ${formatUnits(sellAmount, 18)} ${vaultSymbol}`)
const walletClient = createWalletClient({
account,
chain: arbitrumSepolia,
transport: http(CONFIG.RPC_URL),
})
try {
// 预览卖出
const previewWusd = await publicClient.readContract({
address: CONFIG.TARGET_VAULT,
abi: VAULT_ABI,
functionName: 'previewSell',
args: [sellAmount],
})
console.log(` 预计获得: ${formatUnits(previewWusd, 18)} WUSD`)
// 执行卖出 (进入队列)
console.log(` → 提交卖出请求...`)
const sellHash = await walletClient.writeContract({
address: CONFIG.TARGET_VAULT,
abi: VAULT_ABI,
functionName: 'withdrawYT',
args: [sellAmount],
})
await publicClient.waitForTransactionReceipt({ hash: sellHash })
console.log(` ✓ 请求已进入队列`)
console.log(` 交易哈希: ${sellHash}`)
results.push({
address: account.address,
sellAmount: formatUnits(sellAmount, 18),
expectedWusd: formatUnits(previewWusd, 18),
status: 'queued',
hash: sellHash,
})
} catch (error: any) {
console.error(` ✗ 卖出失败: ${error.message}`)
results.push({
address: account.address,
sellAmount: formatUnits(sellAmount, 18),
expectedWusd: '0',
status: 'failed',
})
}
if (i < testAccounts.length - 1) {
console.log(` ⏳ 等待 ${CONFIG.TX_INTERVAL / 1000} 秒...`)
await sleep(CONFIG.TX_INTERVAL)
}
}
// 获取更新后的队列状态
const newQueueProgress = await publicClient.readContract({
address: CONFIG.TARGET_VAULT,
abi: VAULT_ABI,
functionName: 'getQueueProgress',
})
console.log(`\n📋 更新后队列状态: 已处理 ${newQueueProgress[0]}, 总请求 ${newQueueProgress[1]}, 待处理 ${newQueueProgress[2]}`)
// 处理队列 (如果启用)
if (CONFIG.AUTO_PROCESS_QUEUE && newQueueProgress[2] > 0n) {
console.log('\n⚙ 步骤 2: 管理员批量处理队列...\n')
const pendingCount = Number(newQueueProgress[2])
const batches = Math.ceil(pendingCount / CONFIG.BATCH_PROCESS_SIZE)
for (let batch = 0; batch < batches; batch++) {
const batchSize = Math.min(CONFIG.BATCH_PROCESS_SIZE, pendingCount - batch * CONFIG.BATCH_PROCESS_SIZE)
console.log(` 处理批次 ${batch + 1}/${batches} (${batchSize} 笔)...`)
try {
const processHash = await mainWalletClient.writeContract({
address: CONFIG.TARGET_VAULT,
abi: VAULT_ABI,
functionName: 'processBatchWithdrawals',
args: [BigInt(batchSize)],
})
await publicClient.waitForTransactionReceipt({ hash: processHash })
console.log(` ✓ 批次处理完成: ${processHash}`)
} catch (error: any) {
console.error(` ✗ 处理失败: ${error.message}`)
break
}
await sleep(2000)
}
// 最终队列状态
const finalQueue = await publicClient.readContract({
address: CONFIG.TARGET_VAULT,
abi: VAULT_ABI,
functionName: 'getQueueProgress',
})
console.log(`\n📋 最终队列状态: 已处理 ${finalQueue[0]}, 总请求 ${finalQueue[1]}, 待处理 ${finalQueue[2]}`)
}
// 汇总
console.log('\n' + '='.repeat(60))
console.log('📊 模拟结果汇总')
console.log('='.repeat(60))
const queuedCount = results.filter(r => r.status === 'queued').length
const failCount = results.filter(r => r.status === 'failed').length
const totalSold = results
.filter(r => r.status === 'queued')
.reduce((sum, r) => sum + parseFloat(r.sellAmount), 0)
console.log(`\n成功排队: ${queuedCount}`)
console.log(`失败: ${failCount}`)
console.log(`总卖出 ${vaultSymbol}: ${totalSold.toFixed(2)}`)
console.log('\n详细记录:')
console.table(results.map(r => ({
地址: r.address.slice(0, 10) + '...',
卖出YT: r.sellAmount,
预期WUSD: r.expectedWusd,
状态: r.status,
})))
console.log('\n✅ 模拟完成!')
}
main().catch(console.error)