Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ gateway-files/
# Hummingbot credentials and local data
bots/credentials/
bots/instances/
bots/conf/

# Local MCP configuration (project-specific overrides)
.mcp.json
Expand Down
45 changes: 23 additions & 22 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import logging
import secrets
from contextlib import asynccontextmanager
from typing import Annotated
from urllib.parse import urlparse

import logfire
import logging
from dotenv import load_dotenv

# Load environment variables early
Expand All @@ -22,49 +22,48 @@ def patched_save_to_yml(yml_path, cm):

# Apply the patch before importing hummingbot components
from hummingbot.client.config import config_helpers
config_helpers.save_to_yml = patched_save_to_yml

from hummingbot.core.rate_oracle.rate_oracle import RateOracle, RATE_ORACLE_SOURCES
from hummingbot.core.gateway.gateway_http_client import GatewayHttpClient
from hummingbot.client.config.client_config_map import GatewayConfigMap
config_helpers.save_to_yml = patched_save_to_yml

from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from hummingbot.data_feed.market_data_provider import MarketDataProvider
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from hummingbot.client.config.client_config_map import GatewayConfigMap
from hummingbot.client.config.config_crypt import ETHKeyFileSecretManger
from hummingbot.core.gateway.gateway_http_client import GatewayHttpClient
from hummingbot.core.rate_oracle.rate_oracle import RATE_ORACLE_SOURCES, RateOracle
from hummingbot.data_feed.market_data_provider import MarketDataProvider

