import { expect, test, type APIRequestContext } from '@playwright/test'; import { Wallet } from 'ethers'; import { createWalletContext } from '../setup/wallet-provider'; import { getStackConfig, validateStackHealthy } from '../setup/stack'; import { TAX_RATE_OPTIONS } from '../../kraiken-lib/src/taxRates'; const ACCOUNT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; const ACCOUNT_ADDRESS = new Wallet(ACCOUNT_PRIVATE_KEY).address.toLowerCase(); // Get stack configuration from environment (or defaults) const STACK_CONFIG = getStackConfig(); const STACK_RPC_URL = STACK_CONFIG.rpcUrl; const STACK_WEBAPP_URL = STACK_CONFIG.webAppUrl; const STACK_GRAPHQL_URL = STACK_CONFIG.graphqlUrl; async function fetchPositions(request: APIRequestContext, owner: string) { const response = await request.post(STACK_GRAPHQL_URL, { data: { query: ` query PositionsByOwner($owner: String!) { positionss(where: { owner: $owner }, limit: 100) { items { id owner taxRate stakeDeposit status } } } `, variables: { owner }, }, headers: { 'content-type': 'application/json' }, }); expect(response.ok()).toBeTruthy(); const payload = await response.json(); return (payload?.data?.positionss?.items ?? []) as Array<{ id: string; owner: string; taxRate: number; stakeDeposit: string; status: string; }>; } async function fetchStats(request: APIRequestContext) { const response = await request.post(STACK_GRAPHQL_URL, { data: { query: ` query Stats { stats(id: "0x01") { kraikenTotalSupply kraikenStakedSupply percentageStaked } } `, }, headers: { 'content-type': 'application/json' }, }); expect(response.ok()).toBeTruthy(); const payload = await response.json(); return payload?.data?.stats; } test.describe('Max Stake All Tax Rates', () => { test.beforeAll(async () => { await validateStackHealthy(STACK_CONFIG); }); test('fills all tax rates until maxStake is reached', async ({ browser, request }) => { test.setTimeout(10 * 60 * 1000); // 10 minutes — this test creates 30 staking positions via UI console.log('[TEST] Creating wallet context...'); const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); // Log browser console messages page.on('console', msg => console.log(`[BROWSER] ${msg.type()}: ${msg.text()}`)); page.on('pageerror', error => console.log(`[BROWSER ERROR] ${error.message}`)); try { console.log('[TEST] Loading app...'); await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' }); console.log('[TEST] App loaded, waiting for Vue app to mount...'); // Wait for the Vue app to fully mount by waiting for a key element // The navbar-title is always present regardless of connection state const navbarTitle = page.locator('.navbar-title').first(); await expect(navbarTitle).toBeVisible({ timeout: 30_000 }); console.log('[TEST] Vue app mounted, navbar is visible'); // Trigger a resize event to force Vue's useMobile composable to recalculate // This ensures the app recognizes the desktop screen width set by wallet-provider await page.evaluate(() => { window.dispatchEvent(new Event('resize')); }); await page.waitForTimeout(500); // Give extra time for wallet connectors to initialize await page.waitForTimeout(2_000); // Connect wallet flow: // The wallet-provider sets screen.width to 1280 to ensure desktop mode. // We expect the desktop Connect button to be visible. console.log('[TEST] Looking for Connect button...'); // Desktop Connect button const connectButton = page.locator('.connect-button--disconnected').first(); let panelOpened = false; // Wait for the Connect button with a reasonable timeout if (await connectButton.isVisible({ timeout: 5_000 })) { console.log('[TEST] Found desktop Connect button, clicking...'); await connectButton.click(); panelOpened = true; } else { // Debug: Log current screen.width and navbar-end contents const screenWidth = await page.evaluate(() => window.screen.width); const navbarEndHtml = await page.locator('.navbar-end').innerHTML().catch(() => 'not found'); console.log(`[TEST] DEBUG: screen.width = ${screenWidth}`); console.log(`[TEST] DEBUG: navbar-end HTML = ${navbarEndHtml.substring(0, 500)}`); console.log('[TEST] Connect button not visible - checking for mobile fallback...'); // Fallback to mobile login icon (SVG in navbar-end when disconnected) const mobileLoginIcon = page.locator('.navbar-end svg').first(); if (await mobileLoginIcon.isVisible({ timeout: 2_000 })) { console.log('[TEST] Found mobile login icon, clicking...'); await mobileLoginIcon.click(); panelOpened = true; } else { console.log('[TEST] No Connect button or mobile icon visible - wallet may already be connected'); } } if (panelOpened) { await page.waitForTimeout(1_000); // Look for the injected wallet connector in the slideout panel console.log('[TEST] Looking for wallet connector in panel...'); const injectedConnector = page.locator('.connectors-element').first(); if (await injectedConnector.isVisible({ timeout: 5_000 })) { console.log('[TEST] Clicking first wallet connector...'); await injectedConnector.click(); await page.waitForTimeout(2_000); } else { console.log('[TEST] WARNING: No wallet connector found in panel'); } } // Verify wallet connection console.log('[TEST] Checking for wallet display...'); const walletDisplay = page.getByText(/0xf39F/i).first(); await expect(walletDisplay).toBeVisible({ timeout: 15_000 }); console.log('[TEST] Wallet connected successfully!'); // Step 1: Mint test ETH console.log('[TEST] Navigating to cheats page...'); await page.goto(`${STACK_WEBAPP_URL}/app/#/cheats`); await expect(page.getByRole('heading', { name: 'Cheat Console' })).toBeVisible(); console.log('[TEST] Minting test ETH (10 ETH)...'); await page.getByLabel('RPC URL').fill(STACK_RPC_URL); await page.getByLabel('Recipient').fill(ACCOUNT_ADDRESS); const mintButton = page.getByRole('button', { name: 'Mint' }); await expect(mintButton).toBeEnabled(); await mintButton.click(); await page.waitForTimeout(3_000); // Step 2: Buy a large amount of KRK tokens console.log('[TEST] Buying KRK tokens (swapping 5 ETH)...'); const ethToSpendInput = page.getByLabel('ETH to spend'); await ethToSpendInput.fill('5'); const buyButton = page.getByRole('button', { name: 'Buy' }).last(); await expect(buyButton).toBeVisible(); await buyButton.click(); // Wait for swap to complete console.log('[TEST] Waiting for swap to process...'); try { await page.getByRole('button', { name: /Submitting/i }).waitFor({ state: 'visible', timeout: 5_000 }); await page.getByRole('button', { name: 'Buy' }).last().waitFor({ state: 'visible', timeout: 60_000 }); console.log('[TEST] Swap completed!'); } catch (e) { console.log('[TEST] Swap may have completed instantly'); } await page.waitForTimeout(2_000); // Verify we have KRK tokens console.log('[TEST] Verifying KRK balance via RPC...'); const balanceResponse = await fetch(STACK_RPC_URL, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: STACK_CONFIG.contracts.Kraiken, // KRK token from deployments data: `0x70a08231000000000000000000000000${ACCOUNT_ADDRESS.slice(2)}` // balanceOf(address) }, 'latest'] }) }); const balanceData = await balanceResponse.json(); const balance = BigInt(balanceData.result || '0x0'); console.log(`[TEST] KRK balance: ${balance.toString()} wei`); expect(balance).toBeGreaterThan(0n); // Step 3: Navigate to stake page console.log('[TEST] Navigating to stake page...'); await page.goto(`${STACK_WEBAPP_URL}/app/#/stake`); await page.waitForTimeout(2_000); const tokenAmountSlider = page.getByRole('slider', { name: 'Token Amount' }); await expect(tokenAmountSlider).toBeVisible({ timeout: 15_000 }); // Step 4: Create staking positions for all tax rates console.log(`[TEST] Creating positions for all ${TAX_RATE_OPTIONS.length} tax rates...`); let positionCount = 0; let maxStakeReached = false; // We'll try to create positions with increasing amounts to fill maxStake faster const baseStakeAmount = 1000; for (const taxRateOption of TAX_RATE_OPTIONS) { if (maxStakeReached) break; console.log(`[TEST] Creating position with tax rate ${taxRateOption.index} (${taxRateOption.year}% annually)...`); // Fill stake form const stakeAmountInput = page.getByLabel('Staking Amount'); await expect(stakeAmountInput).toBeVisible({ timeout: 10_000 }); await stakeAmountInput.fill(baseStakeAmount.toString()); const taxSelect = page.getByRole('combobox', { name: 'Tax' }); await taxSelect.selectOption({ value: taxRateOption.index.toString() }); // Click stake button const stakeButton = page.getByRole('main').getByRole('button', { name: /Stake|Snatch and Stake/i }); await expect(stakeButton).toBeVisible({ timeout: 5_000 }); // Check if staking is still possible (button might be disabled if maxStake reached) const isDisabled = await stakeButton.isDisabled(); if (isDisabled) { console.log('[TEST] Stake button is disabled - likely maxStake reached'); maxStakeReached = true; break; } await stakeButton.click(); // Wait for transaction to process try { await page.getByRole('button', { name: /Sign Transaction|Waiting/i }).waitFor({ state: 'visible', timeout: 5_000 }); await page.getByRole('button', { name: /Stake|Snatch and Stake/i }).waitFor({ state: 'visible', timeout: 60_000 }); } catch (e) { // Transaction completed instantly or failed // Check if we got an error message const errorVisible = await page.getByText(/error|failed|exceeded/i).isVisible().catch(() => false); if (errorVisible) { console.log('[TEST] Transaction failed - likely maxStake reached'); maxStakeReached = true; break; } } positionCount++; await page.waitForTimeout(2_000); } console.log(`[TEST] Created ${positionCount} positions`); // Step 5: Verify positions via GraphQL console.log('[TEST] Verifying positions via GraphQL...'); const positions = await fetchPositions(request, ACCOUNT_ADDRESS); console.log(`[TEST] Found ${positions.length} position(s) in GraphQL`); const activePositions = positions.filter(p => p.status === 'Active'); console.log(`[TEST] ${activePositions.length} active positions`); expect(activePositions.length).toBeGreaterThan(0); // Verify we have positions with different tax rates const uniqueTaxRates = new Set(activePositions.map(p => p.taxRate)); console.log(`[TEST] Unique tax rates used: ${uniqueTaxRates.size}`); // We should have created positions for most/all tax rates (may have pre-existing positions from other tests) expect(positionCount).toBeGreaterThan(20); expect(uniqueTaxRates.size).toBeGreaterThan(20); // Step 6: Verify maxStake constraint console.log('[TEST] Verifying maxStake constraint...'); const stats = await fetchStats(request); console.log(`[TEST] Staked percentage: ${stats?.percentageStaked}`); if (stats?.percentageStaked) { const percentageStaked = parseFloat(stats.percentageStaked); // percentageStaked is a ratio (0-1), maxStake is 20% expect(percentageStaked).toBeLessThanOrEqual(1.0); console.log(`[TEST] ✅ MaxStake constraint respected: ${(percentageStaked * 100).toFixed(2)}% staked`); } console.log(`[TEST] ✅ Test complete: Created ${activePositions.length} positions across ${uniqueTaxRates.size} different tax rates`); // Take screenshot of final stake page with all positions console.log('[TEST] Taking screenshot of stake page...'); await page.screenshot({ path: 'test-results/stake-page-with-all-positions.png', fullPage: true }); console.log('[TEST] Screenshot saved to test-results/stake-page-with-all-positions.png'); } finally { await context.close(); } }); });