This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a monorepo with the following structure:
apps/frontend/- React frontend applicationapps/api/- Backend API (non-JavaScript, coming soon)
This project uses bd (beads) for issue tracking. Run bd onboard to get started.
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --status in_progress # Claim work
bd close <id> # Complete work
bd sync # Sync with gitDo not commit unless explicitly asked. Wait for the user to request a commit before staging or committing changes.
Never amend commits. Always create new commits. Only use --amend if the user explicitly asks for it.
When ending a work session, complete ALL steps below. Work is NOT complete until git push succeeds.
- File issues for remaining work - Create issues for anything that needs follow-up
- Run quality gates (if code changed) - Tests, linters, builds
- Update issue status - Close finished work, update in-progress items
- Push to remote:
git pull --rebase bd sync git push git status # MUST show "up to date with origin" - Hand off - Provide context for next session
Rules:
- Work is NOT complete until
git pushsucceeds - NEVER stop before pushing - that leaves work stranded locally
- If push fails, resolve and retry until it succeeds
All commands run from apps/frontend/:
cd apps/frontend
npm run build # TypeScript check + production build
npm run lint # ESLint
npm test # Unit tests
npm test -- path/to/file # Single test file
npm run test:integration # Integration tests (requires Anvil running)
npm run anvil # Start Anvil fork (requires SEPOLIA_RPC_URL in .env)
npm run storybook # Component explorer at http://localhost:6006
npm run build-storybook # Verify stories render after component changesDev server runs at http://localhost:5173.
Plether is a DeFi frontend for trading plDXY-BEAR and plDXY-BULL tokens on Ethereum.
- Framework: Vite + React 19 + TypeScript
- Web3: wagmi + viem + Web3Modal (WalletConnect)
- Styling: Tailwind CSS v4 (CSS-first config in
apps/frontend/src/index.css) - State: Zustand for local state, TanStack Query for server state
- Error Handling: better-result for typed Result-based error handling
apps/frontend/src/pages/- Route components (Dashboard, Mint, Stake, History)apps/frontend/src/components/ui/- Reusable UI primitivesapps/frontend/src/hooks/- Contract interaction hooks returningResult<T, Error>typesapps/frontend/src/stores/- Zustand stores (transactions, settings)apps/frontend/src/contracts/- ABIs and addresses (mainnet + sepolia)apps/frontend/src/utils/errors.ts- TaggedError definitions for transaction errors
One exported component per file. Private helper components used only within the same file are acceptable, but any component intended for reuse must have its own file. Import components via barrel exports (../components/ui) rather than direct file paths.
All async operations (especially contract interactions) return Result<T, E> from better-result:
import { Result } from 'better-result'
import { parseTransactionError, type TransactionError } from '../utils/errors'
async function doThing(): Promise<Result<Hash, TransactionError>> {
return Result.tryPromise({
try: () => someAsyncOp(),
catch: (err) => parseTransactionError(err)
})
}
// Checking results - use STATIC methods:
Result.isOk(result) // ✅ correct
Result.isError(result) // ✅ correct (not isErr)
result.isOk() // ❌ wrong - these don't existError types are defined as TaggedErrors in apps/frontend/src/utils/errors.ts:
UserRejectedError- User cancelled transactionInsufficientFundsError- Gas, token, or allowance issuesContractRevertError- Contract execution failedNetworkError,TimeoutError,UnknownTransactionError
- Approvals: Always use exact amount approvals, never unlimited
- Networks: Mainnet (1), Sepolia (11155111), Anvil local fork (31337)
- Slippage: Max 1% (protocol limit), stored in settingsStore
Never trust code comments about scales. Always verify against actual on-chain values.
When writing formulas with token amounts, oracle prices, or protocol values:
- Query the actual value via RPC (
eth_call) to determine its magnitude empirically - Label each operand's decimals: e.g.,
collateral(18) * oraclePrice(36) * lltv(18) - Compute the divisor:
divisor = sum_of_input_decimals - desired_output_decimals- Example:
18 + 36 - 6 = 48→ divide by10^48to get USDC (6 dec)
- Example:
- Sanity check: plug in real values and verify the output is plausible
Reference scales for this project:
| Value | Scale | How to verify |
|---|---|---|
| ERC20 amount | decimals() (USDC=6, tokens=18) |
balanceOf() |
| Morpho oracle price | 10^36 | price() on the oracle |
| Morpho LLTV | 10^18 | marketParams.lltv |
See API.md for the complete protocol API reference including all contract functions, parameters, and error types.
See APIERRORS.md for all contract error selectors. Use this to:
- Decode revert errors: When a contract call fails with a custom error (e.g.,
0x50285b92), look up the selector to find the error name and meaning - Ensure comprehensive error handling: When implementing new features, review relevant contract errors to handle all possible failure cases with appropriate user messages
- Add ABI to
apps/frontend/src/contracts/abis/ - Add address to
apps/frontend/src/contracts/addresses.ts(mainnet + sepolia) - Create hook in
apps/frontend/src/hooks/using wagmi'suseReadContract/useWriteContract - Return
Result<T, TransactionError>from async operations
The apps/frontend/src/api/ directory contains the typed client for the Plether backend API. This layer provides:
- Aggregated data fetching - Single API call returns all dashboard data
- Server-side caching - Reduces RPC costs and improves load times
- Real-time updates - WebSocket connection for price streaming
- Transaction history - Event-indexed historical data
See specs/backend-api.md for the full API specification.
Usage:
// React Query hooks (preferred for components)
import { useUserDashboard, useProtocolStatus, useMintQuote } from '../api';
function Dashboard() {
const { data: protocol } = useProtocolStatus();
const { data: user } = useUserDashboard(address);
const { data: quote } = useMintQuote(amount);
}
// Direct client (for non-React code)
import { plethApi } from '../api';
const result = await plethApi.getUserDashboard(address);
if (Result.isOk(result)) {
console.log(result.value.data);
}Migration Strategy:
- Read operations: Migrate to API hooks (when backend is deployed)
- Write operations: Keep using wagmi hooks (transactions stay client-side)
- Approvals: Keep using wagmi hooks (user wallet interaction required)
Defined in apps/frontend/src/index.css via @theme:
cyber-neon-green(#00FF99) - Primary accent, plDXY-BULLcyber-electric-fuchsia(#FF00CC) - plDXY-BEAR, secondary actionsbear/bull- Aliases for token-specific styling
- Never use dollar sign ($) to represent USDC values
- Use
formatUsd()fromapps/frontend/src/utils/formatters.tswhich formats numbers without $ - Append "USDC" suffix where appropriate (e.g., "100.00 USDC" not "$100.00")
- Always use 2 decimal places for USDC values
- Values less than 0.01 but greater than 0 display as "<0.01 USDC"
Test Types:
- Unit (
*.test.ts) - Pure functions, stores, hooks with mocked wagmi - Component (Storybook + play functions) - UI interactions
- Integration (
*.integration.test.ts) - Real contracts via Anvil - E2E (
apps/frontend/e2e/*.spec.ts) - Critical user journeys
Commands:
cd apps/frontend
npm test # Unit tests
npm run test:integration # Integration tests (requires: npm run anvil)
npm run test:e2e # E2E tests
npm run test:e2e:ui # E2E with Playwright UIWhen to Write Each Type:
- Pure function → Unit test
- Zustand store → Unit test
- Hook with wagmi → Unit test (mock wagmi) + Integration test (real contracts)
- Component with interactions → Storybook story + play function
- Multi-page user flow → E2E (only critical paths)
Required Patterns:
- Mock wagmi BEFORE importing hooks (hoisting matters)
- Use
Result.isOk(result)/Result.isError(result)(static methods, not instance) - Use
createTestWrapper()for hooks needing React context - Reset Zustand stores in
beforeEach - Skip integration tests gracefully if contracts not deployed
Example - Hook Unit Test:
const mockWriteContract = vi.fn()
vi.mock('wagmi', () => ({
useWriteContract: () => ({ writeContract: mockWriteContract, ... }),
}))
import { useMyHook } from '../useMyHook' // Import AFTER mock
beforeEach(() => {
vi.resetAllMocks()
useTransactionStore.setState({ pendingTransactions: [] })
})Test File Locations:
- Unit:
apps/frontend/src/**/__tests__/*.test.{ts,tsx} - Integration:
apps/frontend/src/**/__tests__/*.integration.test.{ts,tsx} - E2E:
apps/frontend/e2e/tests/*.spec.ts - Stories:
apps/frontend/src/stories/*.stories.tsx
General Rules:
- Tests live in
__tests__/directories adjacent to code - Never reimplement application logic in tests - import and test actual functions
- Design for testability using "functional core, imperative shell": keep pure business logic in
apps/frontend/src/utils/separate from IO code (hooks, API calls)
- For interactive stories, use
playfunction withstep()for named steps - Import from
storybook/test(Storybook 10+)
IMMEDIATELY after implementing any front-end change:
- Identify what changed - Review the modified components/pages
- Navigate to affected pages - Use
mcp__playwright__browser_navigateto visit each changed view - Verify design compliance - Compare against
/context/design-principles.md - Validate feature implementation - Ensure the change fulfills the user's specific request
- Check acceptance criteria - Review any provided context files or requirements
- Capture evidence - Take full page screenshot at desktop viewport (1440px) of each changed view
- Check for errors - Run
mcp__playwright__browser_console_messages⚠️
This verification ensures changes meet design standards and user requirements.
// Navigation & Screenshots
mcp__playwright__browser_navigate(url); // Navigate to page
mcp__playwright__browser_take_screenshot(); // Capture visual evidence
mcp__playwright__browser_resize(
width,
height
); // Test responsiveness
// Interaction Testing
mcp__playwright__browser_click(element); // Test clicks
mcp__playwright__browser_type(
element,
text
); // Test input
mcp__playwright__browser_hover(element); // Test hover states
// Validation
mcp__playwright__browser_console_messages(); // Check for errors
mcp__playwright__browser_snapshot(); // Accessibility check
mcp__playwright__browser_wait_for(
text / element
); // Ensure loadingWhen implementing UI features, verify:
- Visual Hierarchy: Clear focus flow, appropriate spacing
- Consistency: Uses design tokens, follows patterns
- Responsiveness: Works on mobile (375px), tablet (768px), desktop (1440px)
- Accessibility: Keyboard navigable, proper contrast, semantic HTML
- Performance: Fast load times, smooth animations (150-300ms)
- Error Handling: Clear error states, helpful messages
- Polish: Micro-interactions, loading states, empty states
To test with a connected wallet state, inject the mock wallet before navigating. The mock uses EIP-6963 for auto-connection.
// Step 1: Inject mock wallet with EIP-6963 (run ONCE per browser session)
mcp__playwright__browser_run_code({
code: `async (page) => {
await page.addInitScript(() => {
const MOCK_ADDRESS = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
const CHAIN_ID = '0xaa36a7';
const mockProvider = {
isMetaMask: true,
selectedAddress: MOCK_ADDRESS,
chainId: CHAIN_ID,
networkVersion: '11155111',
_metamask: { isUnlocked: () => Promise.resolve(true) },
request: async ({ method }) => {
if (method === 'eth_requestAccounts' || method === 'eth_accounts') return [MOCK_ADDRESS];
if (method === 'eth_chainId') return CHAIN_ID;
if (method === 'eth_getBalance') return '0x8ac7230489e80000';
if (method === 'eth_call') return '0x' + '0'.repeat(64);
return null;
},
on: (event, cb) => {
if (event === 'accountsChanged') setTimeout(() => cb([MOCK_ADDRESS]), 100);
if (event === 'chainChanged') setTimeout(() => cb(CHAIN_ID), 100);
},
removeListener: () => {},
};
Object.defineProperty(window, 'ethereum', { value: mockProvider, writable: false });
// EIP-6963: Auto-announce as MetaMask
const announce = () => window.dispatchEvent(new CustomEvent('eip6963:announceProvider', {
detail: Object.freeze({
info: { uuid: 'mock', name: 'MetaMask', icon: '', rdns: 'io.metamask' },
provider: mockProvider
})
}));
window.addEventListener('eip6963:requestProvider', announce);
announce();
setTimeout(announce, 100);
});
return 'Mock wallet ready';
}`
});
// Step 2: Navigate to page (wallet will auto-connect via EIP-6963)
mcp__playwright__browser_navigate({ url: "http://localhost:5173" });Full implementation: apps/frontend/e2e/fixtures/mockWallet.ts
Screenshots for documentation live in screenshots/. Use consistent settings:
Resolution: 1024x1000 pixels (set with mcp__playwright__browser_resize)
Setup for Real Transactions:
- Move
apps/frontend/src/contracts/addresses.local.jsonto.bakso app uses Sepolia addresses - Change wagmi config to use port 8546 (
http://127.0.0.1:8546) - Use
MOCK_WALLET_ANVIL_SCRIPTfromapps/frontend/e2e/fixtures/mockWallet.tswhich proxies to Anvil - Start Anvil fork:
cd apps/frontend && npm run anvil(uses port 8546 with Sepolia fork)
Key Lessons:
addInitScriptonly runs on NEW page loads - close browser and reopen to apply changes- Hide TanStack devtools before screenshots:
document.querySelector('.tsqd-open-btn-container').style.display = 'none' - Mock wallet needs
eth_sendTransactionhandler to proxy transactions to Anvil - Anvil test account
0xf39F...2266is pre-funded and unlocked for direct tx sending - Screenshots save to
.playwright-mcp/- copy toscreenshots/directory - Always verify dimensions with
file screenshots/*.pngfor consistency
Workflow:
# 1. Setup
mv apps/frontend/src/contracts/addresses.local.json apps/frontend/src/contracts/addresses.local.json.bak
# Edit wagmi.ts: change anvil port to 8546
# 2. Take screenshots with Playwright MCP
mcp__playwright__browser_resize(1024, 1000)
mcp__playwright__browser_run_code(MOCK_WALLET_ANVIL_SCRIPT)
mcp__playwright__browser_navigate("http://localhost:5173/mint")
# ... interact and screenshot ...
# 3. Cleanup
cp .playwright-mcp/*.png screenshots/
mv apps/frontend/src/contracts/addresses.local.json.bak apps/frontend/src/contracts/addresses.local.json
# Revert wagmi.ts port changeSource code for dependencies is available in opensrc/ for deeper understanding of implementation details. Use this when you need to understand how a package works internally, not just its types/interface.
See opensrc/sources.json for the list of available packages and their versions.
Fetching Additional Source Code:
npx opensrc <package> # npm package (e.g., npx opensrc zod)
npx opensrc pypi:<package> # Python package (e.g., npx opensrc pypi:requests)
npx opensrc crates:<package> # Rust crate (e.g., npx opensrc crates:serde)
npx opensrc <owner>/<repo> # GitHub repo (e.g., npx opensrc vercel/ai)