Skip to content

Instantly share code, notes, and snippets.

@roninjin10
Last active January 29, 2026 20:02
Show Gist options
  • Select an option

  • Save roninjin10/a476298eed92feddb69910017dc4b1c7 to your computer and use it in GitHub Desktop.

Select an option

Save roninjin10/a476298eed92feddb69910017dc4b1c7 to your computer and use it in GitHub Desktop.
Uniswap voltaire-effect demo

Uniswap V2 Indexer - Project Prompt

Build a production-ready Uniswap V2 indexer using Effect.ts with a React frontend.

Overview

An Ethereum indexer that tracks Uniswap V2 swaps, pairs, and reserves in real-time, stores them in SQLite, serves them via HTTP API, and displays them in a React UI.

Tech Stack (Required)

Component Library Notes
Runtime Bun Use bun:test for testing
Core effect Services, Layers, Streams, Config
HTTP Server @effect/platform-bun JSON API endpoints
Database @effect/sql-sqlite-bun SQLite storage
Ethereum voltaire-effect RPC types (but see notes below)
Frontend State effect-atom + @effect-atom/atom-react useAtomValue, useAtomSet hooks
Frontend React 19 + Vite Bundled separately

Architecture

┌─────────────────────────────────────────────────────────────┐
│                        index.ts                              │
│  - Loads config from env (ALCHEMY_API_KEY, PORT, DB_PATH)   │
│  - Composes layers                                           │
│  - Runs HTTP server + indexer                                │
└─────────────────────────────────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
┌───────────────┐    ┌───────────────┐    ┌───────────────┐
│ ApiServer     │    │ Indexer       │    │ AppConfig     │
│ /api/pairs    │    │ backfill()    │    │ rpcUrls[]     │
│ /api/swaps    │    │ start()       │    │ port          │
│ /api/status   │    │ blockStream   │    │ dbPath        │
└───────────────┘    └───────────────┘    └───────────────┘
        │                     │
        └──────────┬──────────┘
                   ▼
        ┌───────────────────┐
        │ Repositories      │
        │ PairRepository    │
        │ SwapRepository    │
        │ IndexerState      │
        └───────────────────┘
                   │
                   ▼
        ┌───────────────────┐
        │ SQLite Database   │
        │ @effect/sql       │
        └───────────────────┘

Phase 1: Indexer Core (Backend Only)

Success Criteria

# Start indexer and backfill last 100 blocks
bun run start --index --from=24340000

# In another terminal, query the API
curl http://localhost:3000/api/status
# Returns: {"lastProcessedBlock":24340100,"isRunning":true}

curl http://localhost:3000/api/swaps?limit=5
# Returns: Array of real Uniswap swaps from mainnet

bun test
# All tests pass