from utils.security import BackendAPISecurity
from services.bots_orchestrator import BotsOrchestrator
from services.accounts_service import AccountsService
from services.docker_service import DockerService
from services.gateway_service import GatewayService
from services.market_data_feed_manager import MarketDataFeedManager
# from services.executor_service import ExecutorService
from utils.bot_archiver import BotArchiver
from routers import (
from config import settings
from routers import ( # executors,
accounts,
archived_bots,
backtesting,
bot_orchestration,
connectors,
controllers,
docker,
# executors,
gateway,
gateway_swap,
gateway_clmm,
gateway_proxy,
gateway_swap,
market_data,
portfolio,
rate_oracle,
scripts,
trading
trading,
)
from services.accounts_service import AccountsService
from services.bots_orchestrator import BotsOrchestrator
from services.docker_service import DockerService
from services.gateway_service import GatewayService
from services.market_data_feed_manager import MarketDataFeedManager

from config import settings

# from services.executor_service import ExecutorService
from utils.bot_archiver import BotArchiver
from utils.security import BackendAPISecurity

# Set up logging configuration
logging.basicConfig(
Expand Down Expand Up @@ -233,6 +232,7 @@ async def lifespan(app: FastAPI):
description="API for managing Hummingbot trading instances",
version=VERSION,
lifespan=lifespan,
redirect_slashes=False,
)

# Add CORS middleware
Expand Down Expand Up @@ -311,6 +311,7 @@ def auth_user(
app.include_router(backtesting.router, dependencies=[Depends(auth_user)])
app.include_router(archived_bots.router, dependencies=[Depends(auth_user)])
# app.include_router(executors.router, dependencies=[Depends(auth_user)])
app.include_router(gateway_proxy.router, dependencies=[Depends(auth_user)])

@app.get("/")
async def root():
Expand Down
2 changes: 1 addition & 1 deletion routers/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
router = APIRouter(tags=["Accounts"], prefix="/accounts")


@router.get("/", response_model=List[str])
@router.get("", response_model=List[str])
async def list_accounts(accounts_service: AccountsService = Depends(get_accounts_service)):
"""
Get a list of all account names in the system.
Expand Down
2 changes: 1 addition & 1 deletion routers/archived_bots.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
router = APIRouter(tags=["Archived Bots"], prefix="/archived-bots")


@router.get("/", response_model=List[str])
@router.get("", response_model=List[str])
async def list_databases():
"""
List all available database files in the system.
Expand Down
2 changes: 1 addition & 1 deletion routers/connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
router = APIRouter(tags=["Connectors"], prefix="/connectors")


@router.get("/", response_model=List[str])
@router.get("", response_model=List[str])
async def available_connectors():
"""
Get a list of all available connectors.
Expand Down
6 changes: 3 additions & 3 deletions routers/controllers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import yaml
from typing import Dict, List

import yaml
from fastapi import APIRouter, HTTPException
from starlette import status

Expand All @@ -11,7 +11,7 @@
router = APIRouter(tags=["Controllers"], prefix="/controllers")


@router.get("/", response_model=Dict[str, List[str]])
@router.get("", response_model=Dict[str, List[str]])
async def list_controllers():
"""
List all controllers organized by type.
Expand All @@ -33,7 +33,7 @@ async def list_controllers():


# Controller Configuration endpoints (must come before controller type routes)
@router.get("/configs/", response_model=List[Dict])
@router.get("/configs", response_model=List[Dict])
async def list_controller_configs():
"""
List all controller configurations with metadata.
Expand Down
12 changes: 6 additions & 6 deletions routers/docker.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import os

from fastapi import APIRouter, HTTPException, Depends
from fastapi import APIRouter, Depends, HTTPException

from deps import get_bot_archiver, get_docker_service
from models import DockerImage
from utils.bot_archiver import BotArchiver
from services.docker_service import DockerService
from deps import get_docker_service, get_bot_archiver
from utils.bot_archiver import BotArchiver

router = APIRouter(tags=["Docker"], prefix="/docker")

Expand All @@ -24,7 +24,7 @@ async def is_docker_running(docker_service: DockerService = Depends(get_docker_s
return docker_service.is_docker_running()


@router.get("/available-images/")
@router.get("/available-images")
async def available_images(image_name: str = None, docker_service: DockerService = Depends(get_docker_service)):
"""
Get available Docker images matching the specified name.
Expand Down Expand Up @@ -161,7 +161,7 @@ async def start_container(container_name: str, docker_service: DockerService = D
return docker_service.start_container(container_name)


@router.post("/pull-image/")
@router.post("/pull-image")
async def pull_image(image: DockerImage, docker_service: DockerService = Depends(get_docker_service)):
"""
Initiate Docker image pull as background task.
Expand All @@ -178,7 +178,7 @@ async def pull_image(image: DockerImage, docker_service: DockerService = Depends
return result


@router.get("/pull-status/")
@router.get("/pull-status")
async def get_pull_status(docker_service: DockerService = Depends(get_docker_service)):
"""
Get status of all pull operations.
Expand Down
131 changes: 131 additions & 0 deletions routers/gateway_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Gateway Proxy Router

Catch-all router that forwards requests to Gateway server unchanged.
Dashboard calls /api/gateway-proxy/* and this router forwards to Gateway at localhost:15888/*.

This allows the dashboard to access all Gateway endpoints through the API without
needing each endpoint to be explicitly defined.

Examples:
GET /api/gateway-proxy/wallet -> GET localhost:15888/wallet
POST /api/gateway-proxy/wallet/add -> POST localhost:15888/wallet/add
GET /api/gateway-proxy/config -> GET localhost:15888/config
GET /api/gateway-proxy/trading/clmm/positions-owned -> GET localhost:15888/trading/clmm/positions-owned
"""

import json
import logging

import aiohttp
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi.responses import JSONResponse

from deps import get_accounts_service
from services.accounts_service import AccountsService

logger = logging.getLogger(__name__)

router = APIRouter(tags=["Gateway Proxy"], prefix="/gateway-proxy")


@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def forward_to_gateway(
path: str,
request: Request,
accounts_service: AccountsService = Depends(get_accounts_service)
):
"""
Forward request to Gateway server unchanged.

This catch-all route forwards any request to /api/gateway-proxy/* to the Gateway server.
The request body, headers, and query parameters are passed through unchanged.
The response from Gateway is returned unchanged.

Examples:
GET /api/gateway-proxy/wallet -> GET localhost:15888/wallet
POST /api/gateway-proxy/wallet/add -> POST localhost:15888/wallet/add
GET /api/gateway-proxy/config -> GET localhost:15888/config
"""
gateway_client = accounts_service.gateway_client
gateway_url = gateway_client.base_url

# Build target URL
target_url = f"{gateway_url}/{path}"

# Get query parameters
query_params = dict(request.query_params)

# Get request body if present
body = None
if request.method in ["POST", "PUT", "PATCH", "DELETE"]:
try:
body = await request.json()
except Exception:
# No JSON body or invalid JSON - that's OK for some requests
body = None

try:
# Get or create aiohttp session
session = await gateway_client._get_session()

# Forward the request
async with session.request(
method=request.method,
url=target_url,
params=query_params if query_params else None,
json=body if body else None,
) as response:
# Read response body
response_body = await response.read()

# Try to parse as JSON, otherwise return as-is
content_type = response.headers.get("Content-Type", "")

if "application/json" in content_type:
try:
json_body = json.loads(response_body)
return JSONResponse(
content=json_body,
status_code=response.status,
)
except Exception:
pass

# Return raw response
return Response(
content=response_body,
status_code=response.status,
media_type=content_type or "application/octet-stream",
)

except aiohttp.ClientError as e:
logger.error(f"Gateway proxy error: {e}")
raise HTTPException(
status_code=503,
detail=f"Gateway service unavailable: {str(e)}"
)
except Exception as e:
logger.error(f"Gateway proxy error: {e}")
raise HTTPException(
status_code=500,
detail=f"Gateway proxy error: {str(e)}"
)


# Also expose the root endpoint for health checks
@router.get("")
async def gateway_root(
accounts_service: AccountsService = Depends(get_accounts_service)
):
"""
Gateway health check.
Forwards to Gateway root endpoint to check if it's online.
"""
gateway_client = accounts_service.gateway_client
result = await gateway_client._request("GET", "")
if result is None:
raise HTTPException(status_code=503, detail="Gateway service unavailable")
if "error" in result:
raise HTTPException(status_code=result.get("status", 500), detail=result["error"])
return result
6 changes: 3 additions & 3 deletions routers/scripts.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import yaml
from typing import Dict, List

import yaml
from fastapi import APIRouter, HTTPException
from starlette import status

Expand All @@ -11,7 +11,7 @@
router = APIRouter(tags=["Scripts"], prefix="/scripts")


@router.get("/", response_model=List[str])
@router.get("", response_model=List[str])
async def list_scripts():
"""
List all available scripts.
Expand All @@ -23,7 +23,7 @@ async def list_scripts():


# Script Configuration endpoints (must come before script name routes)
@router.get("/configs/", response_model=List[Dict])
@router.get("/configs", response_model=List[Dict])
async def list_script_configs():
"""
List all script configurations with metadata.
Expand Down