2026-03-03 20:58:01 +00:00
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles. */
2025-10-08 15:51:49 +00:00
import { expect , test , type APIRequestContext } from '@playwright/test' ;
import { Wallet } from 'ethers' ;
import { createWalletContext } from '../setup/wallet-provider' ;
import { getStackConfig , validateStackHealthy } from '../setup/stack' ;
2026-02-20 17:28:59 +01:00
import { navigateSPA } from '../setup/navigate' ;
2025-10-08 15:51:49 +00:00
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 } ) = > {
feat: OptimizerV3 with direct 2D staking-to-LP parameter mapping
Core protocol changes for launch readiness:
- OptimizerV3: binary bear/bull mapping from (staking%, avgTax) — avoids
exploitable AW 30-90 kill zone. Bear: AS=30%, AW=100, CI=0, DD=0.3e18.
Bull: AS=100%, AW=20, CI=0, DD=1e18. UUPS upgradeable with __gap[48].
- Directional VWAP: only records prices on ETH inflow (buys), preventing
sell-side dilution of price memory
- Floor formula: unified max(scarcity, mirror, clamp) — VWAP mirror uses
distance from adjusted VWAP as floor distance, no branching
- PriceOracle (M-1 fix): correct fallback TWAP divisor (60000s, not 300s)
- Access control (M-2 fix): deployer-only guard on one-time setters
- Recenter rate limit (M-3 fix): 60-second cooldown for open recenters
- Safe fallback params: recenter() optimizer-failure defaults changed from
exploitable CI=50%/AW=50 to safe bear-mode CI=0/AW=100
- Recentered event for monitoring and indexing
- VERSION bump to 2, kraiken-lib COMPATIBLE_CONTRACT_VERSIONS updated
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:21:18 +00:00
test . setTimeout ( 10 * 60 * 1000 ) ; // 10 minutes — this test creates 30 staking positions via UI
2025-10-08 15:51:49 +00:00
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' } ) ;
2026-02-02 19:24:57 +01:00
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' ) ;
}
}
2025-10-08 15:51:49 +00:00
// 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...' ) ;
2026-02-20 17:28:59 +01:00
await navigateSPA ( page , '/app/cheats' ) ;
await expect ( page . getByRole ( 'heading' , { name : 'Cheat Console' } ) ) . toBeVisible ( { timeout : 10_000 } ) ;
2025-10-08 15:51:49 +00:00
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 ) ;
2026-03-02 07:23:31 +00:00
// Step 2: Buy a large amount of KRK tokens via the get-krk page
console . log ( '[TEST] Navigating to get-krk page to buy KRK...' ) ;
await navigateSPA ( page , '/app/get-krk' ) ;
await expect ( page . getByRole ( 'heading' , { name : 'Get $KRK Tokens' } ) ) . toBeVisible ( { timeout : 10_000 } ) ;
2025-10-08 15:51:49 +00:00
console . log ( '[TEST] Buying KRK tokens (swapping 5 ETH)...' ) ;
2026-03-02 23:13:05 +00:00
const ethToSpendInput = page . getByTestId ( 'swap-amount-input' ) ;
2026-03-02 07:23:31 +00:00
await expect ( ethToSpendInput ) . toBeVisible ( { timeout : 15_000 } ) ;
2025-10-08 15:51:49 +00:00
await ethToSpendInput . fill ( '5' ) ;
2026-03-02 23:13:05 +00:00
const buyButton = page . getByTestId ( 'swap-buy-button' ) ;
2025-10-08 15:51:49 +00:00
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 } ) ;
2026-03-02 23:47:53 +00:00
await expect ( page . getByTestId ( 'swap-buy-button' ) ) . toHaveText ( 'Buy KRK' , { timeout : 60_000 } ) ;
2025-10-08 15:51:49 +00:00
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 : [ {
2025-11-20 18:54:53 +01:00
to : STACK_CONFIG.contracts.Kraiken , // KRK token from deployments
2025-10-08 15:51:49 +00:00
data : ` 0x70a08231000000000000000000000000 ${ ACCOUNT_ADDRESS . slice ( 2 ) } ` // balanceOf(address)
} , 'latest' ]
} )
} ) ;
const balanceData = await balanceResponse . json ( ) ;
2025-11-20 18:54:53 +01:00
const balance = BigInt ( balanceData . result || '0x0' ) ;
2025-10-08 15:51:49 +00:00
console . log ( ` [TEST] KRK balance: ${ balance . toString ( ) } wei ` ) ;
expect ( balance ) . toBeGreaterThan ( 0 n ) ;
// Step 3: Navigate to stake page
console . log ( '[TEST] Navigating to stake page...' ) ;
2026-02-20 17:28:59 +01:00
await navigateSPA ( page , '/app/stake' ) ;
2025-10-08 15:51:49 +00:00
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 ( ) ;
}
} ) ;
} ) ;