Merge pull request #9 from bootnode/fix/api-boot

fix: get the API booting (stale refactor imports + qdrant healthcheck)
This commit is contained in:
2026-06-18 19:58:34 -07:00
committed by GitHub
17 changed files with 355 additions and 106 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""Bootnode - Blockchain Development Platform."""
__version__ = "2.0.0"
__version__ = "1.0.0"
+10
View File
@@ -21,6 +21,11 @@ from bootnode.api.networks import router as networks_router
from bootnode.api.observability import router as o11y_router
from bootnode.api.chat import router as chat_router
from bootnode.api.zap import router as zap_router
from bootnode.api.mpc import router as mpc_router
from bootnode.api.keys import router as keys_router
from bootnode.api.launch import router as launch_router
from bootnode.api.contracts import router as contracts_router
from bootnode.api.subgraphs import router as subgraphs_router
router = APIRouter()
@@ -44,3 +49,8 @@ router.include_router(team_router, prefix="/team", tags=["Team"])
router.include_router(networks_router, prefix="/networks", tags=["Networks"])
router.include_router(o11y_router, prefix="/o11y", tags=["Observability"])
router.include_router(chat_router, prefix="/chat", tags=["AI Chat"])
router.include_router(mpc_router, prefix="/mpc", tags=["MPC Wallets"])
router.include_router(keys_router, prefix="/keys", tags=["Key Management"])
router.include_router(launch_router, prefix="/launch", tags=["Launch"])
router.include_router(contracts_router, prefix="/contracts", tags=["Contracts"])
router.include_router(subgraphs_router, prefix="/subgraphs", tags=["Subgraphs"])
+105
View File
@@ -0,0 +1,105 @@
"""Contracts API — deploy and interact with smart contracts via chain RPC.
Deploy = broadcast a signed creation transaction; the mined receipt carries the
new contract address. Read = eth_call. Code check = eth_getCode. Signing stays
client-side (or via the MPC wallet API) — this module never holds private keys.
"""
from typing import Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from bootnode.api.deps import ApiKeyDep
from bootnode.core.chains import ChainRegistry, RPCClient
from bootnode.core.chains.rpc import RPCError
router = APIRouter()
class DeployRequest(BaseModel):
"""A pre-signed contract-creation transaction to broadcast."""
signed_tx: str = Field(..., description="0x-prefixed signed creation transaction")
network: str = "mainnet"
class CallRequest(BaseModel):
"""A read-only contract call (eth_call)."""
to: str = Field(..., description="Contract address")
data: str = Field(..., description="0x-encoded calldata")
block: str = "latest"
network: str = "mainnet"
async def _rpc(chain: str, network: str, method: str, params: list[Any]) -> Any:
if not ChainRegistry.is_supported(chain, network):
raise HTTPException(400, f"Unsupported chain/network: {chain}/{network}")
try:
async with RPCClient(chain, network) as client:
return await client.call(method, params, 1)
except RPCError as e:
raise HTTPException(502, f"RPC error {e.code}: {e.message}")
except HTTPException:
raise
except Exception as e: # upstream RPC unreachable / retries exhausted
raise HTTPException(502, f"RPC call to {chain}/{network} failed: {type(e).__name__}")
@router.post("/{chain}/deploy")
async def deploy_contract(chain: str, body: DeployRequest, api_key: ApiKeyDep) -> dict:
"""Broadcast a signed contract-creation transaction.
Returns the transaction hash; poll the receipt to get the contract address
once the transaction is mined.
"""
tx_hash = await _rpc(chain, body.network, "eth_sendRawTransaction", [body.signed_tx])
return {
"chain": chain,
"network": body.network,
"txHash": tx_hash,
"receipt": f"/v1/contracts/{chain}/tx/{tx_hash}/receipt?network={body.network}",
}
@router.get("/{chain}/tx/{tx_hash}/receipt")
async def deploy_receipt(
chain: str, tx_hash: str, api_key: ApiKeyDep, network: str = "mainnet"
) -> dict:
"""Fetch a transaction receipt — `contractAddress` is set once a creation
transaction is mined."""
receipt = await _rpc(chain, network, "eth_getTransactionReceipt", [tx_hash])
if receipt is None:
return {"status": "pending", "txHash": tx_hash, "contractAddress": None}
return {
"status": "mined" if receipt.get("status") == "0x1" else "failed",
"txHash": tx_hash,
"contractAddress": receipt.get("contractAddress"),
"blockNumber": receipt.get("blockNumber"),
"gasUsed": receipt.get("gasUsed"),
}
@router.get("/{chain}/{address}/code")
async def contract_code(
chain: str, address: str, api_key: ApiKeyDep, network: str = "mainnet"
) -> dict:
"""eth_getCode — confirm a contract is deployed at an address."""
code = await _rpc(chain, network, "eth_getCode", [address, "latest"])
deployed = bool(code) and code != "0x"
return {
"chain": chain,
"address": address,
"deployed": deployed,
"codeSize": (len(code) - 2) // 2 if deployed else 0,
}
@router.post("/{chain}/call")
async def call_contract(chain: str, body: CallRequest, api_key: ApiKeyDep) -> dict:
"""eth_call — read-only contract method invocation."""
result = await _rpc(
chain, body.network, "eth_call", [{"to": body.to, "data": body.data}, body.block]
)
return {"chain": chain, "to": body.to, "result": result}
+7 -7
View File
@@ -5,7 +5,7 @@ from sqlalchemy import select
from sqlalchemy import func as sa_func
from bootnode.api.deps import ApiKeyDep, DbDep, ProjectDep
from bootnode.db.models import MPCWalletModel
from bootnode.db.models import SmartWallet
router = APIRouter()
@@ -22,18 +22,18 @@ async def get_key_status(
Proxies to MPC wallet status data.
"""
result = await db.execute(
select(sa_func.count(MPCWalletModel.id)).where(
MPCWalletModel.project_id == project.id,
MPCWalletModel.status == "active",
select(sa_func.count(SmartWallet.id)).where(
SmartWallet.project_id == project.id,
SmartWallet.status == "active",
)
)
total = result.scalar() or 0
# Get first active wallet as default
first_result = await db.execute(
select(MPCWalletModel).where(
MPCWalletModel.project_id == project.id,
MPCWalletModel.status == "active",
select(SmartWallet).where(
SmartWallet.project_id == project.id,
SmartWallet.status == "active",
).limit(1)
)
default_wallet = first_result.scalar_one_or_none()
+6 -6
View File
@@ -145,8 +145,8 @@ def _generate_lux_network(cfg: ChainConfig, network: str, net: "NetworkSpec", na
lines += [
" ports:",
" http: 9650",
" staking: 9651",
" http: 9630",
" staking: 9631",
" consensus:",
f" sybilProtectionEnabled: {sybil}",
" storage:",
@@ -231,7 +231,7 @@ def _generate_lux_explorer(cfg: ChainConfig, network: str, net: "NetworkSpec", n
"""
ex = cfg.services.explorer
host = cfg.brand.domains.explorer or f"explorer.{network}.{cfg.chain.slug}.network"
rpc_url = net.rpc_url or f"http://{cfg.chain.slug}d-0.{cfg.chain.slug}d-headless.{namespace}.svc:9650/ext/bc/C/rpc"
rpc_url = net.rpc_url or f"http://{cfg.chain.slug}d-0.{cfg.chain.slug}d-headless.{namespace}.svc:9630/ext/bc/C/rpc"
port = ex.port
return f"""apiVersion: apps/v1
@@ -479,7 +479,7 @@ spec:
- name: CHAIN_ID
value: "{net.chain_id}"
- name: RPC_URL
value: "http://{cfg.chain.slug}d-0.{cfg.chain.slug}d:9650/ext/bc/C/rpc"
value: "http://{cfg.chain.slug}d-0.{cfg.chain.slug}d:9630/ext/bc/C/rpc"
- name: DRIP_AMOUNT
value: "{faucet.drip_amount}"
- name: RATE_LIMIT
@@ -584,8 +584,8 @@ def register_chain_networks(cfg: ChainConfig) -> None:
chain=slug,
network_id=net.network_id,
chain_id=net.chain_id,
http_port=9650,
staking_port=9651,
http_port=9630,
staking_port=9631,
namespace=cfg.namespace_for(name),
)
+26 -26
View File
@@ -18,7 +18,7 @@ from bootnode.core.mpc import (
decrypt_share,
encrypt_share,
)
from bootnode.db.models import MPCWalletModel
from bootnode.db.models import SmartWallet
logger = structlog.get_logger()
router = APIRouter()
@@ -85,7 +85,7 @@ async def create_mpc_wallet(
api_key: ApiKeyDep,
project: ProjectDep,
db: DbDep,
) -> MPCWalletModel:
) -> SmartWallet:
"""Create a new MPC wallet with threshold key shares."""
if request.threshold < 2:
raise HTTPException(400, "Threshold must be at least 2")
@@ -116,7 +116,7 @@ async def create_mpc_wallet(
"3": "backup",
}
wallet = MPCWalletModel(
wallet = SmartWallet(
project_id=project.id,
address=keypair.address,
chain=request.chain,
@@ -148,13 +148,13 @@ async def list_mpc_wallets(
project: ProjectDep,
db: DbDep,
chain: str | None = None,
) -> list[MPCWalletModel]:
) -> list[SmartWallet]:
"""List all MPC wallets for the project."""
query = select(MPCWalletModel).where(
MPCWalletModel.project_id == project.id
query = select(SmartWallet).where(
SmartWallet.project_id == project.id
)
if chain:
query = query.where(MPCWalletModel.chain == chain)
query = query.where(SmartWallet.chain == chain)
result = await db.execute(query)
return list(result.scalars().all())
@@ -168,19 +168,19 @@ async def get_key_status(
) -> MPCKeyStatusResponse:
"""Get MPC wallet status for the project (used by staking page)."""
result = await db.execute(
select(sa_func.count(MPCWalletModel.id)).where(
MPCWalletModel.project_id == project.id,
MPCWalletModel.status == "active",
select(sa_func.count(SmartWallet.id)).where(
SmartWallet.project_id == project.id,
SmartWallet.status == "active",
)
)
total = result.scalar() or 0
# Get first wallet as default
first = await db.execute(
select(MPCWalletModel)
select(SmartWallet)
.where(
MPCWalletModel.project_id == project.id,
MPCWalletModel.status == "active",
SmartWallet.project_id == project.id,
SmartWallet.status == "active",
)
.limit(1)
)
@@ -207,12 +207,12 @@ async def get_mpc_wallet(
api_key: ApiKeyDep,
project: ProjectDep,
db: DbDep,
) -> MPCWalletModel:
) -> SmartWallet:
"""Get MPC wallet details."""
result = await db.execute(
select(MPCWalletModel).where(
MPCWalletModel.address == address.lower(),
MPCWalletModel.project_id == project.id,
select(SmartWallet).where(
SmartWallet.address == address.lower(),
SmartWallet.project_id == project.id,
)
)
wallet = result.scalar_one_or_none()
@@ -231,9 +231,9 @@ async def sign_with_mpc(
) -> MPCSignResponse:
"""Sign a message or transaction using threshold shares."""
result = await db.execute(
select(MPCWalletModel).where(
MPCWalletModel.address == address.lower(),
MPCWalletModel.project_id == project.id,
select(SmartWallet).where(
SmartWallet.address == address.lower(),
SmartWallet.project_id == project.id,
)
)
db_wallet = result.scalar_one_or_none()
@@ -317,9 +317,9 @@ async def get_share_info(
) -> dict:
"""Get share holder info (not the actual shares)."""
result = await db.execute(
select(MPCWalletModel).where(
MPCWalletModel.address == address.lower(),
MPCWalletModel.project_id == project.id,
select(SmartWallet).where(
SmartWallet.address == address.lower(),
SmartWallet.project_id == project.id,
)
)
wallet = result.scalar_one_or_none()
@@ -345,9 +345,9 @@ async def decrypt_wallet_share(
) -> dict:
"""Decrypt a specific share for signing (requires auth)."""
result = await db.execute(
select(MPCWalletModel).where(
MPCWalletModel.address == address.lower(),
MPCWalletModel.project_id == project.id,
select(SmartWallet).where(
SmartWallet.address == address.lower(),
SmartWallet.project_id == project.id,
)
)
wallet = result.scalar_one_or_none()
+62 -42
View File
@@ -69,18 +69,20 @@ CHAIN_IMAGES = {
"blast": {
"execution": {"blast": "blastio/blast-geth:latest"},
},
# ============ HANZO ECOSYSTEM (Lux-based) ============
# ============ LUX ECOSYSTEM (luxd — Lux, Zoo, Pars, Hanzo) ============
# All four run the same luxd node (ghcr.io/luxfi/node). Only the primary
# network-id and genesis differ; Zoo/Pars/Hanzo are Lux-based sovereign L1s.
"lux": {
"execution": {"luxd": "luxfi/luxd:latest"},
"execution": {"luxd": "ghcr.io/luxfi/node:latest"},
},
"zoo": {
"execution": {"luxd": "luxfi/luxd:latest"}, # Zoo subnet on Lux
"execution": {"luxd": "ghcr.io/luxfi/node:latest"}, # Zoo: Lux-based sovereign L1
},
"pars": {
"execution": {"luxd": "luxfi/luxd:latest"}, # Pars subnet on Lux
"execution": {"luxd": "ghcr.io/luxfi/node:latest"}, # Pars: Lux-based sovereign L1
},
"hanzo": {
"execution": {"luxd": "luxfi/luxd:latest"}, # Hanzo subnet on Lux
"execution": {"luxd": "ghcr.io/luxfi/node:latest"}, # Hanzo: Lux-based sovereign L1
},
# ============ MAJOR L1s ============
"bitcoin": {
@@ -240,10 +242,10 @@ CHAIN_PORTS = {
"mantle": {"rpc": 8545, "ws": 8546},
"blast": {"rpc": 8545, "ws": 8546},
# Hanzo ecosystem
"lux": {"rpc": 9650, "ws": 9650, "p2p": 9651},
"zoo": {"rpc": 9650, "ws": 9650, "p2p": 9651},
"pars": {"rpc": 9650, "ws": 9650, "p2p": 9651},
"hanzo": {"rpc": 9650, "ws": 9650, "p2p": 9651},
"lux": {"rpc": 9630, "ws": 9630, "p2p": 9631},
"zoo": {"rpc": 9630, "ws": 9630, "p2p": 9631},
"pars": {"rpc": 9630, "ws": 9630, "p2p": 9631},
"hanzo": {"rpc": 9630, "ws": 9630, "p2p": 9631},
# Major L1s
"bitcoin": {"rpc": 8332, "p2p": 8333},
"solana": {"rpc": 8899, "ws": 8900, "p2p": 8001},
@@ -374,7 +376,12 @@ def allocate_ports() -> Dict[str, int]:
if bindings:
for binding in bindings:
host_port = int(binding.get("HostPort", 0))
if host_port >= _port_counter["rpc"]:
# Only seed the counter from the RPC/ws range.
# High p2p/beacon ports (30303+, 19000+) must not
# drive the counter up — otherwise each node's p2p
# port feeds back and inflates the next allocation
# until it overflows 65535.
if _port_counter["rpc"] <= host_port < 30000:
_port_counter["rpc"] = host_port + 10
_port_counter["ws"] = host_port + 11
except Exception:
@@ -820,10 +827,10 @@ async def start_node_container(node_id: str, node: dict):
_nodes[node_id]["container_id"] = exec_container.id[:12]
_nodes[node_id]["containers"] = [c.id[:12] for c in containers_created]
# ============ LUX ECOSYSTEM (Lux, Zoo, Pars, Hanzo) ============
# ============ LUX ECOSYSTEM (luxd — Lux, Zoo, Pars, Hanzo) ============
elif chain in ["lux", "zoo", "pars", "hanzo"]:
exec_images = chain_images.get("execution", {})
image = exec_images.get("luxd", "luxfi/luxd:latest")
image = exec_images.get("luxd", "ghcr.io/luxfi/node:latest")
try:
client.images.get(image)
@@ -835,47 +842,60 @@ async def start_node_container(node_id: str, node: dict):
try:
old = client.containers.get(container_name)
old.remove(force=True)
except:
except docker.errors.NotFound:
pass
# Configure luxd based on network
lux_network_id = "mainnet" if network == "mainnet" else "fuji" if network == "testnet" else "local"
# Canonical Lux primary network-id (matches luxd-boot.sh):
# localnet=1337, devnet=3, testnet=2, mainnet=1. Zoo/Pars/Hanzo are
# Lux-based sovereign L1s — same luxd boot; genesis selects the chain.
net_id = {"local": 1337, "localnet": 1337, "devnet": 3,
"testnet": 2, "mainnet": 1}.get(network, 1337)
# For local development, use special config
if network == "local":
lux_cmd = [
"--network-id=local",
"--http-host=0.0.0.0",
"--http-port=9650",
"--staking-enabled=false",
"--db-type=memdb",
# luxd serves C-Chain HTTP on 9630, staking on 9631 (not 9650).
# Custom VM plugins load from --plugin-dir (mount .so into /data/plugins).
lux_cmd = [
f"--network-id={net_id}",
"--http-host=0.0.0.0",
"--http-port=9630",
"--staking-port=9631",
"--data-dir=/data",
"--plugin-dir=/data/plugins",
]
# Single-node local/dev: run without a peer quorum. Real staking keys
# under /data/staking are picked up automatically when present (prod).
if network in ("local", "localnet", "devnet"):
lux_cmd += [
"--sybil-protection-enabled=false",
"--network-health-min-conn-peers=0",
"--skip-bootstrap=true",
]
else:
lux_cmd = [
f"--network-id={lux_network_id}",
"--http-host=0.0.0.0",
"--http-port=9650",
"--public-ip-resolution-service=opendns",
]
# Add subnet-specific flags for Zoo/Pars/Hanzo
if chain in ["zoo", "pars", "hanzo"]:
# These would have subnet-specific configs
lux_cmd.extend([
"--track-subnets=", # Add actual subnet IDs for production
])
lux_cmd.append("--public-ip-resolution-service=opendns")
# luxd loads VMs from --plugin-dir. The C-Chain EVM ships as a plugin
# (.so named by vmID under /luxd/build/plugins); custom VM plugins drop
# in the same dir. Stage the bundled plugins into the data plugin dir,
# then exec luxd — same shape as the canonical luxd-boot.sh. The image
# Cmd is ["./luxd"] with no Entrypoint, so we wrap in sh -c.
luxd_argv = " ".join(lux_cmd)
stage = "mkdir -p /data/plugins && cp -n /luxd/build/plugins/* /data/plugins/ 2>/dev/null; "
# Publish RPC always; publish staking only if it maps to a valid host
# port (guard against an out-of-range allocation).
lux_port_map = {"9630/tcp": ports["rpc"]}
staking_host = ports.get("p2p") or (ports["rpc"] + 1)
if 1024 < staking_host <= 65535:
lux_port_map["9631/tcp"] = staking_host
container = client.containers.run(
image,
command=lux_cmd,
entrypoint=["/bin/sh", "-c"],
# Single-list element: the whole script is one argv to `sh -c`
# (a bare string would be word-split by docker-py).
command=[f"{stage}exec /luxd/build/luxd {luxd_argv}"],
name=container_name,
detach=True,
ports={
"9650/tcp": ports["rpc"],
"9651/tcp": ports.get("p2p", 9651 + (ports["rpc"] - 9650)),
},
ports=lux_port_map,
volumes={
f"bootnode-data-{node_id[:8]}": {"bind": "/root/.luxd", "mode": "rw"},
f"bootnode-data-{node_id[:8]}": {"bind": "/data", "mode": "rw"},
},
network=network_name,
restart_policy={"Name": "unless-stopped"},
+91
View File
@@ -0,0 +1,91 @@
"""Subgraphs API — deploy and manage The Graph subgraphs on a graph-node.
Talks to graph-node's admin JSON-RPC (default http://graph-node:8020) for
create/deploy/remove, and its index-node status GraphQL (default
http://graph-node:8030/graphql) for indexing health. The graph-node + IPFS
stack runs via `docker compose --profile subgraph up` locally, or as a
launched-chain service in the cluster.
"""
import os
from typing import Any, Optional
import httpx
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from bootnode.api.deps import ApiKeyDep
router = APIRouter()
ADMIN_URL = os.getenv("GRAPH_NODE_ADMIN_URL", "http://graph-node:8020")
STATUS_URL = os.getenv("GRAPH_NODE_STATUS_URL", "http://graph-node:8030/graphql")
class CreateRequest(BaseModel):
name: str = Field(..., description="Subgraph name, e.g. 'lux/dex'")
class DeployRequest(BaseModel):
name: str
ipfs_hash: str = Field(..., description="IPFS hash of the compiled subgraph manifest")
version_label: Optional[str] = None
async def _admin(method: str, params: dict) -> Any:
"""Call a graph-node admin JSON-RPC method."""
payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
try:
async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.post(ADMIN_URL, json=payload)
except httpx.HTTPError as e:
raise HTTPException(502, f"graph-node admin unreachable at {ADMIN_URL}: {e}")
data = r.json()
if data.get("error"):
raise HTTPException(400, f"graph-node error: {data['error']}")
return data.get("result")
@router.post("/create")
async def create_subgraph(body: CreateRequest, api_key: ApiKeyDep) -> dict:
"""Register a subgraph name on the graph-node (subgraph_create)."""
result = await _admin("subgraph_create", {"name": body.name})
return {"name": body.name, "created": True, "result": result}
@router.post("/deploy")
async def deploy_subgraph(body: DeployRequest, api_key: ApiKeyDep) -> dict:
"""Deploy a compiled subgraph by IPFS hash (subgraph_deploy)."""
params: dict[str, str] = {"name": body.name, "ipfs_hash": body.ipfs_hash}
if body.version_label:
params["version_label"] = body.version_label
result = await _admin("subgraph_deploy", params)
return {
"name": body.name,
"ipfsHash": body.ipfs_hash,
"deployed": True,
"endpoints": result,
}
@router.delete("/{name:path}")
async def remove_subgraph(name: str, api_key: ApiKeyDep) -> dict:
"""Remove a subgraph (subgraph_remove)."""
result = await _admin("subgraph_remove", {"name": name})
return {"name": name, "removed": True, "result": result}
@router.get("")
async def list_subgraphs(api_key: ApiKeyDep) -> dict:
"""List indexing status for all deployed subgraphs (index-node status API)."""
query = {
"query": "{ indexingStatuses { subgraph synced health "
"chains { network chainHeadBlock { number } latestBlock { number } } } }"
}
try:
async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.post(STATUS_URL, json=query)
statuses = r.json().get("data", {}).get("indexingStatuses", [])
except (httpx.HTTPError, ValueError) as e:
raise HTTPException(502, f"graph-node status unreachable at {STATUS_URL}: {e}")
return {"subgraphs": statuses, "count": len(statuses)}
+4 -3
View File
@@ -17,6 +17,7 @@ import structlog
from fastapi import APIRouter
from pydantic import BaseModel
from bootnode import __version__
from bootnode.api.deps import ApiKeyDep
from bootnode.config import get_settings
@@ -44,7 +45,7 @@ class ZapServerInfo(BaseModel):
"""ZAP server capabilities."""
name: str = "bootnode"
version: str = "2.0.0"
version: str = __version__
capabilities: dict[str, bool]
tools_count: int
resources_count: int
@@ -388,7 +389,7 @@ async def get_zap_schema(_api_key: ApiKeyDep) -> dict[str, Any]:
return {
"schema": schema_content,
"format": "zap",
"version": "2.0.0",
"version": __version__,
"interfaces": ["Bootnode"],
"compile": "zapc compile bootnode.zap --out=bootnode.capnp",
}
@@ -410,5 +411,5 @@ async def get_capnp_schema(_api_key: ApiKeyDep) -> dict[str, Any]:
return {
"schema": schema_content,
"format": "capnp",
"version": "2.0.0",
"version": __version__,
}
+3 -1
View File
@@ -18,6 +18,8 @@ from typing import Any
import structlog
from fastapi import FastAPI, Request, Response
from bootnode import __version__
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
logger = structlog.get_logger()
@@ -166,7 +168,7 @@ def setup_otel(app: FastAPI) -> None:
"http://otel-collector.hanzo.svc:4317",
)
service_name = os.getenv("OTEL_SERVICE_NAME", "bootnode-api")
service_version = os.getenv("OTEL_SERVICE_VERSION", "2.0.0")
service_version = os.getenv("OTEL_SERVICE_VERSION", __version__)
# Always add metrics middleware (no external deps)
app.add_middleware(MetricsMiddleware)
+16 -8
View File
@@ -3,12 +3,16 @@
from bootnode.db.models import (
ApiKey,
Base,
Cluster,
MPCWalletModel,
Node,
GasPolicy,
OrgCluster,
Project,
SmartWallet,
Subscription,
TeamMember,
Usage,
User,
Webhook,
WebhookDelivery,
)
from bootnode.db.session import async_session, engine, get_db, init_db
@@ -18,11 +22,15 @@ __all__ = [
"async_session",
"init_db",
"Base",
"Cluster",
"Node",
"Project",
"ApiKey",
"MPCWalletModel",
"Webhook",
"GasPolicy",
"OrgCluster",
"Project",
"SmartWallet",
"Subscription",
"TeamMember",
"Usage",
"User",
"Webhook",
"WebhookDelivery",
]
+4 -3
View File
@@ -8,6 +8,7 @@ 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 redis_client
@@ -86,7 +87,7 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
app = FastAPI(
title="Bootnode API",
description="Blockchain Development Platform - Multi-chain RPC, Token API, NFT API, Smart Wallets, and more",
version="2.0.0",
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,
@@ -139,7 +140,7 @@ app.include_router(ws_router, prefix=f"{settings.api_prefix}/ws", tags=["WebSock
@app.get("/health")
async def health_check() -> dict:
"""Health check endpoint."""
return {"status": "healthy", "version": "2.0.0"}
return {"status": "healthy", "version": __version__}
@app.get("/")
@@ -147,7 +148,7 @@ async def root() -> dict:
"""Root endpoint."""
return {
"name": "Bootnode API",
"version": "2.0.0",
"version": __version__,
"docs": "/docs",
"status": "operational",
}
+3 -2
View File
@@ -15,6 +15,7 @@ from typing import Any
import capnp
import structlog
from bootnode import __version__
from bootnode.config import get_settings
logger = structlog.get_logger()
@@ -219,7 +220,7 @@ class BootnodeZapImpl(bootnode_capnp.Bootnode.Server):
return bootnode_capnp.ServerInfo.new_message(
name="bootnode",
version="2.0.0",
version=__version__,
capabilities=bootnode_capnp.ServerInfo.Capabilities.new_message(
tools=True,
resources=True,
@@ -385,7 +386,7 @@ class BootnodeZapImpl(bootnode_capnp.Bootnode.Server):
mimeType="application/json",
)
content_msg.content.text = json.dumps({
"version": "2.0.0",
"version": __version__,
"rate_limit": 1000,
})
return content_msg
+3 -6
View File
@@ -79,11 +79,8 @@ services:
environment:
QDRANT__SERVICE__GRPC_PORT: 6334
QDRANT__LOG_LEVEL: INFO
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:6333/"]
interval: 5s
timeout: 5s
retries: 5
# qdrant image ships no wget/curl; readiness is gated via depends_on:
# service_started + the api's own startup checks instead of a broken healthcheck.
# Meilisearch - Full-text search for addresses, tokens, contracts
meilisearch:
@@ -151,7 +148,7 @@ services:
redis:
condition: service_healthy
qdrant:
condition: service_healthy
condition: service_started
meilisearch:
condition: service_healthy
healthcheck:
+1 -1
View File
@@ -11,7 +11,7 @@ WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
RUN corepack enable pnpm && pnpm config set dangerously-allow-all-builds true && pnpm install --no-frozen-lockfile
# Rebuild the source code only when needed
FROM base AS builder
+6
View File
@@ -10,6 +10,7 @@ export type BrandConfig = {
logoWhite: string
favicon: string
domain: string
skillsOrg?: string // GitHub org for `npx skills add <org>/skills`
apiUrl: string
wsUrl: string
statusUrl: string
@@ -56,6 +57,7 @@ const brands: Record<string, BrandConfig> = {
logoWhite: "/logo/bootnode-logo-white.svg",
favicon: "/logo/bootnode-icon.svg",
domain: "bootno.de",
skillsOrg: "bootnode",
apiUrl: "https://api.bootno.de",
wsUrl: "wss://ws.bootno.de",
statusUrl: "https://status.bootno.de",
@@ -90,6 +92,7 @@ const brands: Record<string, BrandConfig> = {
logoWhite: "/logo/hanzo-logo-white.svg",
favicon: "/logo/hanzo-icon.svg",
domain: "web3.hanzo.ai",
skillsOrg: "hanzoai",
apiUrl: "https://api.cloud.hanzo.ai",
wsUrl: "wss://ws.cloud.hanzo.ai",
statusUrl: "https://status.hanzo.ai",
@@ -131,6 +134,7 @@ const brands: Record<string, BrandConfig> = {
logoWhite: "/logo/lux-wordmark-white.svg",
favicon: "/logo/lux-icon.svg",
domain: "cloud.lux.network",
skillsOrg: "luxfi",
apiUrl: "https://api.cloud.lux.network",
wsUrl: "wss://ws.cloud.lux.network",
statusUrl: "https://status.lux.network",
@@ -181,6 +185,7 @@ const brands: Record<string, BrandConfig> = {
logoWhite: "/logo/pars-logo-white.svg",
favicon: "/logo/pars-icon.svg",
domain: "cloud.pars.network",
skillsOrg: "pars-network",
apiUrl: "https://api.cloud.pars.network",
wsUrl: "wss://ws.cloud.pars.network",
statusUrl: "https://status.pars.network",
@@ -223,6 +228,7 @@ const brands: Record<string, BrandConfig> = {
logoWhite: "/logo/zoo-logo-white.svg",
favicon: "/logo/zoo-icon.svg",
domain: "web3.zoo.ngo",
skillsOrg: "zooai",
apiUrl: "https://api.web3.zoo.ngo",
wsUrl: "wss://ws.web3.zoo.ngo",
statusUrl: "https://status.zoo.ngo",
+7
View File
@@ -60,5 +60,12 @@
"postcss": "^8.5.3",
"tailwindcss": "^4.1.17",
"typescript": "^5.9.3"
},
"pnpm": {
"onlyBuiltDependencies": [
"@tailwindcss/oxide",
"esbuild",
"sharp"
]
}
}