harb/web-app/AGENTS.md

279 lines
11 KiB
Markdown
Raw Normal View History

2025-10-11 10:55:49 +00:00
# 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**:
```typescript
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**:
```typescript
// 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
2025-10-11 10:55:49 +00:00
**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