fix: address review feedback on holdout helpers

- Extract rpcCall into helpers/rpc.ts to eliminate the duplicate copy
  in wallet.ts and assertions.ts (warning: code duplication)

- Fix waitForReceipt() in swap.ts to assert receipt.status === '0x1':
  reverted transactions (status 0x0) now throw immediately with a clear
  message instead of letting sellAllKrk silently succeed and fail later
  at the balance assertion (bug)

- Add screen.width debug log to connectWallet() before the isVisible
  check, restoring the regression signal from always-leave.spec.ts (warning)

- Fix expectPoolHasLiquidity() to only assert sqrtPriceX96 > 0 (pool
  is initialised); drop the active-tick liquidity() check which gives
  false negatives when price moves outside all LiquidityManager ranges
  after a sovereign exit (warning)

- Add WETH balance snapshot before/after the swap in sellAllKrk() and
  log a warning when WETH output is 0, making pool health degradation
  visible despite amountOutMinimum: 0n (warning)

- Add before/after screenshots in buyKrk() (holdout-before-buy.png,
  holdout-after-buy.png) to restore CI debugging artefacts (nit)

- Move waitForTimeout(2_000) settle buffer in buyKrk() to the catch
  path only; when the Submitting→idle transition is observed the extra
  wait is redundant (nit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-02 05:59:21 +00:00
parent d0a3bdecdc
commit 77a229018a
4 changed files with 70 additions and 55 deletions

View file

@ -9,7 +9,7 @@ import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { Interface } from 'ethers';
import { navigateSPA } from '../../../tests/setup/navigate';
import { getKrkBalance } from './wallet';
import { rpcCall } from './rpc';
// Infrastructure addresses stable across Anvil forks of Base Sepolia
const SWAP_ROUTER = '0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4';
@ -23,25 +23,33 @@ const ROUTER_ABI = [
// ── Internal helpers ─────────────────────────────────────────────────────────
/** Read an ERC-20 balanceOf in the Node.js context via direct RPC. */
async function erc20BalanceOf(rpcUrl: string, tokenAddress: string, account: string): Promise<bigint> {
const selector = '0x70a08231'; // balanceOf(address)
const data = selector + account.slice(2).padStart(64, '0');
return BigInt((await rpcCall(rpcUrl, 'eth_call', [{ to: tokenAddress, data }, 'latest'])) as string);
}
/**
* Poll eth_getTransactionReceipt until the tx is mined or maxAttempts exceeded.
* Anvil with automine resolves almost immediately; the loop guards against
* instances configured with a block interval or high RPC latency.
*
* Throws if the transaction was mined but reverted (status 0x0) so callers
* get a clear failure rather than a confusing downstream balance-assertion error.
*/
async function waitForReceipt(rpcUrl: string, txHash: string, maxAttempts = 20): Promise<void> {
for (let i = 0; i < maxAttempts; i++) {
const resp = await fetch(rpcUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'eth_getTransactionReceipt',
params: [txHash],
}),
});
const payload = await resp.json();
if (payload.result !== null) return;
const receipt = (await rpcCall(rpcUrl, 'eth_getTransactionReceipt', [txHash])) as Record<
string,
unknown
> | null;
if (receipt !== null) {
if (receipt.status === '0x0') {
throw new Error(`Transaction ${txHash} reverted (status 0x0)`);
}
return; // status === '0x1' — success
}
await new Promise(r => setTimeout(r, 500));
}
throw new Error(`Transaction ${txHash} not mined after ${maxAttempts * 500}ms`);
@ -50,7 +58,7 @@ async function waitForReceipt(rpcUrl: string, txHash: string, maxAttempts = 20):
// ── Public config type ───────────────────────────────────────────────────────
export interface SellConfig {
/** Anvil JSON-RPC endpoint (used to wait for receipt and query KRK balance). */
/** Anvil JSON-RPC endpoint (used to wait for receipt and query token balances). */
rpcUrl: string;
/** Deployed KRAIKEN (KRK) ERC-20 contract address. */
krkAddress: string;
@ -80,6 +88,8 @@ export async function buyKrk(page: Page, ethAmount: string): Promise<void> {
const buyButton = page.getByRole('button', { name: 'Buy KRK' });
await expect(buyButton).toBeVisible({ timeout: 5_000 });
await page.screenshot({ path: 'test-results/holdout-before-buy.png' });
console.log('[swap] Clicking Buy KRK...');
await buyButton.click();
@ -90,9 +100,12 @@ export async function buyKrk(page: Page, ethAmount: string): Promise<void> {
await page.getByRole('button', { name: 'Buy KRK' }).waitFor({ state: 'visible', timeout: 60_000 });
console.log('[swap] Swap completed');
} catch {
// Swap completed before the Submitting state could be observed
console.log('[swap] Button state not observed (swap may have completed instantly)');
await page.waitForTimeout(2_000);
}
await page.waitForTimeout(2_000);
await page.screenshot({ path: 'test-results/holdout-after-buy.png' });
}
/**
@ -102,13 +115,19 @@ export async function buyKrk(page: Page, ethAmount: string): Promise<void> {
*
* This is the "sovereign exit" path it bypasses the UI swap widget and
* sends transactions directly so the test is not gated on the sell-side UI.
*
* Logs a warning if the WETH balance does not increase after the swap, which
* indicates the pool returned 0 output (possible with amountOutMinimum: 0n on
* a partially-drained pool).
*/
export async function sellAllKrk(page: Page, config: SellConfig): Promise<void> {
const krkBalance = await getKrkBalance(config.rpcUrl, config.krkAddress, config.accountAddress);
const krkBalance = await erc20BalanceOf(config.rpcUrl, config.krkAddress, config.accountAddress);
if (krkBalance === 0n) throw new Error('sellAllKrk: KRK balance is 0 — nothing to sell');
console.log(`[swap] Selling ${krkBalance} KRK...`);
const wethBefore = await erc20BalanceOf(config.rpcUrl, WETH, config.accountAddress);
const erc20Iface = new Interface(ERC20_ABI);
const routerIface = new Interface(ROUTER_ABI);
@ -154,4 +173,11 @@ export async function sellAllKrk(page: Page, config: SellConfig): Promise<void>
);
await waitForReceipt(config.rpcUrl, swapTxHash);
console.log('[swap] Swap mined');
const wethAfter = await erc20BalanceOf(config.rpcUrl, WETH, config.accountAddress);
if (wethAfter <= wethBefore) {
console.warn('[swap] WARNING: WETH balance did not increase after sell — pool may have returned 0 output');
} else {
console.log(`[swap] Received ${wethAfter - wethBefore} WETH`);
}
}