diff --git a/scripts/harb-evaluator/helpers/swap.ts b/scripts/harb-evaluator/helpers/swap.ts index 84f40d6..01b83a9 100644 --- a/scripts/harb-evaluator/helpers/swap.ts +++ b/scripts/harb-evaluator/helpers/swap.ts @@ -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, +): 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 { + 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