harb/scripts/harb-evaluator/helpers/assertions.ts
openhands d0a3bdecdc feat: Shared holdout scenario test helpers (#401)
Extract reusable helpers from sovereign-exit/always-leave.spec.ts into
three focused modules under scripts/harb-evaluator/helpers/:

- helpers/wallet.ts: connectWallet, disconnectWallet, getEthBalance,
  getKrkBalance — UI connect/disconnect flow and on-chain balance reads.

- helpers/swap.ts: buyKrk (navigates to the real /app/get-krk page and
  drives the LocalSwapWidget, now that #393 fill() fix is in), sellAllKrk
  (approve + exactInputSingle via window.ethereum, no UI dependency).

- helpers/assertions.ts: expectBalanceIncrease (snapshot/action/assert
  pattern for any token or ETH), expectPoolHasLiquidity (slot0 + liquidity
  sanity check on a Uniswap V3 pool).

always-leave.spec.ts is refactored to use these helpers and to navigate
to /app/get-krk instead of the /app/cheats workaround introduced before
the #393 fix landed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 05:21:24 +00:00

82 lines
3.5 KiB
TypeScript

/**
* 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';
// ── Internal helpers ─────────────────────────────────────────────────────────
async function rpcCall(rpcUrl: string, method: string, params: unknown[]): Promise<unknown> {
const resp = await fetch(rpcUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
});
const payload = await resp.json();
if (payload.error) throw new Error(`RPC ${method}: ${payload.error.message}`);
return payload.result;
}
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)',
'function liquidity() external view returns (uint128)',
];
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);
}
/**
* Assert that the Uniswap V3 pool at `poolAddress` is initialised and has
* non-zero active liquidity at the current tick.
*
* Used after a sovereign exit to confirm the pool remains functional and the
* LiquidityManager's positions are intact.
*/
export async function expectPoolHasLiquidity(rpcUrl: string, poolAddress: string): Promise<void> {
// slot0() — check pool is initialised (sqrtPriceX96 > 0)
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);
// liquidity() — active liquidity at the current tick must be non-zero
const liquidityEncoded = poolIface.encodeFunctionData('liquidity', []);
const liquidityHex = (await rpcCall(rpcUrl, 'eth_call', [
{ to: poolAddress, data: liquidityEncoded },
'latest',
])) as string;
const [liquidity] = poolIface.decodeFunctionResult('liquidity', liquidityHex);
expect(liquidity).toBeGreaterThan(0n);
}