/** * LiquidityManager recenter helper and Anvil block-mining utility. * * Pure Node.js — no browser/Playwright dependency. */ import { Interface, JsonRpcProvider, Wallet } from 'ethers'; import { rpcCall } from './rpc.js'; import { waitForReceipt } from './swap.js'; const RECENTER_ABI = [ 'function recenter() external returns (bool isUp)', 'event Recentered(int24 indexed currentTick, bool indexed isUp)', ]; export interface RecenterConfig { rpcUrl: string; /** Address of the LiquidityManager contract */ lmAddress: string; /** * Private key of an account with recenter access. * In the local Anvil stack, recenterAccess is granted to the deployer * (Anvil account index 0) and the txnBot (Anvil account index 2). * See scripts/bootstrap-common.sh for how access is granted at deploy time. */ privateKey: string; /** Address corresponding to privateKey */ accountAddress: string; } /** * Call LiquidityManager.recenter() from an authorized account. * * Reads isUp from the Recentered(int24 currentTick, bool isUp) event emitted * by the mined transaction — not from a follow-up eth_call, which would * simulate a second recenter on already-updated state and return the wrong value. * * @returns isUp - true if price moved up */ export async function triggerRecenter(config: RecenterConfig): Promise { const provider = new JsonRpcProvider(config.rpcUrl); const wallet = new Wallet(config.privateKey, provider); const iface = new Interface(RECENTER_ABI); const data = iface.encodeFunctionData('recenter', []); console.log('[recenter] Calling LiquidityManager.recenter()...'); const tx = await wallet.sendTransaction({ to: config.lmAddress, data }); await waitForReceipt(config.rpcUrl, tx.hash); console.log(`[recenter] recenter() mined: ${tx.hash}`); provider.destroy(); // Parse isUp from the Recentered event in the receipt logs. // A follow-up eth_call would simulate on post-recenter state and give the wrong result. const receipt = (await rpcCall(config.rpcUrl, 'eth_getTransactionReceipt', [tx.hash])) as { logs: Array<{ address: string; topics: string[]; data: string }>; }; const recenterEventTopic = iface.getEvent('Recentered')!.topicHash; const log = receipt.logs.find( l => l.address.toLowerCase() === config.lmAddress.toLowerCase() && l.topics[0] === recenterEventTopic, ); if (!log) { throw new Error('[recenter] Recentered event not found in receipt — did the tx revert silently?'); } const parsed = iface.parseLog({ topics: log.topics, data: log.data }); const isUp = Boolean(parsed!.args.isUp); console.log(`[recenter] isUp = ${isUp}`); return isUp; } /** * Mine `blocks` empty blocks on Anvil to advance time. * Useful to get past MIN_RECENTER_INTERVAL (if set). * * Uses the anvil_mine RPC method. */ export async function mineBlocks(rpcUrl: string, blocks: number): Promise { if (blocks <= 0) throw new Error(`mineBlocks: blocks must be > 0, got ${blocks}`); await rpcCall(rpcUrl, 'anvil_mine', ['0x' + blocks.toString(16)]); console.log(`[recenter] Mined ${blocks} blocks`); }