harb/web-app/AGENTS.md
johba e5e1308e72 refactor: consolidate CI and local dev orchestration (#108)
## Summary
- Extract shared bootstrap functions into `scripts/bootstrap-common.sh` (eliminates ~120 lines of duplicated forge/cast commands from e2e.yml)
- Create reusable `scripts/wait-for-service.sh` for health checks (replaces 60-line inline wait-for-stack)
- Merge dev and CI entrypoints into unified scripts branching on `CI` env var (delete `docker/ci-entrypoints/`)
- Replace 4 per-service CI Dockerfiles with parameterized `docker/Dockerfile.service-ci`
- Add `sync-tax-rates.mjs` to CI image builder stage
- Fix: CI now grants txnBot recenter access (was missing)
- Fix: txnBot funding parameterized (CI=10eth, local=1eth)
- Delete 5 obsolete migration docs and 4 DinD integration files

Net: -1540 lines removed

Closes #107

## Test plan
- [ ] E2E pipeline passes (bootstrap sources shared script, services use old images with commands override)
- [ ] build-ci-images pipeline builds all 4 services with unified Dockerfile
- [ ] Local dev stack boots via `./scripts/dev.sh start`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: openhands <openhands@all-hands.dev>
Reviewed-on: https://codeberg.org/johba/harb/pulls/108
2026-02-03 12:07:28 +01:00

11 KiB

Web App - Agent Guide

Vue 3 + TypeScript staking interface for KRAIKEN, enabling users to stake tokens, manage positions, and interact with Harberger-tax mechanics.

Technology Snapshot

  • Vue 3 (Composition API) with TypeScript and Vite toolchain
  • Wagmi/Viem for wallet connection and blockchain interaction
  • Vue Router for navigation
  • Axios for GraphQL queries to Ponder indexer
  • Sass-based component styling

Architecture Overview

Chain Configuration Service

Location: src/services/chainConfig.ts

Centralizes all endpoint resolution for different blockchain networks. This service eliminates scattered configuration and provides a single source of truth for network-specific URLs.

Key Method:

chainConfigService.getEndpoint(chainId: number, type: 'graphql' | 'rpc' | 'txnBot'): string

Supported Chains:

  • 31337 - Local Anvil fork (development)
  • 84532 - Base Sepolia (testnet)
  • 8453 - Base Mainnet (production)

Usage Pattern:

// Composables receive chainId and resolve endpoints internally
const endpoint = chainConfigService.getEndpoint(chainId, 'graphql');

Benefits:

  • Single source of truth for all endpoint configuration
  • Easy to test composables with different chainIds
  • No hidden global state dependencies
  • Clear error messages when endpoints aren't configured

Wagmi Integration

Configuration: src/wagmi.ts

Wagmi manages wallet connection and tracks the user's active blockchain:

  • Wallet providers: WalletConnect, Coinbase Wallet
  • Supported chains: Local fork (31337), Base Sepolia (84532)
  • State persistence via localStorage

Chain Determination Flow:

  1. Wagmi detects wallet's current chain via watchChainId()
  2. useWallet exposes account.chainId from wagmi state
  3. Components read account.chainId and pass to composables
  4. Composables watch for chain changes independently and reload data
  5. ChainConfigService maps chainId → endpoint URLs

Key Insight: Wagmi is the source of truth for which chain the wallet is on, but composables don't import wallet state directly. They accept chainId as a parameter for better testability and explicit dependencies.

Key Composables

useWallet()

Purpose: Manages wallet connection, balance, and account state Exports: balance, account, loadBalance() Watchers:

  • watchAccount() - Reloads balance when account/chain changes
  • watchChainId() - Reloads balance on chain switch

Note: Also exports chainData computed property for UI metadata (Uniswap links, chain names). This is separate from endpoint resolution, which goes through ChainConfigService.

usePositions(chainId: number)

Purpose: Loads and manages staking positions from Ponder GraphQL Parameters: chainId - Which chain to query positions from Exports: activePositions, myActivePositions, myClosedPositions, tresholdValue, positionsError, loading Key Features:

  • Watches contract events (PositionCreated, PositionRemoved) to auto-refresh
  • Independent watchChainId() listener reloads on chain switch
  • Exponential backoff retry on GraphQL failures (1.5s → 60s max)

Data Flow:

Component (chainId) → usePositions(chainId)
  → resolveGraphqlEndpoint(chainId)
    → chainConfigService.getEndpoint(chainId, 'graphql')
      → axios.post(endpoint, query)

useStatCollection(chainId: number)

Purpose: Loads protocol-wide statistics (total supply, outstanding stake, inflation, etc.) Parameters: chainId - Which chain to query stats from Exports: kraikenTotalSupply, stakeTotalSupply, outstandingStake, profit7d, inflation7d, maxSlots, claimedSlots, statsError, loading Retry Logic: Same exponential backoff as usePositions

useSnatchSelection(demo, taxRateIndex, chainId)

Purpose: Calculates which positions can be "snatched" based on staking amount and tax rate Parameters:

  • demo - Include own positions in selection
  • taxRateIndex - Maximum tax rate index to snatch
  • chainId - Optional chain ID Dependencies: usePositions, useStatCollection, useStake Exports: snatchablePositions, shortfallShares, floorTax, openPositionsAvailable

useStake()

Purpose: Executes staking transactions (stake, snatch-and-stake) Contract Interaction: Calls Stake.sol via wagmi/viem State Management: Tracks transaction states (StakeAble, SignTransaction, Waiting)

useAdjustTaxRate()

Purpose: Provides tax rate options and handles tax rate adjustments Key Data: taxRates array with pre-calculated yearly/daily rates

useUnstake()

Purpose: Handles position exit transactions Contract Interaction: Calls exitPosition() on Stake contract

Key Components

StakeView.vue

Route: /dashboard Purpose: Main staking dashboard showing chart, statistics, active positions Key Features:

  • Passes initialChainId to all composables for consistent data loading
  • Displays chain support status
  • Shows "Connect Wallet" prompt when disconnected

StakeHolder.vue

Purpose: Staking form with accessibility-focused UI Key Elements:

  • Token amount slider with ARIA labels
  • Tax rate selector dropdown
  • Real-time feedback (floor tax, positions buyout count)
  • Action button with state-driven labels (Stake / Snatch and Stake / Sign Transaction / Waiting)

Accessibility:

  • Semantic HTML with proper ARIA attributes
  • Screen reader announcements for dynamic content
  • Keyboard navigation support

Test Hooks (for Playwright):

  • page.getByRole('slider', { name: 'Token Amount' })
  • page.getByLabel('Staking Amount')
  • page.getByLabel('Tax')

ConnectWallet.vue

Purpose: Wallet connection modal with connector selection Features:

  • Shows connected wallet with avatar (blockies) and address
  • Token balance display ($KRK, Staked $KRK)
  • Buy button with chain-specific Uniswap link (uses chainData.uniswap)

ChartComplete.vue

Purpose: Position visualization showing tax rates and snatchable positions Dependencies: usePositions, useStatCollection, useStake

CollapseActive.vue

Purpose: Expandable position card with profit calculation and actions Actions: Pay Tax, Adjust Tax Rate, Unstake

Shared Utilities

src/utils/logger.ts

Structured logging with namespaces (contract, info, error)

src/utils/helper.ts

Common helpers:

  • bigInt2Number(value, decimals) - Convert Wei to human-readable
  • formatBigIntDivision(a, b) - Safe BigInt division
  • compactNumber(n) - Format large numbers (1.5M, 3.2K)

src/config.ts

Chain configuration data:

  • chainsData - Array of chain metadata (id, graphql, contracts, etc.)
  • getChain(id) - Lookup chain by ID
  • DEFAULT_CHAIN_ID - Auto-detected based on environment/hostname

Contract Interfaces

src/contracts/harb.ts

Contract: Kraiken.sol Methods: getMinStake(), getAllowance(), getNonce(), approve() Setup: setHarbContract() updates contract address when chain changes

src/contracts/stake.ts

Contract: Stake.sol Methods: assetsToShares(), getTaxDue(), payTax(positionId) Setup: setStakeContract() updates contract address when chain changes

Retry Logic & Error Handling

Both usePositions and useStatCollection implement exponential backoff retry for GraphQL failures:

Configuration:

  • Base delay: 1.5 seconds
  • Max delay: 60 seconds
  • Backoff multiplier: 2x (1.5s → 3s → 6s → 12s → 24s → 48s → 60s)

Why Retry:

  • Ponder indexer may not be ready during stack startup
  • Temporary network issues shouldn't permanently break UI
  • Chain switching endpoints may have brief response delays

Implementation (duplicated in both files):

  • formatGraphqlError() - Parse Axios errors into user-friendly messages
  • clearXxxRetryTimer() - Cancel pending retry
  • scheduleXxxRetry() - Queue retry with exponential backoff
  • resolveGraphqlEndpoint() - Resolve chainId to GraphQL URL

Refactoring Opportunity: ~41 lines of duplicate code could be extracted to src/utils/graphqlRetry.ts

Development Workflow

Running Locally

Boot the full stack with ./scripts/dev.sh start (see root CLAUDE.md for details)

Targeted Development

  • npm run dev - Vite dev server (assumes Ponder/Anvil already running)
  • npm run build - Production build with type checking
  • npm run preview - Preview production build
  • npm run test:e2e - Playwright E2E tests (from repo root)

Live Reload

Use ./scripts/watch-kraiken-lib.sh to rebuild kraiken-lib on file changes and auto-restart dependent containers

Testing

E2E Tests

Location: tests/e2e/ (repo root) Framework: Playwright Coverage: Complete user journeys (mint ETH → swap KRK → stake) CI: Woodpecker e2e pipeline runs these against pre-built service images

Test Strategy:

  • Use mocked wallet provider with Anvil accounts
  • Tests automatically start/stop the full stack
  • Rely on semantic HTML and ARIA attributes (not private selectors)

Manual Testing Checklist

  1. Connect wallet (WalletConnect / Coinbase Wallet)
  2. Switch chains and verify data reloads
  3. Stake tokens with different tax rates
  4. Snatch positions with higher tax rate
  5. Adjust tax rate on existing position
  6. Pay tax manually
  7. Unstake position
  8. Verify GraphQL error retry behavior (kill Ponder, observe retries)

Quality Guidelines

  • Composables: Accept chainId parameter instead of importing wallet state directly
  • Watchers: Each composable maintains its own watchChainId() listener for independence
  • Error States: Always expose xxxError and loading refs for UI feedback
  • Type Safety: Use strongly typed interfaces for Position, StatsRecord, etc.
  • Accessibility: Use semantic HTML, ARIA attributes, and keyboard navigation
  • Testability: Design components to work with Playwright role/label selectors

Performance Tips

  • Lazy load routes with Vue Router dynamic imports
  • Debounce assetsToShares() calls in StakeHolder (500ms)
  • Use computed() for derived state to avoid recalculation
  • Clear watchers in onUnmounted() to prevent memory leaks
  • Cancel retry timers on unmount or manual actions

Common Pitfalls

  1. Don't import chainData from useWallet in composables - pass chainId explicitly
  2. Don't forget to pass chainId to loadPositions() / loadStats() on manual refresh
  3. Don't call composables conditionally - they must run on every component render
  4. Don't forget to clear timers/watchers in onUnmounted()
  5. Do reset retry delays on successful loads (retryDelayMs.value = BASE_DELAY)

Future Enhancements

  • Extract retry logic to shared utility (src/utils/graphqlRetry.ts)
  • Remove dead code in useChain.ts (entire file is commented out)
  • Clean up commented debug console.log() statements
  • Implement three-way version validation (contract VERSION ↔ Ponder ↔ web-app)
  • Add GraphQL query caching layer for better performance