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
35 changes: 31 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,25 +111,51 @@ async def lifespan(app: FastAPI):
# 1. Infrastructure Setup
# =========================================================================

# Initialize the secrets manager and log the master account in BEFORE any Gateway
# client exists: hummingbot's GatewayHttpClient decrypts its mTLS client key with
# Security.secrets_manager.password, which stays None until a login. Logging in
# here (instead of lazily on first connector creation) means the status monitor's
# very first ping can never hit an uninitialized Security.
secrets_manager = ETHKeyFileSecretManger(password=settings.security.config_password)
if not BackendAPISecurity.login_account(account_name="master_account", secrets_manager=secrets_manager):
raise RuntimeError(
"CONFIG_PASSWORD does not match the stored password verification file at "
f"bots/{settings.app.password_verification_path}"
)

# Initialize GatewayHttpClient singleton
from utils.gateway_certs import certs_present, sync_client_certs_to_root
parsed_gateway_url = urlparse(settings.gateway.url)
gateway_use_ssl = parsed_gateway_url.scheme == "https"
if gateway_use_ssl:
# SEC-048: the in-process GatewayHttpClient reads its client certs only from
# root_path()/certs. Mirror the shared cert set there if the Gateway was already
# started in a previous run (no-op when certs haven't been generated yet).
from utils.gateway_certs import sync_client_certs_to_root
sync_client_certs_to_root()
gateway_config = GatewayConfigMap(
gateway_api_host=parsed_gateway_url.hostname or "localhost",
gateway_api_port=str(parsed_gateway_url.port or 15888),
gateway_use_ssl=gateway_use_ssl
)
GatewayHttpClient.get_instance(gateway_config)
gateway_client = GatewayHttpClient.get_instance(gateway_config)
# Start the Gateway status monitor so Gateway's network connectors (e.g.
# 'solana-mainnet-beta', and any newly added chain like 'ethereum-unichain') are
# discovered from /config/chains and registered in AllConnectorSettings. Without
# it, a Gateway network only lands in AllConnectorSettings lazily, when a connector
# for it is first constructed; the monitor makes new chains enumerable without
# first deploying a bot on them, and picks them up when Gateway comes online later.
# On a fresh install the shared mTLS certs don't exist until the Gateway is first
# started, and every 2s ping would fail loudly building the SSL context — defer;
# GatewayService.start() starts the monitor once the certs are generated.
if not gateway_use_ssl or certs_present():
gateway_client.start_monitor()
else:
logging.info(
"Gateway mTLS certs not generated yet; status monitor deferred until the Gateway is started"
)
logging.info(f"Initialized GatewayHttpClient with URL: {settings.gateway.url}")

# Initialize secrets manager and database
secrets_manager = ETHKeyFileSecretManger(password=settings.security.config_password)
# Initialize database
db_manager = AsyncDatabaseManager(settings.database.url)
await db_manager.create_tables()
logging.info("Database initialized")
Expand Down Expand Up @@ -309,6 +335,7 @@ async def lifespan(app: FastAPI):
await executor_service.stop()
market_data_service.stop()
await connector_service.stop_all()
GatewayHttpClient.get_instance().stop_monitor()
docker_service.cleanup()
await db_manager.close()

Expand Down
90 changes: 54 additions & 36 deletions routers/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@
from fastapi import APIRouter, Depends, HTTPException, Query

from deps import get_accounts_service, get_gateway_service
from models import (
AddPoolRequest,
AddTokenRequest,
GatewayConfig,
GatewayStatus,
UpdateApiKeysRequest,
)
from models import AddPoolRequest, AddTokenRequest, GatewayConfig, GatewayStatus, UpdateApiKeysRequest
from services.accounts_service import AccountsService
from services.gateway_client import GatewayError, check_gateway_error
from services.gateway_service import GatewayService

