/** * 批量用户卖出模拟脚本 * * 功能: * 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)