Skip to content
Merged
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
44 changes: 22 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 @@ -311,6 +310,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
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