api: the bundler is off unless BUNDLER_ENABLED; only then is BUNDLER_PRIVATE_KEY required
Hanzo CI/CD / test (linux-amd64) (push) Successful in 2m24s
Hanzo CI/CD / test (linux-arm64) (push) Successful in 6m31s
Hanzo CI/CD / cicd (push) Failing after 42m45s
CI/CD / cicd (push) Failing after 1h23m53s

Off, every /v1/bundler route answers 503. JWT_SECRET and API_KEY_SALT stay
required in production.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-23 14:48:54 -07:00
co-authored by Hanzo Dev
parent 98cf1b12e8
commit 1255ca0920
4 changed files with 72 additions and 4 deletions
+13 -2
View File
@@ -2,13 +2,24 @@
from typing import Any
from fastapi import APIRouter, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from bootnode.api.deps import ApiKeyDep
from bootnode.config import get_settings
from bootnode.core.chains import ChainRegistry, RPCClient
router = APIRouter()
def require_bundler() -> None:
"""Refuse every bundler call with 503 unless BUNDLER_ENABLED is set."""
if not get_settings().bundler_enabled:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="ERC-4337 bundler is disabled on this deployment",
)
router = APIRouter(dependencies=[Depends(require_bundler)])
# EntryPoint addresses
+3 -2
View File
@@ -136,7 +136,8 @@ class Settings(BaseSettings):
validation_alias=AliasChoices("commerce_webhook_secret", "hanzo_commerce_webhook_secret"),
)
# ERC-4337 Bundler
# ERC-4337 Bundler: off unless BUNDLER_ENABLED; on, it needs BUNDLER_PRIVATE_KEY
bundler_enabled: bool = False
bundler_private_key: str = ""
bundler_beneficiary: str = ""
@@ -173,7 +174,7 @@ class Settings(BaseSettings):
insecure.append("JWT_SECRET")
if self.api_key_salt == "change-me-in-production":
insecure.append("API_KEY_SALT")
if self.bundler_private_key == "":
if self.bundler_enabled and self.bundler_private_key == "":
insecure.append("BUNDLER_PRIVATE_KEY")
if insecure:
raise RuntimeError(
+54
View File
@@ -0,0 +1,54 @@
"""The ERC-4337 bundler is off unless BUNDLER_ENABLED, and only then needs a key."""
import pytest
from httpx import ASGITransport, AsyncClient
from bootnode.config import Settings, get_settings
from bootnode.main import app
BODY = {"jsonrpc": "2.0", "method": "eth_supportedEntryPoints", "params": [], "id": 1}
def production(**kw) -> Settings:
return Settings(_env_file=None, app_env="production", **kw)
def test_production_starts_without_bundler_key_when_bundler_off():
production(jwt_secret="j", api_key_salt="s").validate_production()
def test_production_requires_bundler_key_when_bundler_on():
s = production(jwt_secret="j", api_key_salt="s", bundler_enabled=True)
with pytest.raises(RuntimeError, match=r"secrets: BUNDLER_PRIVATE_KEY$"):
s.validate_production()
def test_production_accepts_bundler_on_with_key():
production(
jwt_secret="j", api_key_salt="s", bundler_enabled=True, bundler_private_key="0x1"
).validate_production()
def test_production_still_requires_jwt_secret_and_api_key_salt():
with pytest.raises(RuntimeError, match=r"secrets: JWT_SECRET, API_KEY_SALT$"):
production().validate_production()
@pytest.fixture
async def client():
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
yield ac
async def test_bundler_route_is_503_when_off(client: AsyncClient):
assert get_settings().bundler_enabled is False
for path in ("/v1/bundler/ethereum", "/v1/bundler/ethereum/sepolia"):
r = await client.post(path, json=BODY)
assert r.status_code == 503
assert r.json()["detail"] == "ERC-4337 bundler is disabled on this deployment"
async def test_bundler_route_reaches_auth_when_on(client: AsyncClient, monkeypatch):
monkeypatch.setattr(get_settings(), "bundler_enabled", True)
r = await client.post("/v1/bundler/ethereum", json=BODY)
assert r.status_code == 401
+2
View File
@@ -58,6 +58,8 @@ BTC_RPC_PASSWORD=bootnode_btc_secret
# =============================================================================
# ERC-4337 BUNDLER (Optional)
# =============================================================================
# Off by default: /v1/bundler answers 503. Enabling it requires BUNDLER_PRIVATE_KEY.
# BUNDLER_ENABLED=true
# Private key for bundler to submit UserOperations
# BUNDLER_PRIVATE_KEY=0x...