Deliverables

  1. Config Layer (src/config/AppConfig.ts)

    • Read from env: ALCHEMY_API_KEY (optional), PORT (default 3000), DB_PATH
    • Build rpcUrls[]: Alchemy first if key exists, then public fallbacks
    • Public RPCs: https://eth.llamarpc.com, https://rpc.ankr.com/eth
  2. Provider Service (src/services/ProviderService.ts)

    • getBlockNumber(): Effect<bigint, RpcError>
    • getBlock(n): Effect<Block, RpcError>
    • getLogs({fromBlock, toBlock, topics}): Effect<Log[], RpcError>
    • blockStream(from): Stream<Block, RpcError>
    • RPC Fallback: Try URLs in sequence until one succeeds
    • NO MOCK in main code - mock only in test layer
  3. Database (src/db/)

    • Schema: pairs, swaps, indexer_state tables
    • Repositories as Effect Context.Tag services
    • Return Effect<T, SqlError.SqlError>
  4. Indexer (src/indexer/)

    • Event topics (hardcoded - don't compute at runtime):
      Swap: "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822"
      Sync: "0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1"
      PairCreated: "0x0d3648bd0f6ba80134a33ba9275ac585d9d315f0ad8355cddefde31afa28d0e9"
    • backfill(from, to): Batch process historical blocks (100 blocks per getLogs call)
    • start(from?): Backfill to head, then stream new blocks
    • Decode events manually (slice hex data, no ABI library needed)
  5. HTTP API (src/http/ApiServer.ts)

    • GET /health{ok: true}
    • GET /api/pairs → All pairs
    • GET /api/pairs/:id → Single pair (404 if not found)
    • GET /api/swaps?limit=50 → Recent swaps
    • GET /api/swaps/:pairId → Swaps for pair
    • GET /api/status{lastProcessedBlock, updatedAt, isRunning}
    • CORS headers for frontend dev
  6. Tests (tests/)

    • Use :memory: SQLite for repository tests
    • Create ProviderServiceMock layer for indexer tests (only in test files)
    • Test event decoding with real log data samples

File Structure

├── src/
│   ├── config/AppConfig.ts
│   ├── db/
│   │   ├── schema.ts
│   │   └── repository.ts
│   ├── indexer/
│   │   ├── events.ts        # Topic hashes + decoders
│   │   ├── BlockProcessor.ts
│   │   └── Indexer.ts
│   ├── http/ApiServer.ts
│   ├── services/
│   │   ├── DatabaseService.ts
│   │   └── ProviderService.ts
│   └── types/index.ts
├── tests/
│   ├── repository.test.ts
│   └── indexer.test.ts
├── index.ts
├── package.json
└── .env.example

Phase 2: React Frontend

Success Criteria

# Terminal 1: Backend
bun run start --index --from=24340000

# Terminal 2: Frontend
bun run dev:ui
# Open http://localhost:5173
# See: Status bar (green "Connected", block number updating)
# See: Trading Pairs table with real pairs
# See: Recent Swaps table with real swaps
# Click pair → Price panel shows reserves

Deliverables

  1. Vite Setup (vite.config.ts, web/)

    • Proxy /api/* to backend at localhost:3000
    • React 19 with @vitejs/plugin-react
  2. Atoms (src/frontend/atoms.ts)

    • pairsAtom: Atom<Pair[]>
    • recentSwapsAtom: Atom<Swap[]>
    • selectedPairIdAtom: Atom<string | null>
    • indexerStatusAtom: Atom<{isRunning, lastBlock, error}>
    • isLoadingAtom: Atom<boolean>
  3. App Component (src/frontend/App.tsx)

    • Use useAtomValue / useAtomSet from @effect-atom/atom-react
    • Wrap app in <RegistryProvider>
    • Fetch from /api/* on mount
    • Poll /api/status every 5 seconds
  4. UI Components

    • Status bar: Connection indicator, last block
    • Pairs table: Clickable rows, show reserves
    • Swaps table: Time, pair, buy/sell, amount, etherscan link
    • Price panel: Shows when pair selected

File Structure (additions)

├── web/
│   ├── index.html
│   └── main.tsx          # RegistryProvider + App
├── src/frontend/
│   ├── atoms.ts
│   └── App.tsx
├── vite.config.ts

Phase 3: Production Polish

Success Criteria

  • Indexer recovers from RPC errors without crashing
  • Can backfill 10,000+ blocks without memory issues
  • Logs are structured and useful

Deliverables

  1. Structured Logging

    • Use Effect's logging with timestamps
    • Log: blocks processed, errors, RPC fallbacks
  2. Error Recovery

    • Retry failed RPC calls with exponential backoff
    • On persistent failure, log and continue to next block
    • Never crash the HTTP server
  3. Graceful Shutdown

    • Handle SIGINT/SIGTERM
    • Commit current block before exit
  4. Performance

    • Batch inserts for swaps (insert many per transaction)
    • Index database columns used in queries

Reference Data

Uniswap V2 Addresses (Mainnet)

Factory: 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
Deployed at block: 10000835

Sample Log Data (for tests)

// Real Swap log from mainnet
const SAMPLE_SWAP_LOG = {
  address: "0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852",
  topics: [
    "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822",
    "0x0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad",
    "0x0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad"
  ],
  data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000000000000",
  blockNumber: "0x173b5e7",
  transactionHash: "0x...",
  logIndex: "0x42"
}

Public RPCs (in priority order)

1. https://eth-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}  (if key set)
2. https://eth.llamarpc.com
3. https://rpc.ankr.com/eth
4. https://ethereum.publicnode.com

Anti-Patterns to Avoid

  1. NO mocks in production code - ProviderServiceMock belongs in tests/ only
  2. NO dynamic topic computation - Hardcode the hex strings
  3. NO ABI decoding libraries - Slice hex manually, it's simple enough
  4. NO useState in frontend - Use effect-atom throughout
  5. NO polling from indexer - Use proper block streaming
  6. NO any types - Everything typed with Effect's error channel

Commands

# Development
bun install
bun run start                        # API server only
bun run start --index                # API + indexer from last block
bun run start --index --from=24340000  # API + indexer from specific block
bun run dev:ui                       # Frontend dev server

# Testing
bun test                             # All tests
bun run typecheck                    # TypeScript check

# Production
bun run build:ui                     # Build frontend
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment