Merge pull request 'fix: evaluator: add sellKrk browser helper (uses sell widget from #456) (#461)' (#465) from fix/issue-461 into master

This commit is contained in:
johba 2026-03-05 15:25:47 +01:00
commit 0b5752ca52

View file

@ -1,7 +1,8 @@
/**
* Shared swap helpers for holdout scenarios.
*
* buyKrk drives the real get-krk page swap widget (UI path, requires #393 fix).
* buyKrk drives the real get-krk page swap widget (UI path, requires #393 fix).
* sellKrk drives the get-krk page sell widget UI (requires #456 sell tab).
* sellAllKrk submits approve + exactInputSingle directly via window.ethereum
* (no UI widget the Uniswap router handles the on-chain leg).
*/
@ -167,6 +168,127 @@ export async function buyKrk(page: Page, ethAmount: string, opts?: BuyKrkOptions
await page.screenshot({ path: `test-results/${screenshotPrefix}-after-buy.png` });
}
/**
* Navigate to the get-krk page, switch to Sell tab, fill KRK amount, click Sell.
* Wallet must already be connected.
*
* Drives the real sell widget UI (requires the #456 sell tab).
*
* If config is provided, creates an eth_newFilter for WETH Transfer events to the
* account before clicking Sell, polls eth_getFilterLogs until the event arrives
* (confirming the swap is mined), then returns the WETH balance delta. Otherwise
* returns 0n (caller is responsible for verification).
*
* @param page - Playwright page with injected wallet
* @param amount - KRK amount to sell (as string). Use 'max' to click the Max button.
* @param screenshotPrefix - Optional prefix for screenshot filenames
* @param config - Optional config for on-chain WETH receipt confirmation
* @returns WETH received (balance diff) or 0n if config is not provided
*/
export async function sellKrk(
page: Page,
amount: string,
screenshotPrefix?: string,
config?: Pick<SellConfig, 'rpcUrl' | 'accountAddress'>,
): Promise<bigint> {
console.log(`[swap] Selling ${amount} KRK via get-krk page sell widget...`);
await navigateSPA(page, '/app/get-krk');
await expect(page.getByRole('heading', { name: 'Get $KRK Tokens' })).toBeVisible({ timeout: 10_000 });
const sellTab = page.getByTestId('swap-mode-sell');
await expect(sellTab).toBeVisible({ timeout: 10_000 });
await sellTab.click();
const sellInput = page.getByTestId('swap-sell-amount-input');
if (amount === 'max') {
const maxButton = page.locator('.max-button');
await expect(maxButton).toBeVisible({ timeout: 5_000 });
await maxButton.click();
// setMax() is async — wait for the composable to populate the input via loadKrkBalance()
await expect(sellInput).not.toHaveValue('', { timeout: 10_000 });
console.log('[swap] Clicked Max button');
} else {
await expect(sellInput).toBeVisible({ timeout: 5_000 });
await sellInput.fill(amount);
console.log(`[swap] Filled sell amount: ${amount}`);
}
const wethBefore = config ? await erc20BalanceOf(config.rpcUrl, WETH, config.accountAddress) : 0n;
// Create WETH Transfer event filter BEFORE the sell (if config provided)
let filterId: string | undefined;
if (config) {
filterId = (await rpcCall(config.rpcUrl, 'eth_newFilter', [
{
address: WETH,
topics: [
TRANSFER_TOPIC,
null, // any sender (pool/router)
'0x' + config.accountAddress.slice(2).padStart(64, '0'), // to our account
],
fromBlock: 'latest',
},
])) as string;
console.log(`[swap] WETH Transfer filter created: ${filterId}`);
}
if (screenshotPrefix) {
await page.screenshot({ path: `test-results/${screenshotPrefix}-before-sell.png` });
}
const sellButton = page.getByTestId('swap-sell-button');
await expect(sellButton).toBeVisible({ timeout: 5_000 });
console.log('[swap] Clicking Sell KRK...');
await sellButton.click();
// Button cycles: "Sell KRK" → "Approving…" / "Selling…" → "Sell KRK"
try {
await sellButton.filter({ hasText: /Approving…|Selling…/i }).waitFor({ state: 'visible', timeout: 5_000 });
console.log('[swap] Sell in progress...');
} catch {
// Sell completed before the transient state could be observed
console.log('[swap] Button state not observed (sell may have completed instantly)');
}
await expect(sellButton).toHaveText('Sell KRK', { timeout: 60_000 });
console.log('[swap] Sell completed (UI idle)');
// Wait for on-chain confirmation via WETH Transfer event
if (config && filterId) {
console.log('[swap] Waiting for WETH Transfer event...');
const deadline = Date.now() + 15_000;
let received = false;
while (Date.now() < deadline) {
const logs = (await rpcCall(config.rpcUrl, 'eth_getFilterLogs', [filterId])) as unknown[];
if (logs && logs.length > 0) {
received = true;
console.log(`[swap] WETH Transfer event received (${logs.length} log(s))`);
break;
}
// eslint-disable-next-line no-restricted-syntax -- Polling with timeout: eth_getFilterLogs is HTTP-only polling (not push). See AGENTS.md #Engineering Principles.
await new Promise(r => setTimeout(r, 200));
}
await rpcCall(config.rpcUrl, 'eth_uninstallFilter', [filterId]).catch(() => {});
if (!received) {
throw new Error(`No WETH Transfer event received within 15s after selling ${amount} KRK`);
}
}
if (screenshotPrefix) {
await page.screenshot({ path: `test-results/${screenshotPrefix}-after-sell.png` });
}
if (!config) return 0n;
const wethAfter = await erc20BalanceOf(config.rpcUrl, WETH, config.accountAddress);
const wethReceived = wethAfter - wethBefore;
if (wethReceived <= 0n) {
console.warn('[swap] WARNING: WETH balance did not increase after sell — pool may have returned 0 output');
} else {
console.log(`[swap] Received ${wethReceived} WETH`);
}
return wethReceived;
}
/**
* Query the current KRK balance, then approve the Uniswap router and swap
* all KRK back to WETH via on-chain transactions submitted through the