mirror of
https://github.com/logos-blockchain/logos-blockchain-block-explorer-template.git
synced 2026-01-02 05:03:07 +00:00
Standardize env variables. Revamp README.
This commit is contained in:
parent
64578fba42
commit
687cecdac7
@ -5,11 +5,10 @@ COPY . /app
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
ENV NODE_COMPOSE_FILEPATH=/app/docker-compose.yml
|
|
||||||
ENV PYTHONPATH=/app:/app/src
|
ENV PYTHONPATH=/app:/app/src
|
||||||
ENV UV_INSTALL_DIR=/usr/local/bin
|
ENV UV_INSTALL_DIR=/usr/local/bin
|
||||||
ENV NODE_API=http
|
ENV NBE_NODE_API=http
|
||||||
ENV NODE_MANAGER=noop
|
ENV NBE_NODE_MANAGER=noop
|
||||||
|
|
||||||
# Package manager and dependencies
|
# Package manager and dependencies
|
||||||
# RUN apt-get update && apt-get install -y curl git
|
# RUN apt-get update && apt-get install -y curl git
|
||||||
|
|||||||
152
README.md
152
README.md
@ -1,23 +1,141 @@
|
|||||||
# Nomos Block Explorer
|
# Nomos Block Explorer
|
||||||
|
|
||||||
## Assumptions
|
This is a Proof of Concept (PoC) for a block explorer for the Nomos blockchain.
|
||||||
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.
|
|
||||||
|
|
||||||
## TODO
|
## Features
|
||||||
- Better backfilling
|
|
||||||
- Upsert on backfill
|
- Frontend (React-like SPA)
|
||||||
- Change Sqlite -> Postgres
|
- Client-side routing with Home, Block, and Transaction pages.
|
||||||
- Performance improvements on API and DB calls
|
- Home: Live stream of the latest Blocks and Transactions.
|
||||||
- Fix assumptions, so we don't rely on them
|
- Block: Details of a Block, including a list of its transactions.
|
||||||
- DbRepository interfaces
|
- Transaction: Details of a Transaction.
|
||||||
- Setup DB Migrations
|
- Backend (FastAPI)
|
||||||
- Tests
|
- API
|
||||||
- Fix assumption of 1 block per slot
|
- REST API to query Blocks and Transactions.
|
||||||
- Log colouring
|
- SSE API to stream live Blocks (and its transactions).
|
||||||
- Handle reconnections:
|
- 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
|
- Failures to connect to Node
|
||||||
- Timeouts
|
- Timeouts
|
||||||
- Stream closed
|
- Stream closed
|
||||||
|
- Frontend
|
||||||
|
- Add a block / transaction search bar
|
||||||
|
- Make pages work with block/transaction hash, rather than the `id`
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from asyncio import Task, gather
|
|||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from pydantic import Field
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from starlette.datastructures import State
|
from starlette.datastructures import State
|
||||||
|
|
||||||
@ -18,15 +19,15 @@ ENV_FILEPATH = DIR_REPO.joinpath(".env")
|
|||||||
class NBESettings(BaseSettings):
|
class NBESettings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_file=ENV_FILEPATH, extra="ignore")
|
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_api: Literal["http", "fake"] = Field(alias="NBE_NODE_API")
|
||||||
node_manager: Literal["docker", "noop"]
|
node_manager: Literal["docker", "noop"] = Field(alias="NBE_NODE_MANAGER")
|
||||||
|
|
||||||
node_api_host: str = "127.0.0.1"
|
node_api_host: str = Field(alias="NBE_NODE_API_HOST", default="127.0.0.1")
|
||||||
node_api_port: int = 18080
|
node_api_port: int = Field(alias="NBE_NODE_API_PORT", default=18080)
|
||||||
node_api_timeout: int = 60
|
node_api_timeout: int = Field(alias="NBE_NODE_API_TIMEOUT", default=60)
|
||||||
node_api_protocol: str = "http"
|
node_api_protocol: str = Field(alias="NBE_NODE_API_PROTOCOL", default="http")
|
||||||
|
|
||||||
|
|
||||||
class NBEState(State):
|
class NBEState(State):
|
||||||
|
|||||||
@ -13,9 +13,8 @@ from utils.protocols import FromRandom
|
|||||||
|
|
||||||
|
|
||||||
def _should_randomize_transactions():
|
def _should_randomize_transactions():
|
||||||
is_debug = getenv("DEBUG", "False").lower() == "true"
|
is_debug = getenv("NBE_DEBUG", "False").lower() == "true"
|
||||||
is_debug__randomize_transactions = getenv("DEBUG__RANDOMIZE_TRANSACTIONS", "False").lower() == "true"
|
return is_debug
|
||||||
return is_debug and is_debug__randomize_transactions
|
|
||||||
|
|
||||||
|
|
||||||
def _get_random_transactions() -> List[SignedTransactionSerializer]:
|
def _get_random_transactions() -> List[SignedTransactionSerializer]:
|
||||||
@ -34,7 +33,7 @@ class BlockSerializer(NbeSerializer, FromRandom):
|
|||||||
def model_validate_json(cls, *args, **kwargs) -> Self:
|
def model_validate_json(cls, *args, **kwargs) -> Self:
|
||||||
self = super().model_validate_json(*args, **kwargs)
|
self = super().model_validate_json(*args, **kwargs)
|
||||||
if _should_randomize_transactions():
|
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()
|
self.transactions = _get_random_transactions()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class DockerModeManager(NodeManager):
|
class DockerModeManager(NodeManager):
|
||||||
def __init__(self, settings: "NBESettings"):
|
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(
|
self.client: DockerClient = DockerClient(
|
||||||
client_type="docker",
|
client_type="docker",
|
||||||
compose_files=[settings.node_compose_filepath],
|
compose_files=[settings.node_compose_filepath],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user