- 新增 ARB Sepolia + BNB Testnet 多链支持 - 添加 LendingPanel 借贷系统组件 - 添加 LendingAdminPanel 管理面板 - 添加 USDCPanel USDC 操作组件 - 添加 HoldersPanel 持有人信息组件 - 添加 AutoTestPanel 自动化测试组件 - 重构 LP 组件为模块化结构 (LP/) - 添加多个调试和测试脚本 - 修复 USDC 精度动态配置 - 优化合约配置支持多链切换 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
/**
|
|
* 批量用户买入模拟脚本
|
|
*
|
|
* 功能:
|
|
* 1. 从 HD 钱包派生多个测试账户
|
|
* 2. 从主账户给测试账户转 WUSD
|
|
* 3. 批量执行 depositYT 买入交易
|
|
*
|
|
* 使用方法:
|
|
* npx tsx scripts/batch-buy-simulation.ts
|
|
*/
|
|
|
|
import {
|
|
createPublicClient,
|
|
createWalletClient,
|
|
http,
|
|
parseUnits,
|
|
formatUnits,
|
|
type Address,
|
|
type Hex
|
|
} from 'viem'
|
|
import { arbitrumSepolia } from 'viem/chains'
|
|
import { privateKeyToAccount, mnemonicToAccount } from 'viem/accounts'
|
|
|
|
// ============ 配置 ============
|
|
|
|
// 合约地址
|
|
const CONTRACTS = {
|
|
WUSD: '0x6d2bf81a631dFE19B2f348aE92cF6Ef41ca2DF98' as Address,
|
|
FACTORY: '0x982716f32F10BCB5B5944c1473a8992354bF632b' as Address,
|
|
VAULTS: {
|
|
YT_A: '0x0cA35994F033685E7a57ef9bc5d00dd3cf927330' as Address,
|
|
YT_B: '0x333805C9EE75f59Aa2Cc79DfDe2499F920c7b408' as Address,
|
|
YT_C: '0x6DF0ED6f0345F601A206974973dE9fC970598587' as Address,
|
|
}
|
|
}
|
|
|
|
// 模拟配置
|
|
const CONFIG = {
|
|
// 主账户私钥 (用于分发 WUSD)
|
|
MAIN_PRIVATE_KEY: '0xa082a7037105ebd606bee80906687e400d89899bbb6ba0273a61528c2f5fab89' as Hex,
|
|
|
|
// HD 钱包助记词 (用于生成测试账户)
|
|
// 测试用,请勿在生产环境使用
|
|
TEST_MNEMONIC: 'test test test test test test test test test test test junk',
|
|
|
|
// 模拟用户数量
|
|
USER_COUNT: 3,
|
|
|
|
// 每个用户的买入金额范围 (WUSD)
|
|
MIN_BUY_AMOUNT: 10,
|
|
MAX_BUY_AMOUNT: 100,
|
|
|
|
// 目标金库
|
|
TARGET_VAULT: CONTRACTS.VAULTS.YT_A,
|
|
|
|
// 交易间隔 (毫秒)
|
|
TX_INTERVAL: 2000,
|
|
|
|
// RPC URL
|
|
RPC_URL: 'https://sepolia-rollup.arbitrum.io/rpc',
|
|
}
|
|
|
|
// ============ ABI ============
|
|
|
|
const WUSD_ABI = [
|
|
{
|
|
inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }],
|
|
name: 'transfer',
|
|
outputs: [{ type: 'bool' }],
|
|
stateMutability: 'nonpayable',
|
|
type: 'function',
|
|
},
|
|
{
|
|
inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }],
|
|
name: 'approve',
|
|
outputs: [{ type: 'bool' }],
|
|
stateMutability: 'nonpayable',
|
|
type: 'function',
|
|
},
|
|
{
|
|
inputs: [{ name: 'account', type: 'address' }],
|
|
name: 'balanceOf',
|
|
outputs: [{ type: 'uint256' }],
|
|
stateMutability: 'view',
|
|
type: 'function',
|
|
},
|
|
] as const
|
|
|
|
const VAULT_ABI = [
|
|
{
|
|
inputs: [{ name: '_wusdAmount', type: 'uint256' }],
|
|
name: 'depositYT',
|
|
outputs: [],
|
|
stateMutability: 'nonpayable',
|
|
type: 'function',
|
|
},
|
|
{
|
|
inputs: [{ name: '_wusdAmount', type: 'uint256' }],
|
|
name: 'previewBuy',
|
|
outputs: [{ type: 'uint256' }],
|
|
stateMutability: 'view',
|
|
type: 'function',
|
|
},
|
|
{
|
|
inputs: [{ name: 'account', type: 'address' }],
|
|
name: 'balanceOf',
|
|
outputs: [{ type: 'uint256' }],
|
|
stateMutability: 'view',
|
|
type: 'function',
|
|
},
|
|
{
|
|
inputs: [],
|
|
name: 'symbol',
|
|
outputs: [{ type: 'string' }],
|
|
stateMutability: 'view',
|
|
type: 'function',
|
|
},
|
|
] as const
|
|
|
|
// ============ 工具函数 ============
|
|
|
|
function randomAmount(min: number, max: number): bigint {
|
|
const amount = Math.random() * (max - min) + min
|
|
return parseUnits(amount.toFixed(2), 18)
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
}
|
|
|
|
// ============ 主逻辑 ============
|
|
|
|
async function main() {
|
|
console.log('🚀 批量用户买入模拟开始\n')
|
|
|
|
// 创建客户端
|
|
const publicClient = createPublicClient({
|
|
chain: arbitrumSepolia,
|
|
transport: http(CONFIG.RPC_URL),
|
|
})
|
|
|
|
// 主账户
|
|
const mainAccount = privateKeyToAccount(CONFIG.MAIN_PRIVATE_KEY)
|
|
const mainWalletClient = createWalletClient({
|
|
account: mainAccount,
|
|
chain: arbitrumSepolia,
|
|
transport: http(CONFIG.RPC_URL),
|
|
})
|
|
|
|
console.log(`📍 主账户: ${mainAccount.address}`)
|
|
|
|
// 检查主账户 WUSD 余额
|
|
const mainBalance = await publicClient.readContract({
|
|
address: CONTRACTS.WUSD,
|
|
abi: WUSD_ABI,
|
|
functionName: 'balanceOf',
|
|
args: [mainAccount.address],
|
|
})
|
|
console.log(`💰 主账户 WUSD 余额: ${formatUnits(mainBalance, 18)}`)
|
|
|
|
// 获取金库 symbol
|
|
const vaultSymbol = await publicClient.readContract({
|
|
address: CONFIG.TARGET_VAULT,
|
|
abi: VAULT_ABI,
|
|
functionName: 'symbol',
|
|
})
|
|
console.log(`🏦 目标金库: ${vaultSymbol} (${CONFIG.TARGET_VAULT})`)
|
|
|
|
// 生成测试账户
|
|
console.log(`\n👥 生成 ${CONFIG.USER_COUNT} 个测试账户...\n`)
|
|
|
|
const testAccounts = []
|
|
for (let i = 0; i < CONFIG.USER_COUNT; i++) {
|
|
const account = mnemonicToAccount(CONFIG.TEST_MNEMONIC, { addressIndex: i })
|
|
const buyAmount = randomAmount(CONFIG.MIN_BUY_AMOUNT, CONFIG.MAX_BUY_AMOUNT)
|
|
testAccounts.push({ account, buyAmount })
|
|
console.log(` 账户 ${i + 1}: ${account.address} | 计划买入: ${formatUnits(buyAmount, 18)} WUSD`)
|
|
}
|
|
|
|
// 计算总需要的 WUSD
|
|
const totalNeeded = testAccounts.reduce((sum, a) => sum + a.buyAmount, 0n)
|
|
console.log(`\n📊 总计需要: ${formatUnits(totalNeeded, 18)} WUSD`)
|
|
|
|
if (mainBalance < totalNeeded) {
|
|
console.error(`❌ 主账户余额不足! 需要 ${formatUnits(totalNeeded, 18)}, 只有 ${formatUnits(mainBalance, 18)}`)
|
|
return
|
|
}
|
|
|
|
// 第一步: 给测试账户转 ETH (gas 费)
|
|
console.log('\n⛽ 步骤 1: 分发 ETH (gas 费) 到测试账户...\n')
|
|
|
|
const gasAmount = parseUnits('0.05', 18) // 0.05 ETH 足够多次交易 (Arbitrum Sepolia gas 较高)
|
|
|
|
for (let i = 0; i < testAccounts.length; i++) {
|
|
const { account } = testAccounts[i]
|
|
|
|
// 检查 ETH 余额
|
|
const ethBalance = await publicClient.getBalance({ address: account.address })
|
|
|
|
if (ethBalance >= gasAmount) {
|
|
console.log(` ✓ 账户 ${i + 1} 已有足够 ETH: ${formatUnits(ethBalance, 18)} ETH`)
|
|
continue
|
|
}
|
|
|
|
const transferAmount = gasAmount - ethBalance
|
|
console.log(` → 转 ETH 给账户 ${i + 1}: ${formatUnits(transferAmount, 18)} ETH...`)
|
|
|
|
try {
|
|
const hash = await mainWalletClient.sendTransaction({
|
|
to: account.address,
|
|
value: transferAmount,
|
|
})
|
|
await publicClient.waitForTransactionReceipt({ hash })
|
|
console.log(` ✓ ETH 转账成功`)
|
|
} catch (error: any) {
|
|
console.error(` ✗ ETH 转账失败: ${error.message}`)
|
|
}
|
|
|
|
await sleep(500)
|
|
}
|
|
|
|
// 第二步: 给测试账户转 WUSD
|
|
console.log('\n📤 步骤 2: 分发 WUSD 到测试账户...\n')
|
|
|
|
for (let i = 0; i < testAccounts.length; i++) {
|
|
const { account, buyAmount } = testAccounts[i]
|
|
|
|
// 检查是否已有足够余额
|
|
const currentBalance = await publicClient.readContract({
|
|
address: CONTRACTS.WUSD,
|
|
abi: WUSD_ABI,
|
|
functionName: 'balanceOf',
|
|
args: [account.address],
|
|
})
|
|
|
|
if (currentBalance >= buyAmount) {
|
|
console.log(` ✓ 账户 ${i + 1} 已有足够余额: ${formatUnits(currentBalance, 18)} WUSD`)
|
|
continue
|
|
}
|
|
|
|
const transferAmount = buyAmount - currentBalance
|
|
console.log(` → 转账给账户 ${i + 1}: ${formatUnits(transferAmount, 18)} WUSD...`)
|
|
|
|
try {
|
|
const hash = await mainWalletClient.writeContract({
|
|
address: CONTRACTS.WUSD,
|
|
abi: WUSD_ABI,
|
|
functionName: 'transfer',
|
|
args: [account.address, transferAmount],
|
|
})
|
|
|
|
await publicClient.waitForTransactionReceipt({ hash })
|
|
console.log(` ✓ 转账成功: ${hash}`)
|
|
} catch (error: any) {
|
|
console.error(` ✗ 转账失败: ${error.message}`)
|
|
}
|
|
|
|
await sleep(1000)
|
|
}
|
|
|
|
// 第三步: 批量买入
|
|
console.log('\n🛒 步骤 3: 执行批量买入...\n')
|
|
|
|
const results: { address: string; amount: string; ytReceived: string; status: string; hash?: string }[] = []
|
|
|
|
for (let i = 0; i < testAccounts.length; i++) {
|
|
const { account, buyAmount } = testAccounts[i]
|
|
|
|
console.log(`\n[${i + 1}/${testAccounts.length}] 账户: ${account.address}`)
|
|
console.log(` 买入金额: ${formatUnits(buyAmount, 18)} WUSD`)
|
|
|
|
const walletClient = createWalletClient({
|
|
account,
|
|
chain: arbitrumSepolia,
|
|
transport: http(CONFIG.RPC_URL),
|
|
})
|
|
|
|
try {
|
|
// 预览买入
|
|
const previewYT = await publicClient.readContract({
|
|
address: CONFIG.TARGET_VAULT,
|
|
abi: VAULT_ABI,
|
|
functionName: 'previewBuy',
|
|
args: [buyAmount],
|
|
})
|
|
console.log(` 预计获得: ${formatUnits(previewYT, 18)} ${vaultSymbol}`)
|
|
|
|
// 授权
|
|
console.log(` → 授权 WUSD...`)
|
|
const approveHash = await walletClient.writeContract({
|
|
address: CONTRACTS.WUSD,
|
|
abi: WUSD_ABI,
|
|
functionName: 'approve',
|
|
args: [CONFIG.TARGET_VAULT, buyAmount],
|
|
})
|
|
await publicClient.waitForTransactionReceipt({ hash: approveHash })
|
|
console.log(` ✓ 授权成功`)
|
|
|
|
// 买入
|
|
console.log(` → 执行买入...`)
|
|
const buyHash = await walletClient.writeContract({
|
|
address: CONFIG.TARGET_VAULT,
|
|
abi: VAULT_ABI,
|
|
functionName: 'depositYT',
|
|
args: [buyAmount],
|
|
})
|
|
await publicClient.waitForTransactionReceipt({ hash: buyHash })
|
|
|
|
// 检查 YT 余额
|
|
const ytBalance = await publicClient.readContract({
|
|
address: CONFIG.TARGET_VAULT,
|
|
abi: VAULT_ABI,
|
|
functionName: 'balanceOf',
|
|
args: [account.address],
|
|
})
|
|
|
|
console.log(` ✓ 买入成功! YT 余额: ${formatUnits(ytBalance, 18)} ${vaultSymbol}`)
|
|
console.log(` 交易哈希: ${buyHash}`)
|
|
|
|
results.push({
|
|
address: account.address,
|
|
amount: formatUnits(buyAmount, 18),
|
|
ytReceived: formatUnits(ytBalance, 18),
|
|
status: 'success',
|
|
hash: buyHash,
|
|
})
|
|
|
|
} catch (error: any) {
|
|
console.error(` ✗ 买入失败: ${error.message}`)
|
|
results.push({
|
|
address: account.address,
|
|
amount: formatUnits(buyAmount, 18),
|
|
ytReceived: '0',
|
|
status: 'failed',
|
|
})
|
|
}
|
|
|
|
// 交易间隔
|
|
if (i < testAccounts.length - 1) {
|
|
console.log(` ⏳ 等待 ${CONFIG.TX_INTERVAL / 1000} 秒...`)
|
|
await sleep(CONFIG.TX_INTERVAL)
|
|
}
|
|
}
|
|
|
|
// 输出汇总
|
|
console.log('\n' + '='.repeat(60))
|
|
console.log('📊 模拟结果汇总')
|
|
console.log('='.repeat(60))
|
|
|
|
const successCount = results.filter(r => r.status === 'success').length
|
|
const failCount = results.filter(r => r.status === 'failed').length
|
|
const totalBought = results
|
|
.filter(r => r.status === 'success')
|
|
.reduce((sum, r) => sum + parseFloat(r.amount), 0)
|
|
const totalYT = results
|
|
.filter(r => r.status === 'success')
|
|
.reduce((sum, r) => sum + parseFloat(r.ytReceived), 0)
|
|
|
|
console.log(`\n成功: ${successCount} 笔`)
|
|
console.log(`失败: ${failCount} 笔`)
|
|
console.log(`总买入 WUSD: ${totalBought.toFixed(2)}`)
|
|
console.log(`总获得 ${vaultSymbol}: ${totalYT.toFixed(2)}`)
|
|
|
|
console.log('\n详细记录:')
|
|
console.table(results.map(r => ({
|
|
地址: r.address.slice(0, 10) + '...',
|
|
买入WUSD: r.amount,
|
|
获得YT: r.ytReceived,
|
|
状态: r.status,
|
|
})))
|
|
|
|
console.log('\n✅ 模拟完成!')
|
|
}
|
|
|
|
// 运行
|
|
main().catch(console.error)
|