278 lines
11 KiB
Markdown
278 lines
11 KiB
Markdown
# 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)
|
|
**References**: See `INTEGRATION_TEST_STATUS.md` and `SWAP_VERIFICATION.md`
|
|
|
|
**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
|