2026-03-05 10:52:28 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
|
2026-03-05 11:27:31 +00:00
|
|
|
const WETH_ABI = ['function deposit() payable'];
|
2026-03-05 10:52:28 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 11:27:31 +00:00
|
|
|
async function erc20Balance(rpcUrl: string, tokenAddress: string, account: string): Promise<bigint> {
|
|
|
|
|
const data = '0x70a08231' + account.slice(2).padStart(64, '0');
|
|
|
|
|
return BigInt((await rpcCall(rpcUrl, 'eth_call', [{ to: tokenAddress, data }, 'latest'])) as string);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 10:52:28 +00:00
|
|
|
/**
|
|
|
|
|
* 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)
|
2026-03-05 11:27:31 +00:00
|
|
|
// Snapshot balance before so krkBought is the delta, not the cumulative total.
|
|
|
|
|
const krkBefore = await erc20Balance(config.rpcUrl, config.krkAddress, config.accountAddress);
|
2026-03-05 10:52:28 +00:00
|
|
|
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);
|
|
|
|
|
|
2026-03-05 11:27:31 +00:00
|
|
|
const krkBought = (await erc20Balance(config.rpcUrl, config.krkAddress, config.accountAddress)) - krkBefore;
|
2026-03-05 10:52:28 +00:00
|
|
|
console.log(`[market] Bought ${krkBought} KRK`);
|
|
|
|
|
|
|
|
|
|
if (krkBought === 0n) {
|
2026-03-05 11:27:31 +00:00
|
|
|
provider.destroy();
|
2026-03-05 10:52:28 +00:00
|
|
|
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...`);
|
2026-03-05 11:27:31 +00:00
|
|
|
const wethBefore = await erc20Balance(config.rpcUrl, WETH, config.accountAddress);
|
2026-03-05 10:52:28 +00:00
|
|
|
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);
|
|
|
|
|
|
2026-03-05 11:27:31 +00:00
|
|
|
provider.destroy();
|
|
|
|
|
|
|
|
|
|
const wethRecovered = (await erc20Balance(config.rpcUrl, WETH, config.accountAddress)) - wethBefore;
|
2026-03-05 10:52:28 +00:00
|
|
|
console.log(`[market] Recovered ${wethRecovered} WETH`);
|
|
|
|
|
|
|
|
|
|
return { krkBought, wethRecovered };
|
|
|
|
|
}
|