## 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
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:
- Wagmi detects wallet's current chain via
watchChainId() useWalletexposesaccount.chainIdfrom wagmi state- Components read
account.chainIdand pass to composables - Composables watch for chain changes independently and reload data
ChainConfigServicemaps 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 changeswatchChainId()- 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 selectiontaxRateIndex- Maximum tax rate index to snatchchainId- Optional chain ID Dependencies:usePositions,useStatCollection,useStakeExports: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
initialChainIdto 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-readableformatBigIntDivision(a, b)- Safe BigInt divisioncompactNumber(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 IDDEFAULT_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 messagesclearXxxRetryTimer()- Cancel pending retryscheduleXxxRetry()- Queue retry with exponential backoffresolveGraphqlEndpoint()- 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 checkingnpm run preview- Preview production buildnpm 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
- Connect wallet (WalletConnect / Coinbase Wallet)
- Switch chains and verify data reloads
- Stake tokens with different tax rates
- Snatch positions with higher tax rate
- Adjust tax rate on existing position
- Pay tax manually
- Unstake position
- Verify GraphQL error retry behavior (kill Ponder, observe retries)
Quality Guidelines
- Composables: Accept
chainIdparameter instead of importing wallet state directly - Watchers: Each composable maintains its own
watchChainId()listener for independence - Error States: Always expose
xxxErrorandloadingrefs 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
- Don't import
chainDatafromuseWalletin composables - passchainIdexplicitly - Don't forget to pass
chainIdtoloadPositions()/loadStats()on manual refresh - Don't call composables conditionally - they must run on every component render
- Don't forget to clear timers/watchers in
onUnmounted() - 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