Replace UBI with ETH reserve in ring buffer, fix Dockerfile HEALTHCHECK, enhance LiveStats (#154)
This commit is contained in:
parent
31063379a8
commit
76b2635e63
16 changed files with 2028 additions and 89 deletions
366
tests/e2e/06-dashboard-pages.spec.ts
Normal file
366
tests/e2e/06-dashboard-pages.spec.ts
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
import { test, expect, 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Fetch holder data from GraphQL
|
||||
*/
|
||||
async function fetchHolder(request: APIRequestContext, address: string) {
|
||||
const response = await request.post(STACK_GRAPHQL_URL, {
|
||||
data: {
|
||||
query: `query { holders(address: "${address.toLowerCase()}") { address balance } }`,
|
||||
},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
const payload = await response.json();
|
||||
return payload?.data?.holders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch active positions for an owner from GraphQL
|
||||
*/
|
||||
async function fetchPositions(request: APIRequestContext, owner: string) {
|
||||
const response = await request.post(STACK_GRAPHQL_URL, {
|
||||
data: {
|
||||
query: `query {
|
||||
positionss(where: { owner: "${owner.toLowerCase()}", status: "Active" }, limit: 5) {
|
||||
items { id owner taxRate kraikenDeposit status share }
|
||||
}
|
||||
}`,
|
||||
},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
const payload = await response.json();
|
||||
return payload?.data?.positionss?.items ?? [];
|
||||
}
|
||||
|
||||
test.describe('Dashboard Pages', () => {
|
||||
test.beforeAll(async ({ request }) => {
|
||||
await validateStackHealthy(STACK_CONFIG);
|
||||
|
||||
// Wait for ponder to index positions created by earlier tests (01-05).
|
||||
// Ponder runs in realtime mode but may lag a few seconds behind the chain.
|
||||
const maxWaitMs = 30_000;
|
||||
const pollMs = 1_000;
|
||||
const start = Date.now();
|
||||
let found = false;
|
||||
while (Date.now() - start < maxWaitMs) {
|
||||
const positions = await fetchPositions(request, ACCOUNT_ADDRESS);
|
||||
if (positions.length > 0) {
|
||||
found = true;
|
||||
console.log(`[TEST] Ponder has ${positions.length} positions after ${Date.now() - start}ms`);
|
||||
break;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, pollMs));
|
||||
}
|
||||
if (!found) {
|
||||
console.log('[TEST] WARNING: No positions found in ponder after 30s — tests may fail');
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('Wallet Dashboard', () => {
|
||||
test('renders wallet page with balance and protocol stats', async ({ browser }) => {
|
||||
const context = await createWalletContext(browser, {
|
||||
privateKey: ACCOUNT_PRIVATE_KEY,
|
||||
rpcUrl: STACK_RPC_URL,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${STACK_WEBAPP_URL}/app/#/wallet/${ACCOUNT_ADDRESS}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should show the address (truncated)
|
||||
const addressText = await page.textContent('body');
|
||||
expect(addressText).toContain(ACCOUNT_ADDRESS.slice(0, 6));
|
||||
|
||||
// Should show KRK balance (non-zero after test 01 mints + swaps)
|
||||
const balanceEl = page.locator('text=/\\d+.*KRK/i').first();
|
||||
await expect(balanceEl).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Should show ETH backing card
|
||||
const ethBacking = page.locator('text=/ETH Backing/i').first();
|
||||
await expect(ethBacking).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Should show floor value card
|
||||
const floorValue = page.locator('text=/Floor Value/i').first();
|
||||
await expect(floorValue).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Should show protocol health metrics
|
||||
const ethReserve = page.locator('text=/ETH Reserve/i').first();
|
||||
await expect(ethReserve).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({
|
||||
path: 'test-results/dashboard-wallet.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// No console errors
|
||||
const realErrors = errors.filter(
|
||||
e => !e.includes('favicon') && !e.includes('DevTools')
|
||||
);
|
||||
expect(realErrors).toHaveLength(0);
|
||||
|
||||
console.log('[TEST] ✅ Wallet dashboard renders correctly');
|
||||
} finally {
|
||||
await page.close();
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('wallet page shows staking positions when they exist', async ({ browser, request }) => {
|
||||
// First verify positions exist (created by test 01)
|
||||
const positions = await fetchPositions(request, ACCOUNT_ADDRESS);
|
||||
console.log(`[TEST] Found ${positions.length} positions for ${ACCOUNT_ADDRESS}`);
|
||||
|
||||
if (positions.length === 0) {
|
||||
console.log('[TEST] ⚠️ No positions found — skipping position list check');
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const context = await createWalletContext(browser, {
|
||||
privateKey: ACCOUNT_PRIVATE_KEY,
|
||||
rpcUrl: STACK_RPC_URL,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await page.goto(`${STACK_WEBAPP_URL}/app/#/wallet/${ACCOUNT_ADDRESS}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should show position entries with links to position detail
|
||||
const positionLink = page.locator(`a[href*="/position/"]`).first();
|
||||
await expect(positionLink).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
console.log('[TEST] ✅ Wallet dashboard shows staking positions');
|
||||
} finally {
|
||||
await page.close();
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('wallet page handles unknown address gracefully', async ({ browser }) => {
|
||||
const context = await createWalletContext(browser, {
|
||||
privateKey: ACCOUNT_PRIVATE_KEY,
|
||||
rpcUrl: STACK_RPC_URL,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
try {
|
||||
// Navigate to a wallet with no balance
|
||||
const unknownAddr = '0x0000000000000000000000000000000000000001';
|
||||
await page.goto(`${STACK_WEBAPP_URL}/app/#/wallet/${unknownAddr}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Page should render without crashing
|
||||
const body = await page.textContent('body');
|
||||
expect(body).toBeTruthy();
|
||||
|
||||
// Should show zero or empty state (not crash)
|
||||
const realErrors = errors.filter(
|
||||
e => !e.includes('favicon') && !e.includes('DevTools')
|
||||
);
|
||||
expect(realErrors).toHaveLength(0);
|
||||
|
||||
console.log('[TEST] ✅ Wallet page handles unknown address gracefully');
|
||||
} finally {
|
||||
await page.close();
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Position Dashboard', () => {
|
||||
test('renders position page with valid position data', async ({ browser, request }) => {
|
||||
// Find a real position ID from GraphQL
|
||||
const positions = await fetchPositions(request, ACCOUNT_ADDRESS);
|
||||
console.log(`[TEST] Found ${positions.length} positions`);
|
||||
|
||||
if (positions.length === 0) {
|
||||
console.log('[TEST] ⚠️ No positions found — skipping');
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const positionId = positions[0].id;
|
||||
console.log(`[TEST] Testing position #${positionId}`);
|
||||
|
||||
const context = await createWalletContext(browser, {
|
||||
privateKey: ACCOUNT_PRIVATE_KEY,
|
||||
rpcUrl: STACK_RPC_URL,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${STACK_WEBAPP_URL}/app/#/position/${positionId}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should show position ID
|
||||
const body = await page.textContent('body');
|
||||
expect(body).toContain(positionId);
|
||||
|
||||
// Should show deposit amount
|
||||
const deposited = page.locator('text=/Deposited/i').first();
|
||||
await expect(deposited).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Should show current value
|
||||
const currentValue = page.locator('text=/Current Value/i').first();
|
||||
await expect(currentValue).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Should show tax paid
|
||||
const taxPaid = page.locator('text=/Tax Paid/i').first();
|
||||
await expect(taxPaid).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Should show net return
|
||||
const netReturn = page.locator('text=/Net Return/i').first();
|
||||
await expect(netReturn).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Should show tax rate
|
||||
const taxRate = page.locator('text=/Tax Rate/i').first();
|
||||
await expect(taxRate).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Should show snatch risk indicator
|
||||
const snatchRisk = page.locator('text=/Snatch Risk/i').first();
|
||||
await expect(snatchRisk).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Should show daily tax cost
|
||||
const dailyTax = page.locator('text=/Daily Tax/i').first();
|
||||
await expect(dailyTax).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Should show owner link to wallet page
|
||||
const ownerLink = page.locator('a[href*="/wallet/"]').first();
|
||||
await expect(ownerLink).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({
|
||||
path: 'test-results/dashboard-position.png',
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// No console errors
|
||||
const realErrors = errors.filter(
|
||||
e => !e.includes('favicon') && !e.includes('DevTools')
|
||||
);
|
||||
expect(realErrors).toHaveLength(0);
|
||||
|
||||
console.log('[TEST] ✅ Position dashboard renders correctly');
|
||||
} finally {
|
||||
await page.close();
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('position page handles non-existent position gracefully', async ({ browser }) => {
|
||||
const context = await createWalletContext(browser, {
|
||||
privateKey: ACCOUNT_PRIVATE_KEY,
|
||||
rpcUrl: STACK_RPC_URL,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${STACK_WEBAPP_URL}/app/#/position/999999999`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should show "not found" state without crashing
|
||||
const body = await page.textContent('body');
|
||||
expect(body).toBeTruthy();
|
||||
// Look for not-found or error messaging
|
||||
const hasNotFound = body?.toLowerCase().includes('not found') ||
|
||||
body?.toLowerCase().includes('no position') ||
|
||||
body?.toLowerCase().includes('does not exist');
|
||||
expect(hasNotFound).toBeTruthy();
|
||||
|
||||
const realErrors = errors.filter(
|
||||
e => !e.includes('favicon') && !e.includes('DevTools')
|
||||
);
|
||||
expect(realErrors).toHaveLength(0);
|
||||
|
||||
console.log('[TEST] ✅ Position page handles non-existent ID gracefully');
|
||||
} finally {
|
||||
await page.close();
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('position page links back to wallet dashboard', async ({ browser, request }) => {
|
||||
const positions = await fetchPositions(request, ACCOUNT_ADDRESS);
|
||||
if (positions.length === 0) {
|
||||
console.log('[TEST] ⚠️ No positions — skipping');
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const positionId = positions[0].id;
|
||||
const context = await createWalletContext(browser, {
|
||||
privateKey: ACCOUNT_PRIVATE_KEY,
|
||||
rpcUrl: STACK_RPC_URL,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await page.goto(`${STACK_WEBAPP_URL}/app/#/position/${positionId}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Click owner link → should navigate to wallet page
|
||||
const ownerLink = page.locator('a[href*="/wallet/"]').first();
|
||||
await expect(ownerLink).toBeVisible({ timeout: 10_000 });
|
||||
await ownerLink.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Should now be on the wallet page
|
||||
expect(page.url()).toContain('/wallet/');
|
||||
|
||||
console.log('[TEST] ✅ Position → Wallet navigation works');
|
||||
} finally {
|
||||
await page.close();
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -119,7 +119,7 @@ async function main() {
|
|||
|
||||
// Marcus stakes at LOW tax rate (index 2 = 5% yearly)
|
||||
console.log('\n Creating Marcus position (LOW tax)...');
|
||||
const marcusAmount = ethers.parseEther('300');
|
||||
const marcusAmount = ethers.parseEther('500');
|
||||
const marcusTaxRate = 2; // 5% yearly (0.0137% daily)
|
||||
|
||||
const marcusKrk = krk.connect(marcus) as typeof krk;
|
||||
|
|
@ -132,7 +132,7 @@ async function main() {
|
|||
let nonce = await provider.getTransactionCount(marcus.address);
|
||||
let snatchTx = await marcusStake.snatch(marcusAmount, marcus.address, marcusTaxRate, [], { nonce });
|
||||
let receipt = await snatchTx.wait();
|
||||
console.log(` ✓ Marcus position created (300 KRK @ 5% tax)`);
|
||||
console.log(` ✓ Marcus position created (500 KRK @ 5% tax)`);
|
||||
|
||||
// Sarah stakes at MEDIUM tax rate (index 10 = 60% yearly)
|
||||
console.log('\n Creating Sarah position (MEDIUM tax)...');
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const variants = [
|
|||
{
|
||||
id: 'defensive',
|
||||
name: 'Variant A (Defensive)',
|
||||
url: 'http://localhost:5174/#/',
|
||||
url: 'http://localhost:8081/#/',
|
||||
headline: 'The token that can\'t be rugged.',
|
||||
subtitle: '$KRK has a price floor backed by real ETH. An AI manages it. You just hold.',
|
||||
cta: 'Get $KRK',
|
||||
|
|
@ -46,7 +46,7 @@ const variants = [
|
|||
{
|
||||
id: 'offensive',
|
||||
name: 'Variant B (Offensive)',
|
||||
url: 'http://localhost:5174/#/offensive',
|
||||
url: 'http://localhost:8081/#/offensive',
|
||||
headline: 'The AI that trades while you sleep.',
|
||||
subtitle: 'An autonomous AI agent managing $KRK liquidity 24/7. Capturing alpha. Deepening positions. You just hold and win.',
|
||||
cta: 'Get Your Edge',
|
||||
|
|
@ -55,7 +55,7 @@ const variants = [
|
|||
{
|
||||
id: 'mixed',
|
||||
name: 'Variant C (Mixed)',
|
||||
url: 'http://localhost:5174/#/mixed',
|
||||
url: 'http://localhost:8081/#/mixed',
|
||||
headline: 'DeFi without the rug pull.',
|
||||
subtitle: 'AI-managed liquidity with an ETH-backed floor. Real upside, protected downside.',
|
||||
cta: 'Buy $KRK',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue