diff --git a/scripts/harb-evaluator/helpers/swap.ts b/scripts/harb-evaluator/helpers/swap.ts index cde36c9..01b83a9 100644 --- a/scripts/harb-evaluator/helpers/swap.ts +++ b/scripts/harb-evaluator/helpers/swap.ts @@ -174,35 +174,40 @@ export async function buyKrk(page: Page, ethAmount: string, opts?: BuyKrkOptions * * Drives the real sell widget UI (requires the #456 sell tab). * - * If config is provided, queries WETH balance before and after the sell and - * returns the delta. Otherwise returns 0n (caller is responsible for verification). + * 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 to query WETH received after sell + * @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?: SellConfig, + config?: Pick, ): Promise { 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 { - const sellInput = page.getByTestId('swap-sell-amount-input'); await expect(sellInput).toBeVisible({ timeout: 5_000 }); await sellInput.fill(amount); console.log(`[swap] Filled sell amount: ${amount}`); @@ -210,6 +215,23 @@ export async function sellKrk( 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` }); } @@ -228,7 +250,28 @@ export async function sellKrk( 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'); + 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` });