harb/tests/e2e/01-acquire-and-stake.spec.ts
johba a555a2fdd1 refactor: migrate kraiken-lib to explicit subpath imports (BREAKING CHANGE) (#89)
Removes the barrel export pattern in favor of explicit subpath imports
for better tree-shaking and clearer dependencies.

## Breaking Changes
- Removed `src/helpers.ts` barrel export
- Removed `./helpers` from package.json exports
- Root `kraiken-lib` import now raises build errors
- Consumers MUST use explicit subpaths:
  - `kraiken-lib/abis` - Contract ABIs
  - `kraiken-lib/staking` - Staking helpers
  - `kraiken-lib/snatch` - Snatch selection
  - `kraiken-lib/ids` - Position ID utilities
  - `kraiken-lib/subgraph` - Byte conversion utilities
  - `kraiken-lib/taxRates` - Tax rate constants
  - `kraiken-lib/version` - Version validation

## Changes
- kraiken-lib:
  - Bumped version to 1.0.0 (breaking change)
  - Updated src/index.ts to raise build errors
  - Added backward-compatible ABI aliases (KraikenAbi, StakeAbi)
  - Updated all test files to use .js extensions and new imports
  - Updated documentation (README, AGENTS.md)

- Consumer updates:
  - services/ponder: Updated ponder.config.ts to use kraiken-lib/abis
  - web-app: Updated all imports to use subpaths
    - composables/usePositions.ts: kraiken-lib/subgraph
    - contracts/harb.ts: kraiken-lib/abis
    - contracts/stake.ts: kraiken-lib/abis

## Migration Guide
```typescript
// OLD
import { getSnatchList } from 'kraiken-lib/helpers';
import { KraikenAbi } from 'kraiken-lib';

// NEW
import { getSnatchList } from 'kraiken-lib/snatch';
import { KraikenAbi } from 'kraiken-lib/abis';
```

Fixes #86

Co-authored-by: openhands <openhands@all-hands.dev>
Reviewed-on: https://codeberg.org/johba/harb/pulls/89
2025-11-20 18:54:53 +01:00

217 lines
9.2 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';
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: 5) {
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;
}>;
}
test.describe('Acquire & Stake', () => {
// Validate that a healthy stack exists before running tests
// Tests do NOT start their own stack - stack must be running already
test.beforeAll(async () => {
await validateStackHealthy(STACK_CONFIG);
});
test('users can swap KRK via UI', 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' });
console.log('[TEST] App loaded, waiting for wallet to initialize...');
// Wait for wallet to be fully recognized
await page.waitForTimeout(3_000);
// Check if wallet shows as connected in UI
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!');
console.log('[TEST] Verifying stack version footer...');
const versionFooter = page.getByTestId('stack-version-footer');
await expect(versionFooter).toBeVisible({ timeout: 15_000 });
await expect(page.getByTestId('stack-version-contracts')).not.toHaveText(/Loading|—/i);
await expect(page.getByTestId('stack-version-ponder')).not.toHaveText(/Loading|—/i);
await expect(page.getByTestId('stack-version-kraiken-lib')).toHaveText(/^v\d+/i);
await expect(page.getByTestId('stack-version-web-app')).toHaveText(/^v/i);
const warningBanner = page.getByTestId('stack-version-warning');
await expect(warningBanner).toHaveCount(0);
console.log('[TEST] Stack version footer verified.');
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...');
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);
console.log('[TEST] Buying KRK tokens via swap...');
await page.screenshot({ path: 'test-results/before-swap.png' });
// Check if swap is available
const buyWarning = await page.getByText('Connect to the Base Sepolia fork').isVisible().catch(() => false);
if (buyWarning) {
throw new Error('Swap not available - chain config issue persists');
}
const ethToSpendInput = page.getByLabel('ETH to spend');
await ethToSpendInput.fill('0.05');
const buyButton = page.getByRole('button', { name: 'Buy' }).last();
await expect(buyButton).toBeVisible();
console.log('[TEST] Clicking Buy button...');
await buyButton.click();
// Wait for button to show "Submitting..." then return to "Buy"
console.log('[TEST] Waiting for swap to process...');
try {
await page.getByRole('button', { name: /Submitting/i }).waitFor({ state: 'visible', timeout: 5_000 });
console.log('[TEST] Swap initiated, waiting for completion...');
await page.getByRole('button', { name: 'Buy' }).last().waitFor({ state: 'visible', timeout: 60_000 });
console.log('[TEST] Swap completed!');
} catch (e) {
console.log('[TEST] No "Submitting" state detected, swap may have completed instantly');
}
await page.waitForTimeout(2_000);
console.log('[TEST] Verifying swap via RPC...');
// Query the blockchain directly to verify KRK balance increased
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);
console.log('[TEST] ✅ Swap successful! KRK balance > 0');
// Now test staking via UI
console.log('[TEST] Navigating to stake page...');
await page.goto(`${STACK_WEBAPP_URL}/app/#/stake`);
await page.waitForTimeout(2_000);
// Wait for the stake form to be initialized
console.log('[TEST] Waiting for stake form to load...');
const tokenAmountSlider = page.getByRole('slider', { name: 'Token Amount' });
await expect(tokenAmountSlider).toBeVisible({ timeout: 15_000 });
console.log('[TEST] Filling stake form via accessible controls...');
// Take screenshot before interaction
await page.screenshot({ path: 'test-results/before-stake-fill.png' });
// Check if input is visible and enabled
const stakeAmountInput = page.getByLabel('Staking Amount');
console.log('[TEST] Checking if staking amount input is visible...');
await expect(stakeAmountInput).toBeVisible({ timeout: 10_000 });
console.log('[TEST] Staking amount input is visible, filling value...');
await stakeAmountInput.fill('100');
console.log('[TEST] Filled staking amount!');
const taxSelect = page.getByRole('combobox', { name: 'Tax' });
console.log('[TEST] Selecting tax rate...');
await taxSelect.selectOption({ value: '2' });
console.log('[TEST] Tax rate selected!');
console.log('[TEST] Clicking stake button...');
// Use the main form's submit button (large/block), not the small position card buttons
const stakeButton = page.getByRole('main').getByRole('button', { name: /Stake|Snatch and Stake/i });
await expect(stakeButton).toBeVisible({ timeout: 5_000 });
await stakeButton.click();
// Wait for transaction to process
console.log('[TEST] Waiting for stake transaction...');
try {
await page.getByRole('button', { name: /Sign Transaction|Waiting/i }).waitFor({ state: 'visible', timeout: 5_000 });
console.log('[TEST] Transaction initiated, waiting for completion...');
await page.getByRole('button', { name: /Stake|Snatch and Stake/i }).waitFor({ state: 'visible', timeout: 60_000 });
console.log('[TEST] Stake transaction completed!');
} catch (e) {
console.log('[TEST] Transaction may have completed instantly');
}
await page.waitForTimeout(3_000);
// Verify staking position via GraphQL
console.log('[TEST] Verifying staking position via GraphQL...');
const positions = await fetchPositions(request, ACCOUNT_ADDRESS);
console.log(`[TEST] Found ${positions.length} position(s)`);
expect(positions.length).toBeGreaterThan(0);
const activePositions = positions.filter(p => p.status === 'Active');
expect(activePositions.length).toBeGreaterThan(0);
console.log(`[TEST] ✅ Staking successful! Created ${activePositions.length} active position(s)`);
console.log('[TEST] ✅ E2E test complete: Full journey verified (Mint → Swap → Stake)');
} finally {
await context.close();
}
});
});