Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Atlas — Claude Code Context

Atlas is an EVM blockchain explorer (indexer + API + frontend) for ev-node based chains.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor grammar and fenced-code-block nit.

Two small documentation quality issues flagged by the linters:

  1. Line 3ev-node based should be hyphenated: ev-node-based chains.
  2. Line 17 — The directory-tree fenced block has no language tag, triggering MD040. Adding text suppresses the warning.
✏️ Proposed fix
-Atlas is an EVM blockchain explorer (indexer + API + frontend) for ev-node based chains.
+Atlas is an EVM blockchain explorer (indexer + API + frontend) for ev-node-based chains.
-```
+```text
 atlas/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Atlas is an EVM blockchain explorer (indexer + API + frontend) for ev-node based chains.
Atlas is an EVM blockchain explorer (indexer + API + frontend) for ev-node-based chains.
🧰 Tools
🪛 LanguageTool

[grammar] ~3-~3: Use a hyphen to join words.
Context: ...r (indexer + API + frontend) for ev-node based chains. ## Tech Stack | Layer | ...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 3, Replace the unhyphenated phrase "ev-node based" with
"ev-node-based" in the sentence "Atlas is an EVM blockchain explorer (indexer +
API + frontend) for ev-node based chains." and add the language tag "text" to
the directory-tree fenced code block (the block beginning with "```" that
contains the atlas/ tree) so the fence becomes "```text" to satisfy MD040.


## Tech Stack

| Layer | Tech |
|---|---|
| Indexer | Rust, tokio, sqlx, alloy, tokio-postgres (binary COPY) |
| API | Rust, Axum, sqlx, tower-http |
| Database | PostgreSQL (partitioned tables) |
| Frontend | React, TypeScript, Vite, Tailwind CSS, Bun |
| Deployment | Docker Compose, nginx (unprivileged, port 8080→80) |

## Repository Layout

```
atlas/
├── backend/
│ ├── Cargo.toml # Workspace — all dep versions live here
│ ├── crates/
│ │ ├── atlas-common/ # Shared types, DB pool, error handling, Pagination
│ │ ├── atlas-indexer/ # Block fetcher, batch writer, metadata fetcher
│ │ └── atlas-api/ # Axum REST API
│ └── migrations/ # sqlx migrations (run at startup by both crates)
├── frontend/
│ ├── src/
│ │ ├── api/ # Typed API clients (axios)
│ │ ├── components/ # Shared UI components
│ │ ├── hooks/ # React hooks (useBlocks, useLatestBlockHeight, …)
│ │ ├── pages/ # One file per page/route
│ │ └── types/ # Shared TypeScript types
│ ├── Dockerfile # Multi-stage: oven/bun:1 → nginx-unprivileged:alpine
│ └── nginx.conf # SPA routing + /api/ reverse proxy to atlas-api:3000
├── docker-compose.yml
└── .env.example
```

## Key Architectural Decisions

### Database connection pools
- **API pool**: 20 connections, `statement_timeout = '10s'` set via `after_connect` hook
- **Indexer pool**: 20 connections (configurable via `DB_MAX_CONNECTIONS`), same timeout
- **Binary COPY client**: separate `tokio-postgres` direct connection (bypasses sqlx pool), conditional TLS based on `sslmode` in DATABASE_URL
- **Migrations**: run with a dedicated 1-connection pool with **no** statement_timeout (index builds can take longer than 10s)

### Pagination — blocks table
The blocks table can have 80M+ rows. `OFFSET` on large pages causes 30s+ full index scans. Instead:
```rust
// cursor = max_block - (page - 1) * limit — uses clamped limit(), not raw offset()
let limit = pagination.limit(); // clamped to 100
let cursor = (total_count - 1) - (pagination.page.saturating_sub(1) as i64) * limit;
// Query: WHERE number <= $cursor ORDER BY number DESC LIMIT $1
```
`total_count` comes from `MAX(number) + 1` (O(1), not COUNT(*)).

### Row count estimation
For large tables (transactions, addresses), use `pg_class.reltuples` instead of `COUNT(*)`:
```rust
// handlers/mod.rs — get_table_count(pool, "table_name")
// Partition-aware: sums child reltuples, falls back to parent
// For tables < 100k rows: falls back to exact COUNT(*)
```

### HTTP timeout
`TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(10))` wraps all routes — returns 408 if any handler exceeds 10s.

### AppState (API)
```rust
pub struct AppState {
pub pool: PgPool,
pub rpc_url: String,
pub solc_path: String,
pub admin_api_key: Option<String>,
pub chain_id: u64, // fetched from RPC once at startup via eth_chainId
pub chain_name: String, // from CHAIN_NAME env var, defaults to "Unknown"
}
```

### Frontend API client
- Base URL: `/api` (proxied by nginx to `atlas-api:3000`)
- Fast polling endpoint: `GET /api/height` → `{ block_height, indexed_at }` — used by navbar every 2s
- Chain status: `GET /api/status` → full chain info, fetched once on page load

## Important Conventions

- **Rust**: idiomatic — use `.min()`, `.max()`, `|=`, `+=` over manual if/assign
- **SQL**: never use `OFFSET` for large tables — use keyset/cursor pagination
- **Migrations**: use `run_migrations(&database_url)` (not `&pool`) to get a timeout-free connection
- **Frontend**: uses Bun (not npm/yarn). Lockfile is `bun.lock` (text, Bun ≥ 1.2). Build with `bunx vite build` (skips tsc type check).
- **Docker**: frontend image uses `nginxinc/nginx-unprivileged:alpine` (non-root, port 8080). API/indexer use `alpine` with `ca-certificates`.
- **Commits**: authored by the user only — no Claude co-author lines.

## Environment Variables

Key vars (see `.env.example` for full list):

| Var | Used by | Default |
|---|---|---|
| `DATABASE_URL` | all | required |
| `RPC_URL` | indexer, api | required |
| `CHAIN_NAME` | api | `"Unknown"` |
| `DB_MAX_CONNECTIONS` | indexer | `20` |
| `BATCH_SIZE` | indexer | `100` |
| `FETCH_WORKERS` | indexer | `10` |
| `ADMIN_API_KEY` | api | none |

## Running Locally

```bash
# Start full stack
docker compose up -d

# Rebuild a single service after code changes
docker compose build atlas-api && docker compose up -d atlas-api

# Backend only (no Docker)
cd backend && cargo build --workspace
```

## Common Gotchas

- `get_table_count(pool, table_name)` — pass the table name, it's not hardcoded anymore
- `run_migrations` takes `&str` (database URL), not `&PgPool`
- The blocks cursor uses `pagination.limit()` (clamped), not `pagination.offset()` — they diverge when client sends `limit > 100`
- `bun.lock` not `bun.lockb` — Bun ≥ 1.2 uses text format
16 changes: 15 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,23 @@ Response format:

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/status` | Current block height and indexer timestamp |
| GET | `/api/height` | Current block height and indexer timestamp (lightweight, safe to poll frequently) |
| GET | `/api/status` | Full chain status: chain ID, chain name, block height, total transactions, total addresses |
| GET | `/health` | Health check (returns "OK") |

**`/api/status` response:**
```json
{
"chain_id": 1,
"chain_name": "My Chain",
"block_height": 1000000,
"total_transactions": 5000000,
"total_addresses": 200000,
"indexed_at": "2026-01-01T00:00:00+00:00"
}
```
`chain_name` is set via the `CHAIN_NAME` environment variable.

### Blocks

| Method | Path | Description |
Expand Down