/** * Direct-RPC market simulation helper. * * Executes swaps via ethers Wallet (not browser/Playwright) so tests can * simulate market activity from non-user accounts without a UI. */ import { Interface, JsonRpcProvider, Wallet } from 'ethers'; import { waitForReceipt } from './swap.js'; import { rpcCall } from './rpc.js'; // Infrastructure addresses stable across Anvil forks of Base Sepolia const SWAP_ROUTER = '0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4'; const WETH = '0x4200000000000000000000000000000000000006'; const POOL_FEE = 10_000; // 1% tier used by the KRAIKEN pool const WETH_ABI = ['function deposit() payable']; const ERC20_ABI = ['function approve(address spender, uint256 amount) returns (bool)']; const ROUTER_ABI = [ 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)', ]; export interface MarketSwapConfig { rpcUrl: string; /** Anvil account private key for signing */ privateKey: string; /** Account address */ accountAddress: string; /** KRK token address */ krkAddress: string; } async function erc20Balance(rpcUrl: string, tokenAddress: string, account: string): Promise { const data = '0x70a08231' + account.slice(2).padStart(64, '0'); return BigInt((await rpcCall(rpcUrl, 'eth_call', [{ to: tokenAddress, data }, 'latest'])) as string); } /** * Execute a round-trip swap: buy KRK with `ethAmount` ETH, then sell ALL KRK back. * Uses direct RPC transactions (not browser/UI). Net effect on price ≈ 0, * but generates trading volume through the pool. * * Steps: * 1. Wrap ETH to WETH: call WETH.deposit{value: ethAmount}() * 2. Approve WETH to SwapRouter02 * 3. exactInputSingle: WETH → KRK (buy) * 4. Approve KRK to SwapRouter02 * 5. exactInputSingle: KRK → WETH (sell all) * * @returns Object with { krkBought: bigint, wethRecovered: bigint } */ export async function roundTripSwap( config: MarketSwapConfig, ethAmount: bigint, ): Promise<{ krkBought: bigint; wethRecovered: bigint }> { const provider = new JsonRpcProvider(config.rpcUrl); const wallet = new Wallet(config.privateKey, provider); const wethIface = new Interface(WETH_ABI); const erc20Iface = new Interface(ERC20_ABI); const routerIface = new Interface(ROUTER_ABI); const maxApproval = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); // Step 1: Wrap ETH → WETH console.log(`[market] Wrapping ${ethAmount} wei ETH to WETH...`); const depositData = wethIface.encodeFunctionData('deposit', []); const depositTx = await wallet.sendTransaction({ to: WETH, data: depositData, value: ethAmount }); await waitForReceipt(config.rpcUrl, depositTx.hash); console.log('[market] ETH wrapped to WETH'); // Step 2: Approve WETH to router console.log('[market] Approving WETH to router...'); const approveWethData = erc20Iface.encodeFunctionData('approve', [SWAP_ROUTER, maxApproval]); const approveWethTx = await wallet.sendTransaction({ to: WETH, data: approveWethData }); await waitForReceipt(config.rpcUrl, approveWethTx.hash); console.log('[market] WETH approved'); // Step 3: Buy KRK (WETH → KRK) // Snapshot balance before so krkBought is the delta, not the cumulative total. const krkBefore = await erc20Balance(config.rpcUrl, config.krkAddress, config.accountAddress); console.log('[market] Buying KRK (WETH → KRK)...'); const buyData = routerIface.encodeFunctionData('exactInputSingle', [ { tokenIn: WETH, tokenOut: config.krkAddress, fee: POOL_FEE, recipient: config.accountAddress, amountIn: ethAmount, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n, }, ]); const buyTx = await wallet.sendTransaction({ to: SWAP_ROUTER, data: buyData }); await waitForReceipt(config.rpcUrl, buyTx.hash); const krkBought = (await erc20Balance(config.rpcUrl, config.krkAddress, config.accountAddress)) - krkBefore; console.log(`[market] Bought ${krkBought} KRK`); if (krkBought === 0n) { provider.destroy(); throw new Error('[market] roundTripSwap: bought 0 KRK — pool may be empty or price limit hit'); } // Step 4: Approve KRK to router console.log('[market] Approving KRK to router...'); const approveKrkData = erc20Iface.encodeFunctionData('approve', [SWAP_ROUTER, maxApproval]); const approveKrkTx = await wallet.sendTransaction({ to: config.krkAddress, data: approveKrkData }); await waitForReceipt(config.rpcUrl, approveKrkTx.hash); console.log('[market] KRK approved'); // Step 5: Sell all KRK (KRK → WETH) console.log(`[market] Selling ${krkBought} KRK back to WETH...`); const wethBefore = await erc20Balance(config.rpcUrl, WETH, config.accountAddress); const sellData = routerIface.encodeFunctionData('exactInputSingle', [ { tokenIn: config.krkAddress, tokenOut: WETH, fee: POOL_FEE, recipient: config.accountAddress, amountIn: krkBought, amountOutMinimum: 0n, sqrtPriceLimitX96: 0n, }, ]); const sellTx = await wallet.sendTransaction({ to: SWAP_ROUTER, data: sellData }); await waitForReceipt(config.rpcUrl, sellTx.hash); provider.destroy(); const wethRecovered = (await erc20Balance(config.rpcUrl, WETH, config.accountAddress)) - wethBefore; console.log(`[market] Recovered ${wethRecovered} WETH`); return { krkBought, wethRecovered }; }