Files
bootnode/api/bootnode/main.py
T
zeekayandHanzo Dev c1996cacc8
Hanzo CI/CD / cicd (push) Successful in 8m30s
CI/CD / cicd (push) Successful in 8m30s
api: read Hanzo KV from KV_URL; a KV outage no longer fails requests
The cache client is now the KV client (core/cache/kv.py, kv_client),
configured by KV_URL. rate_limit_check allows the request and logs when KV
is unreachable, and the RPC cache treats an outage as a miss. cloud_billing
reads KV directly instead of a missing usage_tracker.redis attribute; the
NFT refresh enqueues through an arq pool.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 00:29:49 -07:00

167 lines
5.2 KiB
Python

"""Bootnode API - Main application entry point."""
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import structlog
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import ORJSONResponse
from bootnode import __version__
from bootnode.api import router as api_router
from bootnode.config import get_settings
from bootnode.core.cache import kv_client
from bootnode.core.datastore import datastore_client
from bootnode.core.kms import inject_secrets
from bootnode.db.session import engine, init_db
from bootnode.ws import router as ws_router
from bootnode.zap import start_zap_server, stop_zap_server
# Load secrets from Hanzo KMS before settings
inject_secrets()
# ---------------------------------------------------------------------------
# Hanzo O11y — Traces + Metrics + Logs
# All telemetry → OTEL Collector → Datastore + Loki
# Prometheus /metrics served natively for ServiceMonitor scraping
# ---------------------------------------------------------------------------
from bootnode.core.o11y import setup_otel, setup_otel_logging
# Wire OTEL log export early (before any logging)
setup_otel_logging()
logger = structlog.get_logger()
settings = get_settings()
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
"""Application lifespan handler."""
# Startup
logger.info("Starting Bootnode API", env=settings.app_env)
await init_db()
await kv_client.initialize()
# Initialize DataStore - optional, won't fail startup
try:
await datastore_client.initialize()
logger.info("DataStore connected")
except Exception as e:
logger.warning("DataStore not available", error=str(e))
# Start native ZAP server (Cap'n Proto RPC over TCP)
zap_server = None
if settings.zap_enabled:
try:
zap_server = await start_zap_server(
host=settings.zap_host,
port=settings.zap_port,
)
logger.info(
"ZAP server started",
url=f"zap://{settings.zap_host}:{settings.zap_port}",
)
except Exception as e:
logger.warning("ZAP server failed to start", error=str(e))
yield
# Shutdown
logger.info("Shutting down Bootnode API")
# Stop ZAP server
if zap_server:
try:
await stop_zap_server()
logger.info("ZAP server stopped")
except Exception as e:
logger.warning("ZAP server stop error", error=str(e))
await engine.dispose()
await kv_client.close()
await datastore_client.close()
app = FastAPI(
title="Bootnode API",
description="Blockchain Development Platform - Multi-chain RPC, Token API, NFT API, Smart Wallets, and more",
version=__version__,
docs_url="/docs" if not settings.is_production else None,
redoc_url="/redoc" if not settings.is_production else None,
openapi_url="/openapi.json" if not settings.is_production else None,
default_response_class=ORJSONResponse,
lifespan=lifespan,
)
# Hanzo O11y — /metrics + OTEL traces/metrics/logs
setup_otel(app)
# Validate production secrets (fail-fast)
settings.validate_production()
# CORS - all cloud domains for white-label multi-network support
production_origins = [
"https://bootno.de",
"https://lux.build",
"https://cloud.lux.network",
"https://lux.cloud",
"https://console.lux.cloud",
"https://cloud.pars.network",
"https://cloud.zoo.network",
"https://cloud.hanzo.ai",
"https://docs.bootno.de",
]
# Add frontend_url if set
if settings.frontend_url:
origin = settings.frontend_url.rstrip("/")
if origin not in production_origins:
production_origins.append(origin)
# Add extra origins from ALLOWED_ORIGINS env var
for extra in getattr(settings, "allowed_origins", []):
origin = extra.strip().rstrip("/")
if origin and origin not in production_origins:
production_origins.append(origin)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"] if not settings.is_production else production_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API routes
app.include_router(api_router, prefix=settings.api_prefix)
# Include WebSocket routes
app.include_router(ws_router, prefix=f"{settings.api_prefix}/ws", tags=["WebSocket"])
@app.get("/health")
async def health_check() -> dict:
"""Health check endpoint."""
return {"status": "healthy", "version": __version__}
@app.get("/")
async def root() -> dict:
"""Root endpoint.
`docs` mirrors app.docs_url rather than hardcoding "/docs". The FastAPI app
above disables docs/redoc/openapi in production on purpose, so advertising
"/docs" unconditionally pointed every caller of the public API root at a URL
that 404s — api.bootno.de says `"docs": "/docs"` and /docs returns
{"detail":"Not Found"}. Reading the real value keeps the two in step however
the setting is flipped, instead of leaving a second place to remember.
"""
body = {
"name": "Bootnode API",
"version": __version__,
"status": "operational",
}
if app.docs_url:
body["docs"] = app.docs_url
return body