router = APIRouter(tags=["Gateway"], prefix="/gateway")
Expand Down Expand Up @@ -171,11 +166,13 @@ async def list_connectors(accounts_service: AccountsService = Depends(get_accoun
if not await accounts_service.gateway_client.ping():
raise HTTPException(status_code=503, detail="Gateway service is not available")

result = await accounts_service.gateway_client._request("GET", "config/connectors")
result = check_gateway_error(await accounts_service.gateway_client.get_connectors())
return normalize_gateway_response(result)

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error listing connectors: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error listing connectors: {str(e)}")

Expand All @@ -195,11 +192,13 @@ async def get_connector_config(
if not await accounts_service.gateway_client.ping():
raise HTTPException(status_code=503, detail="Gateway service is not available")

result = await accounts_service.gateway_client.get_config(connector_name)
result = check_gateway_error(await accounts_service.gateway_client.get_config(connector_name))
return normalize_gateway_response(result)

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error getting connector config: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting connector config: {str(e)}")

Expand Down Expand Up @@ -227,7 +226,9 @@ async def update_connector_config(
for path, value in config_updates.items():
# Convert snake_case to camelCase if needed
camel_path = snake_to_camel(path) if '_' in path else path
result = await accounts_service.gateway_client.update_config(connector_name, camel_path, value)
result = check_gateway_error(
await accounts_service.gateway_client.update_config(connector_name, camel_path, value)
)
results.append(result)

return {
Expand All @@ -240,6 +241,8 @@ async def update_connector_config(

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error updating connector config: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error updating connector config: {str(e)}")

Expand All @@ -266,11 +269,12 @@ async def get_api_keys(accounts_service: AccountsService = Depends(get_accounts_
if not await accounts_service.gateway_client.ping():
raise HTTPException(status_code=503, detail="Gateway service is not available")

result = await accounts_service.gateway_client.get_api_keys()
return result
return check_gateway_error(await accounts_service.gateway_client.get_api_keys())

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error getting API keys: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting API keys: {str(e)}")

Expand Down Expand Up @@ -336,11 +340,12 @@ async def list_chains(accounts_service: AccountsService = Depends(get_accounts_s
if not await accounts_service.gateway_client.ping():
raise HTTPException(status_code=503, detail="Gateway service is not available")

result = await accounts_service.gateway_client.get_chains()
return result
return check_gateway_error(await accounts_service.gateway_client.get_chains())

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error listing chains: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error listing chains: {str(e)}")

Expand Down Expand Up @@ -368,7 +373,9 @@ async def list_pools_legacy(
# This is a simple mapping - in production, you'd want to look this up
chain = "solana" if connector_name in ["raydium", "meteora", "orca", "pancakeswap-sol"] else "ethereum"

pools = await accounts_service.gateway_client.get_pools(chain, network, connector=connector_name)
pools = check_gateway_error(
await accounts_service.gateway_client.get_pools(chain, network, connector=connector_name)
)

if not pools:
return []
Expand All @@ -379,6 +386,8 @@ async def list_pools_legacy(

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error getting pools: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting pools: {str(e)}")

Expand All @@ -399,7 +408,7 @@ async def list_networks(accounts_service: AccountsService = Depends(get_accounts
if not await accounts_service.gateway_client.ping():
raise HTTPException(status_code=503, detail="Gateway service is not available")

chains_result = await accounts_service.gateway_client.get_chains()
chains_result = check_gateway_error(await accounts_service.gateway_client.get_chains())

# Flatten chain-network combinations into network IDs
networks = []
Expand All @@ -422,6 +431,8 @@ async def list_networks(accounts_service: AccountsService = Depends(get_accounts

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error listing networks: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error listing networks: {str(e)}")

Expand All @@ -443,11 +454,13 @@ async def get_network_config(
if not await accounts_service.gateway_client.ping():
raise HTTPException(status_code=503, detail="Gateway service is not available")

result = await accounts_service.gateway_client.get_config(network_id)
result = check_gateway_error(await accounts_service.gateway_client.get_config(network_id))
return normalize_gateway_response(result)

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error getting network config: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting network config: {str(e)}")

Expand Down Expand Up @@ -477,7 +490,9 @@ async def update_network_config(
for path, value in config_updates.items():
# Convert snake_case to camelCase if needed
camel_path = snake_to_camel(path) if '_' in path else path
result = await accounts_service.gateway_client.update_config(network_id, camel_path, value)
result = check_gateway_error(
await accounts_service.gateway_client.update_config(network_id, camel_path, value)
)
results.append(result)

return {
Expand All @@ -490,6 +505,8 @@ async def update_network_config(

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error updating network config: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error updating network config: {str(e)}")

Expand Down Expand Up @@ -519,7 +536,7 @@ async def get_network_tokens(
raise HTTPException(status_code=400, detail=f"Invalid network_id format: '{network_id}'. Use 'chain-network'")

chain, network = parts
result = await accounts_service.gateway_client.get_tokens(chain, network)
result = check_gateway_error(await accounts_service.gateway_client.get_tokens(chain, network))

# Apply search filter
if search and "tokens" in result:
Expand All @@ -534,6 +551,8 @@ async def get_network_tokens(

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error getting network tokens: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting network tokens: {str(e)}")

Expand Down Expand Up @@ -575,17 +594,14 @@ async def add_network_token(
# Use symbol as name if name is not provided
token_name = token_request.name if token_request.name else token_request.symbol

result = await accounts_service.gateway_client.add_token(
check_gateway_error(await accounts_service.gateway_client.add_token(
chain=chain,
network=network,
address=token_request.address,
symbol=token_request.symbol,
name=token_name,
decimals=token_request.decimals
)

if "error" in result:
raise HTTPException(status_code=400, detail=f"Failed to add token: {result.get('error')}")
))

return {
"success": True,
Expand All @@ -599,6 +615,8 @@ async def add_network_token(

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Failed to add token: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error adding token: {str(e)}")

Expand Down Expand Up @@ -632,14 +650,11 @@ async def save_network_token(

chain, network = parts

result = await accounts_service.gateway_client.save_token(
result = check_gateway_error(await accounts_service.gateway_client.save_token(
chain=chain,
network=network,
token_address=token_address
)

if "error" in result:
raise HTTPException(status_code=400, detail=f"Failed to save token: {result.get('error')}")
))

token_info = result.get("token", {})
return {
Expand All @@ -656,6 +671,8 @@ async def save_network_token(

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Failed to save token: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error saving token: {str(e)}")

Expand Down Expand Up @@ -688,14 +705,11 @@ async def delete_network_token(

chain, network = parts

result = await accounts_service.gateway_client.delete_token(
check_gateway_error(await accounts_service.gateway_client.delete_token(
chain=chain,
network=network,
token_address=token_address
)

if "error" in result:
raise HTTPException(status_code=400, detail=f"Failed to delete token: {result.get('error')}")
))

return {
"success": True,
Expand All @@ -706,6 +720,8 @@ async def delete_network_token(

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Failed to delete token: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error deleting token: {str(e)}")

Expand Down Expand Up @@ -743,13 +759,13 @@ async def get_network_pools(
raise HTTPException(status_code=400, detail=f"Invalid network_id format: '{network_id}'. Use 'chain-network'")

chain, network = parts
pools = await accounts_service.gateway_client.get_pools(
pools = check_gateway_error(await accounts_service.gateway_client.get_pools(
chain=chain,
network=network,
connector=connector,
pool_type=pool_type,
search=search
)
))

# Normalize each pool
normalized_pools = [normalize_gateway_response(pool) for pool in pools] if pools else []
Expand All @@ -762,6 +778,8 @@ async def get_network_pools(

except HTTPException:
raise
except GatewayError as e:
raise HTTPException(status_code=e.status, detail=f"Gateway error getting network pools: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting network pools: {str(e)}")

Expand Down
Loading
Loading