fix: get the API booting (stale refactor imports + qdrant healthcheck)

The api crashed on import after a half-finished model refactor, and the qdrant
healthcheck used a binary not in the image.

- compose.yml: qdrant ships no wget/curl, so its healthcheck always failed and
  blocked the api (depends_on service_healthy). Dropped the broken healthcheck and
  gate the api on qdrant service_started (qdrant itself serves fine on :6333).
- db/__init__.py: reconcile to the real models — drop removed Node + Cluster
  (now CRDs) and MPCWalletModel; export the actual set (User, OrgCluster,
  SmartWallet, Subscription, TeamMember, etc.).
- api/mpc + api/keys: MPCWalletModel -> SmartWallet (the renamed model).

API now boots healthy: GET /health 200 {"status":"healthy","version":"2.0.0"},
/docs serving, 42 node/chain/fleet endpoints live (fleets deploy/scale, presets,
pod health/logs). Full local stack up: postgres, redis, clickhouse, qdrant,
meilisearch, api.

Note (follow-up, not blocking boot): api/nodes/db_nodes.py + db/seed_nodes.py
still import removed Node/Cluster models — dead code to remove or port to the
fleet/CRD path; the web frontend build fails on a stale pnpm lockfile.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-06-18 22:33:54 +00:00
co-authored by Hanzo Dev
parent a6c2aee6cf
commit 4f0c68df12
4 changed files with 52 additions and 47 deletions
+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()
+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()
+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",
]
+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: