2026-03-02 05:21:24 +00:00
|
|
|
/**
|
|
|
|
|
* Shared assertion helpers for holdout scenarios.
|
|
|
|
|
*
|
|
|
|
|
* These wrap Playwright's `expect` with protocol-specific checks so that
|
|
|
|
|
* scenario specs stay at the "what" level rather than encoding RPC details.
|
|
|
|
|
*/
|
|
|
|
|
import { expect } from '@playwright/test';
|
|
|
|
|
import { Interface } from 'ethers';
|
2026-03-02 05:59:21 +00:00
|
|
|
import { rpcCall } from './rpc';
|
2026-03-02 05:21:24 +00:00
|
|
|
|
|
|
|
|
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
async function getTokenBalance(rpcUrl: string, token: string, address: string): Promise<bigint> {
|
|
|
|
|
if (token.toLowerCase() === 'eth') {
|
|
|
|
|
const result = (await rpcCall(rpcUrl, 'eth_getBalance', [address, 'latest'])) as string;
|
|
|
|
|
return BigInt(result);
|
|
|
|
|
}
|
|
|
|
|
const selector = '0x70a08231'; // balanceOf(address)
|
|
|
|
|
const data = selector + address.slice(2).padStart(64, '0');
|
|
|
|
|
const result = (await rpcCall(rpcUrl, 'eth_call', [{ to: token, data }, 'latest'])) as string;
|
|
|
|
|
return BigInt(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const POOL_ABI = [
|
|
|
|
|
'function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)',
|
|
|
|
|
];
|
|
|
|
|
const poolIface = new Interface(POOL_ABI);
|
|
|
|
|
|
|
|
|
|
// ── Exported helpers ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Snapshot the balance of `token` for `address`, run `action`, then assert the
|
|
|
|
|
* balance increased.
|
|
|
|
|
*
|
|
|
|
|
* @param token - An ERC-20 contract address, or the string `'eth'` for native ETH.
|
|
|
|
|
*/
|
|
|
|
|
export async function expectBalanceIncrease(
|
|
|
|
|
rpcUrl: string,
|
|
|
|
|
token: string,
|
|
|
|
|
address: string,
|
|
|
|
|
action: () => Promise<void>,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const before = await getTokenBalance(rpcUrl, token, address);
|
|
|
|
|
await action();
|
|
|
|
|
const after = await getTokenBalance(rpcUrl, token, address);
|
|
|
|
|
expect(after).toBeGreaterThan(before);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-02 05:59:21 +00:00
|
|
|
* Assert that the Uniswap V3 pool at `poolAddress` is initialised.
|
2026-03-02 05:21:24 +00:00
|
|
|
*
|
2026-03-02 05:59:21 +00:00
|
|
|
* Checks that sqrtPriceX96 > 0, which confirms the pool has been seeded and
|
|
|
|
|
* can execute swaps. Active-tick liquidity is intentionally not checked here:
|
|
|
|
|
* after a sovereign exit the LiquidityManager's three range positions (Floor,
|
|
|
|
|
* Anchor, Discovery) may all sit outside the current tick while the pool
|
|
|
|
|
* itself remains functional.
|
2026-03-02 05:21:24 +00:00
|
|
|
*/
|
|
|
|
|
export async function expectPoolHasLiquidity(rpcUrl: string, poolAddress: string): Promise<void> {
|
|
|
|
|
const slot0Encoded = poolIface.encodeFunctionData('slot0', []);
|
|
|
|
|
const slot0Hex = (await rpcCall(rpcUrl, 'eth_call', [{ to: poolAddress, data: slot0Encoded }, 'latest'])) as string;
|
|
|
|
|
const [sqrtPriceX96] = poolIface.decodeFunctionResult('slot0', slot0Hex);
|
|
|
|
|
expect(sqrtPriceX96).toBeGreaterThan(0n);
|
|
|
|
|
}
|