258 lines
10 KiB
TypeScript
258 lines
10 KiB
TypeScript
|
|
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 }) => {
|
||
|
|
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' });
|
||
|
|
await page.waitForTimeout(3_000);
|
||
|
|
|
||
|
|
// 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: '0xe527ddac2592faa45884a0b78e4d377a5d3df8cc', // KRK token
|
||
|
|
data: `0x70a08231000000000000000000000000${ACCOUNT_ADDRESS.slice(2)}` // balanceOf(address)
|
||
|
|
}, 'latest']
|
||
|
|
})
|
||
|
|
});
|
||
|
|
|
||
|
|
const balanceData = await balanceResponse.json();
|
||
|
|
const balance = BigInt(balanceData.result || '0');
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|