diff --git a/Dockerfile b/Dockerfile
index d66d184..701108c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,11 +5,10 @@ COPY . /app
WORKDIR /app
# Environment variables
-ENV NODE_COMPOSE_FILEPATH=/app/docker-compose.yml
ENV PYTHONPATH=/app:/app/src
ENV UV_INSTALL_DIR=/usr/local/bin
-ENV NODE_API=http
-ENV NODE_MANAGER=noop
+ENV NBE_NODE_API=http
+ENV NBE_NODE_MANAGER=noop
# Package manager and dependencies
# RUN apt-get update && apt-get install -y curl git
diff --git a/README.md b/README.md
index 7270dc2..358d550 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,141 @@
# Nomos Block Explorer
-## Assumptions
-There are a few assumptions made to facilitate the development of the PoC:
-- One block per slot.
-- If a range has been backfilled, it has been fully successfully backfilled.
-- Backfilling strategy assumes there's, at most, one gap to fill.
+This is a Proof of Concept (PoC) for a block explorer for the Nomos blockchain.
-## TODO
-- Better backfilling
-- Upsert on backfill
-- Change Sqlite -> Postgres
-- Performance improvements on API and DB calls
-- Fix assumptions, so we don't rely on them
-- DbRepository interfaces
-- Setup DB Migrations
-- Tests
-- Fix assumption of 1 block per slot
-- Log colouring
-- Handle reconnections:
+## Features
+
+- Frontend (React-like SPA)
+ - Client-side routing with Home, Block, and Transaction pages.
+ - Home: Live stream of the latest Blocks and Transactions.
+ - Block: Details of a Block, including a list of its transactions.
+ - Transaction: Details of a Transaction.
+- Backend (FastAPI)
+ - API
+ - REST API to query Blocks and Transactions.
+ - SSE API to stream live Blocks (and its transactions).
+ - Node Management
+ - Pluggable API (e.g. `fake`, `http`) to query nodes.
+ - Pluggable Manager (e.g. `noop`, `docker`) to manage local nodes.
+ - Simple backfilling mechanism to populate historical blocks.
+
+## Architecture
+
+The Nomos Block Explorer follows a three-tier architecture with a clear separation of concerns:
+
+### High-Level Overview
+
+```mermaid
+graph LR;
+A[Nomos
Node] -->|REST/SSE| B["Backend
(FastAPI)"]
+B -->|REST/SSE| C["Frontend
(Preact)"]
+B <--> D["Database
(SQLite)"]
+```
+
+### Components
+
+#### 1. Frontend (`/static`)
+- **Framework**: Preact (lightweight React alternative)
+- **Routing**: Client-side SPA routing
+- **Architecture**: Component-based with functional components
+- **Communication**: REST API calls and Server-Sent Events (SSE) for real-time updates
+
+#### 2. Backend (`/src`)
+- **Framework**: FastAPI (Python async web framework)
+- **API Layer** (`/src/api`): Serializers, REST and streaming endpoints
+- **Core** (`/src/core`): Application setup, configuration, base types and mixins
+- **Database Layer** (`/src/db`): Repository pattern for data access
+- **Models** (`/src/models`): Domain models (Block, Transaction, Header, etc.)
+- **Node Integration** (`/src/node`):
+ - **API**: Pluggable adapters to communicate with Nomos nodes (`fake`, `http`) and serializers
+ - **Manager**: Pluggable node lifecycle management (`noop`, `docker`)
+
+#### 3. Data Flow
+
+1. **Node Updates**: On startup, the backend starts listening for new blocks from the node and stores them in the database
+2. **Backfilling**: After at least one block is in the database, the backend fetches historical blocks from the node and stores them
+3. **Client Updates**: Frontend subscribes to SSE endpoints for real-time block and transaction updates
+4. **Data Access**: All queries route through repository classes for consistent data access
+
+#### 4. Key Design Patterns
+
+- **Repository Pattern**: Abstraction layer for database operations (`BlockRepository`, `TransactionRepository`)
+- **Strategy Pattern**: Pluggable Node API implementations (fake for testing, HTTP for production)
+- **Adapter Pattern**: Serializers convert between Node API formats and internal domain models
+- **Observer Pattern**: SSE streams for pushing real-time updates to clients
+
+
+## Requirements
+
+- Python 3.14
+- UV Package Manager
+
+### Optional
+
+- Docker: To run a local node.
+
+## How to run
+
+1. Install the dependencies:
+ ```bash
+ uv sync
+ ```
+
+2. Run the block explorer:
+ ```bash
+ python src/main.py
+ ```
+
+- You can optionally run it via Docker with:
+ ```bash
+ docker build -t nomos-block-explorer . && docker run -p 8000:8000 nomos-block-explorer
+ ```
+
+### Configuration
+
+The block explorer is configured through environment variables. The following variables are available:
+```dotenv
+NBE_LOG_LEVEL=DEBUG # DEBUG, INFO, WARNING, ERROR, CRITICAL
+
+NBE_DEBUG=true # Randomizes transactions in BlockSerializer
+
+NBE_NODE_MANAGER=noop # noop, docker
+NBE_NODE_COMPOSE_FILEPATH=/path/to/docker-compose.yml # Only used if NODE_MANAGER=docker
+
+NBE_NODE_API=http # fake, http
+NBE_NODE_API_HOST=localhost # Only used if NODE_API=http
+NBE_NODE_API_PORT=18080 # Only used if NODE_API=http
+NBE_NODE_API_TIMEOUT=60 # Only used if NODE_API=http
+NBE_NODE_API_PROTOCOL=http # Only used if NODE_API=http
+
+NBE_HOST=0.0.0.0 # Block Explorer's listening host
+NBE_PORT=8000 # Block Explorer's listening port
+```
+If running the Block Explorer with Docker, these can be overridden.
+
+## Considerations
+
+This PoC makes simplifications to focus on the core features:
+- Each slot has exactly one block.
+- When backfilling, the block explorer will only backfill from the earliest block's slot to genesis.
+
+## Ideas and improvements
+
+- Fix aforementioned assumptions
+- Backfilling
+ - Make requests concurrently
+ - Backfill all slots
+ - Upsert received blocks and transactions
+- Database
+ - Update to Postgres
+ - Add migrations management
+ - Add relevant indexes to columns
+- Add interfaces to database repositories: `BlockRepository` and `TransactionRepository`
+- Add tests
+- Colour logs by level
+- Reconnections
- Failures to connect to Node
- Timeouts
- Stream closed
+- Frontend
+ - Add a block / transaction search bar
+ - Make pages work with block/transaction hash, rather than the `id`
diff --git a/src/core/app.py b/src/core/app.py
index 1f531c7..74a34e4 100644
--- a/src/core/app.py
+++ b/src/core/app.py
@@ -2,6 +2,7 @@ from asyncio import Task, gather
from typing import Literal, Optional
from fastapi import FastAPI
+from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from starlette.datastructures import State
@@ -18,15 +19,15 @@ ENV_FILEPATH = DIR_REPO.joinpath(".env")
class NBESettings(BaseSettings):
model_config = SettingsConfigDict(env_file=ENV_FILEPATH, extra="ignore")
- node_compose_filepath: str
+ node_compose_filepath: Optional[str] = Field(alias="NBE_NODE_COMPOSE_FILEPATH", default=None)
- node_api: Literal["http", "fake"]
- node_manager: Literal["docker", "noop"]
+ node_api: Literal["http", "fake"] = Field(alias="NBE_NODE_API")
+ node_manager: Literal["docker", "noop"] = Field(alias="NBE_NODE_MANAGER")
- node_api_host: str = "127.0.0.1"
- node_api_port: int = 18080
- node_api_timeout: int = 60
- node_api_protocol: str = "http"
+ node_api_host: str = Field(alias="NBE_NODE_API_HOST", default="127.0.0.1")
+ node_api_port: int = Field(alias="NBE_NODE_API_PORT", default=18080)
+ node_api_timeout: int = Field(alias="NBE_NODE_API_TIMEOUT", default=60)
+ node_api_protocol: str = Field(alias="NBE_NODE_API_PROTOCOL", default="http")
class NBEState(State):
diff --git a/src/node/api/serializers/block.py b/src/node/api/serializers/block.py
index a0ef0ee..d4ecfc9 100644
--- a/src/node/api/serializers/block.py
+++ b/src/node/api/serializers/block.py
@@ -13,9 +13,8 @@ from utils.protocols import FromRandom
def _should_randomize_transactions():
- is_debug = getenv("DEBUG", "False").lower() == "true"
- is_debug__randomize_transactions = getenv("DEBUG__RANDOMIZE_TRANSACTIONS", "False").lower() == "true"
- return is_debug and is_debug__randomize_transactions
+ is_debug = getenv("NBE_DEBUG", "False").lower() == "true"
+ return is_debug
def _get_random_transactions() -> List[SignedTransactionSerializer]:
@@ -34,7 +33,7 @@ class BlockSerializer(NbeSerializer, FromRandom):
def model_validate_json(cls, *args, **kwargs) -> Self:
self = super().model_validate_json(*args, **kwargs)
if _should_randomize_transactions():
- logger.debug("DEBUG and DEBUG__RANDOMIZE_TRANSACTIONS are enabled, randomizing Block's transactions.")
+ logger.debug("DEBUG flag is enabled, randomizing Block's transactions.")
self.transactions = _get_random_transactions()
return self
diff --git a/src/node/manager/docker.py b/src/node/manager/docker.py
index 6e1ee0d..597d1c7 100644
--- a/src/node/manager/docker.py
+++ b/src/node/manager/docker.py
@@ -16,6 +16,9 @@ logger = logging.getLogger(__name__)
class DockerModeManager(NodeManager):
def __init__(self, settings: "NBESettings"):
+ if not settings.node_compose_filepath:
+ raise ValueError("Node compose filepath environment variable is not set.")
+
self.client: DockerClient = DockerClient(
client_type="docker",
compose_files=[settings.node_compose_filepath],