harb/scripts/harb-evaluator/helpers/recenter.ts

87 lines
3.1 KiB
TypeScript
Raw Normal View History

/**
* 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<boolean> {
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<void> {
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`);
}