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