Standardize env variables. Revamp README.

This commit is contained in:
Alejandro Cabeza Romero 2025-11-05 11:52:08 +01:00
parent 64578fba42
commit 687cecdac7
No known key found for this signature in database
GPG Key ID: DA3D14AE478030FD
5 changed files with 151 additions and 31 deletions

View File

@ -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

152
README.md
View File

@ -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<br/>Node] -->|REST/SSE| B["Backend<br/>(FastAPI)"]
B -->|REST/SSE| C["Frontend<br/>(Preact)"]
B <--> D["Database<br/>(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`

View File

@ -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):

View File

@ -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

View File

@ -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],