From c3bd6f1fcb5197028cf8a3b1c04ce44f149c033e Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 4 Mar 2026 12:56:05 -0300 Subject: [PATCH 01/15] fix: add explicit operation IDs to gateway proxy routes Split the single api_route handler into separate handlers for each HTTP method (GET, POST, PUT, DELETE, PATCH) with explicit operation_ids. This ensures unique operation IDs in the OpenAPI schema. Co-Authored-By: Claude Opus 4.5 --- routers/gateway_proxy.py | 78 +++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/routers/gateway_proxy.py b/routers/gateway_proxy.py index d735c137..a6a0b596 100644 --- a/routers/gateway_proxy.py +++ b/routers/gateway_proxy.py @@ -2,16 +2,16 @@ 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/*. +Dashboard calls /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 + GET /gateway-proxy/wallet -> GET localhost:15888/wallet + POST /gateway-proxy/wallet/add -> POST localhost:15888/wallet/add + GET /gateway-proxy/config -> GET localhost:15888/config + GET /gateway-proxy/trading/clmm/positions-owned -> GET localhost:15888/trading/clmm/positions-owned """ import json @@ -29,24 +29,12 @@ router = APIRouter(tags=["Gateway Proxy"], prefix="/gateway-proxy") -@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) -async def forward_to_gateway( +async def _forward_to_gateway( path: str, request: Request, - accounts_service: AccountsService = Depends(get_accounts_service) + accounts_service: AccountsService ): - """ - 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 - """ + """Internal handler that forwards requests to Gateway.""" gateway_client = accounts_service.gateway_client gateway_url = gateway_client.base_url @@ -113,6 +101,56 @@ async def forward_to_gateway( ) +@router.get("/{path:path}", operation_id="gateway_proxy_get") +async def gateway_proxy_get( + path: str, + request: Request, + accounts_service: AccountsService = Depends(get_accounts_service) +): + """GET request to Gateway. Example: GET /gateway-proxy/wallet""" + return await _forward_to_gateway(path, request, accounts_service) + + +@router.post("/{path:path}", operation_id="gateway_proxy_post") +async def gateway_proxy_post( + path: str, + request: Request, + accounts_service: AccountsService = Depends(get_accounts_service) +): + """POST request to Gateway. Example: POST /gateway-proxy/wallet/add""" + return await _forward_to_gateway(path, request, accounts_service) + + +@router.put("/{path:path}", operation_id="gateway_proxy_put") +async def gateway_proxy_put( + path: str, + request: Request, + accounts_service: AccountsService = Depends(get_accounts_service) +): + """PUT request to Gateway.""" + return await _forward_to_gateway(path, request, accounts_service) + + +@router.delete("/{path:path}", operation_id="gateway_proxy_delete") +async def gateway_proxy_delete( + path: str, + request: Request, + accounts_service: AccountsService = Depends(get_accounts_service) +): + """DELETE request to Gateway.""" + return await _forward_to_gateway(path, request, accounts_service) + + +@router.patch("/{path:path}", operation_id="gateway_proxy_patch") +async def gateway_proxy_patch( + path: str, + request: Request, + accounts_service: AccountsService = Depends(get_accounts_service) +): + """PATCH request to Gateway.""" + return await _forward_to_gateway(path, request, accounts_service) + + # Also expose the root endpoint for health checks @router.get("") async def gateway_root( From b9db532263f3c89f2a0bebff8e7079a09e378a49 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 11 Mar 2026 19:42:23 -0300 Subject: [PATCH 02/15] Add SwapExecutor support - Register SwapExecutor in EXECUTOR_REGISTRY - Add swap_executor to available executor types Requires: hummingbot/hummingbot#8117 Co-Authored-By: Claude Opus 4.5 --- routers/executors.py | 5 +++++ services/executor_service.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/routers/executors.py b/routers/executors.py index 8cb8545c..2b56fb99 100644 --- a/routers/executors.py +++ b/routers/executors.py @@ -241,6 +241,11 @@ async def get_available_executor_types(): "type": "lp_executor", "description": "LP position management for CLMM pools (Meteora, Raydium) ", "use_case": "Automated liquidity provision with position tracking" + }, + { + "type": "swap_executor", + "description": "Single swap execution on Gateway AMM connectors", + "use_case": "Executing swaps on DEXs like Jupiter with retry logic" } ] } diff --git a/services/executor_service.py b/services/executor_service.py index 13b24dc1..aaec2757 100644 --- a/services/executor_service.py +++ b/services/executor_service.py @@ -26,6 +26,8 @@ from hummingbot.strategy_v2.executors.order_executor.order_executor import OrderExecutor from hummingbot.strategy_v2.executors.position_executor.data_types import PositionExecutorConfig from hummingbot.strategy_v2.executors.position_executor.position_executor import PositionExecutor +from hummingbot.strategy_v2.executors.swap_executor.data_types import SwapExecutorConfig +from hummingbot.strategy_v2.executors.swap_executor.swap_executor import SwapExecutor from hummingbot.strategy_v2.executors.twap_executor.data_types import TWAPExecutorConfig from hummingbot.strategy_v2.executors.twap_executor.twap_executor import TWAPExecutor from hummingbot.strategy_v2.executors.xemm_executor.data_types import XEMMExecutorConfig @@ -82,6 +84,7 @@ class ExecutorService: "xemm_executor": (XEMMExecutor, XEMMExecutorConfig), "order_executor": (OrderExecutor, OrderExecutorConfig), "lp_executor": (LPExecutor, LPExecutorConfig), + "swap_executor": (SwapExecutor, SwapExecutorConfig), } def __init__( From f9f203dde0486ddd172ed7b943468e0126707215 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 12 Mar 2026 16:55:43 -0300 Subject: [PATCH 03/15] fix: remove misleading restart message for token add/delete Tokens are available immediately after adding - no Gateway restart needed. Co-Authored-By: Claude Opus 4.5 --- routers/gateway.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/routers/gateway.py b/routers/gateway.py index 8bca7309..6c599b87 100644 --- a/routers/gateway.py +++ b/routers/gateway.py @@ -1,19 +1,20 @@ -from fastapi import APIRouter, HTTPException, Depends, Query -from typing import Optional, Dict, List import re +from typing import Dict, List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query + +from deps import get_accounts_service, get_gateway_service from models import ( - GatewayConfig, - GatewayStatus, AddPoolRequest, AddTokenRequest, CreateWalletRequest, - ShowPrivateKeyRequest, + GatewayConfig, + GatewayStatus, SendTransactionRequest, + ShowPrivateKeyRequest, ) -from services.gateway_service import GatewayService from services.accounts_service import AccountsService -from deps import get_gateway_service, get_accounts_service +from services.gateway_service import GatewayService router = APIRouter(tags=["Gateway"], prefix="/gateway") @@ -605,9 +606,7 @@ async def add_network_token( return { "success": True, - "message": f"Token {token_request.symbol} added to {network_id}. Restart Gateway for changes to take effect.", - "restart_required": True, - "restart_endpoint": "POST /gateway/restart", + "message": f"Token {token_request.symbol} added to {network_id}.", "token": { "symbol": token_request.symbol, "address": token_request.address, @@ -660,9 +659,7 @@ async def delete_network_token( return { "success": True, - "message": f"Token {token_address} deleted from {network_id}. Restart Gateway for changes to take effect.", - "restart_required": True, - "restart_endpoint": "POST /gateway/restart", + "message": f"Token {token_address} deleted from {network_id}.", "token_address": token_address, "network_id": network_id } From e04c2a10c370014348e626ce847c9ab9502223b7 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Mar 2026 09:47:53 -0700 Subject: [PATCH 04/15] fix: skip Gateway placeholder wallet addresses when fetching balances Gateway templates use placeholder addresses like '' which cause errors when the API tries to fetch balances. Now detects and skips these placeholder patterns. Co-Authored-By: Claude Opus 4.5 --- services/accounts_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/accounts_service.py b/services/accounts_service.py index f3818190..404737c0 100644 --- a/services/accounts_service.py +++ b/services/accounts_service.py @@ -1932,6 +1932,11 @@ async def _update_gateway_balances(self, chain_networks: Optional[List[str]] = N logger.debug(f"Chain '{chain}' missing defaultWallet, skipping") continue + # Skip placeholder wallet addresses from Gateway templates (e.g., '') + if default_wallet.startswith("<") and default_wallet.endswith(">"): + logger.debug(f"Chain '{chain}' has placeholder defaultWallet '{default_wallet}', skipping") + continue + if not default_networks: # Fall back to defaultNetwork (singular) if defaultNetworks not set default_network = config.get("defaultNetwork") From 7dd6a39b4c9e814bf7f422f8bfca2c38ffe6e581 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Mar 2026 09:52:43 -0700 Subject: [PATCH 05/15] feat: add deploy-v2-script endpoint and set-default wallet support Issue #129: Restore deploy-v2-script endpoint - Add V2ScriptDeployment model for script-based bot deployment - Add POST /bot-orchestration/deploy-v2-script endpoint - Track bot runs in database for script deployments Issue #124: Add set-default wallet functionality - Add set_default parameter to POST /accounts/gateway/add-wallet (default: true) - Add POST /gateway/wallets/set-default endpoint to change default wallet - Add set_default_wallet method to GatewayClient Also: - Enable CLMM add/remove liquidity endpoints (uncommented) Co-Authored-By: Claude Opus 4.5 --- models/__init__.py | 4 + models/bot_orchestration.py | 10 + models/gateway.py | 7 + routers/accounts.py | 5 +- routers/bot_orchestration.py | 64 +++++- routers/gateway.py | 52 +++++ routers/gateway_clmm.py | 370 +++++++++++++++++------------------ services/accounts_service.py | 5 +- services/gateway_client.py | 7 + 9 files changed, 334 insertions(+), 190 deletions(-) diff --git a/models/__init__.py b/models/__init__.py index 62da9281..b000dd01 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -43,6 +43,7 @@ StopAndArchiveResponse, StopBotAction, V2ControllerDeployment, + V2ScriptDeployment, ) # Connector models @@ -84,6 +85,7 @@ GatewayWalletCredential, GatewayWalletInfo, SendTransactionRequest, + SetDefaultWalletRequest, ShowPrivateKeyRequest, ) @@ -213,6 +215,7 @@ "StopAndArchiveRequest", "StopAndArchiveResponse", "V2ControllerDeployment", + "V2ScriptDeployment", # Trading models "TradeRequest", "TradeResponse", @@ -282,6 +285,7 @@ "CreateWalletRequest", "ShowPrivateKeyRequest", "SendTransactionRequest", + "SetDefaultWalletRequest", "GatewayWalletCredential", "GatewayWalletInfo", "GatewayBalanceRequest", diff --git a/models/bot_orchestration.py b/models/bot_orchestration.py index d71252b0..a23dd242 100644 --- a/models/bot_orchestration.py +++ b/models/bot_orchestration.py @@ -94,6 +94,16 @@ class StopAndArchiveResponse(BaseModel): # Bot deployment models +class V2ScriptDeployment(BaseModel): + """Configuration for deploying a bot with a script""" + instance_name: str = Field(description="Unique name for the bot instance") + credentials_profile: str = Field(description="Name of the credentials profile to use") + image: str = Field(default="hummingbot/hummingbot:latest", description="Docker image for the Hummingbot instance") + script: Optional[str] = Field(default=None, description="Script name to run (without .py extension)") + script_config: Optional[str] = Field(default=None, description="Script configuration file name (without .yml extension)") + headless: bool = Field(default=False, description="Run in headless mode (no UI)") + + class V2ControllerDeployment(BaseModel): """Configuration for deploying a bot with controllers""" instance_name: str = Field(description="Unique name for the bot instance") diff --git a/models/gateway.py b/models/gateway.py index 11b97132..de9e5ddd 100644 --- a/models/gateway.py +++ b/models/gateway.py @@ -54,6 +54,7 @@ class GatewayWalletCredential(BaseModel): chain: str = Field(description="Blockchain chain (e.g., 'solana', 'ethereum')") private_key: str = Field(description="Wallet private key") network: Optional[str] = Field(default=None, description="Network to use (defaults to chain's default)") + set_default: bool = Field(default=True, description="Set as default wallet for this chain") class GatewayWalletInfo(BaseModel): @@ -63,6 +64,12 @@ class GatewayWalletInfo(BaseModel): network: str = Field(description="Network the wallet is configured for") +class SetDefaultWalletRequest(BaseModel): + """Request to set the default wallet for a chain""" + chain: str = Field(description="Blockchain chain (e.g., 'solana', 'ethereum')") + address: str = Field(description="Wallet address to set as default") + + # ============================================ # Pool and Token Management Models # ============================================ diff --git a/routers/accounts.py b/routers/accounts.py index 8d6de870..0c8bb785 100644 --- a/routers/accounts.py +++ b/routers/accounts.py @@ -171,7 +171,7 @@ async def add_gateway_wallet( Add a wallet to Gateway. Gateway handles encryption and storage internally. Args: - wallet_credential: Wallet credentials (chain and private_key) + wallet_credential: Wallet credentials (chain, private_key, and optional set_default) Returns: Wallet information from Gateway including address @@ -182,7 +182,8 @@ async def add_gateway_wallet( try: result = await accounts_service.add_gateway_wallet( chain=wallet_credential.chain, - private_key=wallet_credential.private_key + private_key=wallet_credential.private_key, + set_default=wallet_credential.set_default ) return result except HTTPException: diff --git a/routers/bot_orchestration.py b/routers/bot_orchestration.py index 5634d3eb..8224c25d 100644 --- a/routers/bot_orchestration.py +++ b/routers/bot_orchestration.py @@ -7,7 +7,7 @@ from database import AsyncDatabaseManager, BotRunRepository from deps import get_bot_archiver, get_bots_orchestrator, get_database_manager, get_docker_service -from models import StartBotAction, StopBotAction, V2ControllerDeployment +from models import StartBotAction, StopBotAction, V2ControllerDeployment, V2ScriptDeployment from services.bots_orchestrator import BotsOrchestrator from services.docker_service import DockerService from utils.bot_archiver import BotArchiver @@ -683,3 +683,65 @@ async def deploy_v2_controllers( except Exception as e: logging.error(f"Error deploying V2 controllers: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/deploy-v2-script") +async def deploy_v2_script( + deployment: V2ScriptDeployment, + docker_manager: DockerService = Depends(get_docker_service), + db_manager: AsyncDatabaseManager = Depends(get_database_manager) +): + """ + Deploy a V2 script bot with optional script configuration. + This endpoint creates and starts a Hummingbot instance running the specified script. + + Args: + deployment: V2ScriptDeployment configuration containing instance name, credentials, + optional script name and configuration + docker_manager: Docker service dependency + db_manager: Database manager dependency + + Returns: + Dictionary with deployment response including instance details + + Raises: + HTTPException: 500 if deployment fails + """ + try: + # Generate unique instance name with timestamp + timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") + unique_instance_name = f"{deployment.instance_name}-{timestamp}" + + # Update deployment with unique name + deployment.instance_name = unique_instance_name + + # Create the hummingbot instance + response = docker_manager.create_hummingbot_instance(deployment) + + if response.get("success"): + response["unique_instance_name"] = unique_instance_name + + # Track bot run if deployment was successful + try: + async with db_manager.get_session_context() as session: + bot_run_repo = BotRunRepository(session) + await bot_run_repo.create_bot_run( + bot_name=unique_instance_name, + instance_name=unique_instance_name, + strategy_type="script", + strategy_name=deployment.script or "default", + account_name=deployment.credentials_profile, + config_name=deployment.script_config, + image_version=deployment.image, + deployment_config=deployment.dict() + ) + logger.info(f"Created bot run record for script deployment {unique_instance_name}") + except Exception as e: + logger.error(f"Failed to create bot run record: {e}") + # Don't fail the deployment if bot run creation fails + + return response + + except Exception as e: + logging.error(f"Error deploying V2 script: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/routers/gateway.py b/routers/gateway.py index 8bca7309..250b2930 100644 --- a/routers/gateway.py +++ b/routers/gateway.py @@ -8,6 +8,7 @@ AddPoolRequest, AddTokenRequest, CreateWalletRequest, + SetDefaultWalletRequest, ShowPrivateKeyRequest, SendTransactionRequest, ) @@ -814,3 +815,54 @@ async def send_transaction( raise except Exception as e: raise HTTPException(status_code=500, detail=f"Error sending transaction: {str(e)}") + + +@router.post("/wallets/set-default") +async def set_default_wallet( + request: SetDefaultWalletRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +) -> Dict: + """ + Set the default wallet for a chain in Gateway. + + When multiple wallets are configured for a chain, this endpoint allows + switching which wallet is used as the default for operations. + + Args: + request: Contains chain and wallet address to set as default + + Returns: + Dict with success status and updated wallet info. + + Example: POST /gateway/wallets/set-default + { + "chain": "solana", + "address": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + } + """ + try: + 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.set_default_wallet( + chain=request.chain, + address=request.address + ) + + if result is None: + raise HTTPException(status_code=502, detail="Failed to set default wallet: Gateway returned no response") + + if "error" in result: + raise HTTPException(status_code=400, detail=f"Failed to set default wallet: {result.get('error')}") + + return { + "success": True, + "message": f"Set {request.address} as default wallet for {request.chain}", + "chain": request.chain, + "address": request.address + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error setting default wallet: {str(e)}") diff --git a/routers/gateway_clmm.py b/routers/gateway_clmm.py index 72292459..cb23e9d9 100644 --- a/routers/gateway_clmm.py +++ b/routers/gateway_clmm.py @@ -705,191 +705,191 @@ async def open_clmm_position( raise HTTPException(status_code=500, detail=f"Error opening CLMM position: {str(e)}") -# @router.post("/clmm/add") -# async def add_liquidity_to_clmm_position( -# request: CLMMAddLiquidityRequest, -# accounts_service: AccountsService = Depends(get_accounts_service), -# db_manager: AsyncDatabaseManager = Depends(get_database_manager) -# ): -# """ -# Add MORE liquidity to an EXISTING CLMM position. -# -# Example: -# connector: 'meteora' -# network: 'solana-mainnet-beta' -# position_address: '...' -# base_token_amount: 0.5 -# quote_token_amount: 50.0 -# slippage_pct: 1 -# wallet_address: (optional) -# -# Returns: -# Transaction hash -# """ -# try: -# if not await accounts_service.gateway_client.ping(): -# raise HTTPException(status_code=503, detail="Gateway service is not available") -# -# # Parse network_id -# chain, network = accounts_service.gateway_client.parse_network_id(request.network) -# -# # Get wallet address -# wallet_address = await accounts_service.gateway_client.get_wallet_address_or_default( -# chain=chain, -# wallet_address=request.wallet_address -# ) -# -# # Add liquidity to existing position -# result = await accounts_service.gateway_client.clmm_add_liquidity( -# connector=request.connector, -# network=network, -# wallet_address=wallet_address, -# position_address=request.position_address, -# base_token_amount=float(request.base_token_amount) if request.base_token_amount else None, -# quote_token_amount=float(request.quote_token_amount) if request.quote_token_amount else None, -# slippage_pct=float(request.slippage_pct) if request.slippage_pct else 1.0 -# ) -# -# transaction_hash = result.get("signature") or result.get("txHash") or result.get("hash") -# if not transaction_hash: -# raise HTTPException(status_code=500, detail="No transaction hash returned from Gateway") -# -# # Get transaction status from Gateway response -# tx_status = get_transaction_status_from_response(result) -# -# # Extract gas fee from Gateway response -# data = result.get("data", {}) -# gas_fee = data.get("fee") -# gas_token = "SOL" if chain == "solana" else "ETH" if chain == "ethereum" else None -# -# # Store ADD_LIQUIDITY event in database -# try: -# async with db_manager.get_session_context() as session: -# clmm_repo = GatewayCLMMRepository(session) -# -# # Get position to link event -# position = await clmm_repo.get_position_by_address(request.position_address) -# if position: -# event_data = { -# "position_id": position.id, -# "transaction_hash": transaction_hash, -# "event_type": "ADD_LIQUIDITY", -# "base_token_amount": float(request.base_token_amount) if request.base_token_amount else None, -# "quote_token_amount": float(request.quote_token_amount) if request.quote_token_amount else None, -# "gas_fee": float(gas_fee) if gas_fee else None, -# "gas_token": gas_token, -# "status": tx_status -# } -# await clmm_repo.create_event(event_data) -# logger.info(f"Recorded CLMM ADD_LIQUIDITY event: {transaction_hash} (status: {tx_status}, gas: {gas_fee} {gas_token})") -# except Exception as db_error: -# logger.error(f"Error recording ADD_LIQUIDITY event: {db_error}", exc_info=True) -# -# return { -# "transaction_hash": transaction_hash, -# "position_address": request.position_address, -# "status": "submitted" -# } -# -# except HTTPException: -# raise -# except ValueError as e: -# raise HTTPException(status_code=400, detail=str(e)) -# except Exception as e: -# logger.error(f"Error adding liquidity to CLMM position: {e}", exc_info=True) -# raise HTTPException(status_code=500, detail=f"Error adding liquidity to CLMM position: {str(e)}") -# -# -# @router.post("/clmm/remove") -# async def remove_liquidity_from_clmm_position( -# request: CLMMRemoveLiquidityRequest, -# accounts_service: AccountsService = Depends(get_accounts_service), -# db_manager: AsyncDatabaseManager = Depends(get_database_manager) -# ): -# """ -# Remove SOME liquidity from a CLMM position (partial removal). -# -# Example: -# connector: 'meteora' -# network: 'solana-mainnet-beta' -# position_address: '...' -# percentage: 50 -# wallet_address: (optional) -# -# Returns: -# Transaction hash -# """ -# try: -# if not await accounts_service.gateway_client.ping(): -# raise HTTPException(status_code=503, detail="Gateway service is not available") -# -# # Parse network_id -# chain, network = accounts_service.gateway_client.parse_network_id(request.network) -# -# # Get wallet address -# wallet_address = await accounts_service.gateway_client.get_wallet_address_or_default( -# chain=chain, -# wallet_address=request.wallet_address -# ) -# -# # Remove liquidity -# result = await accounts_service.gateway_client.clmm_remove_liquidity( -# connector=request.connector, -# network=network, -# wallet_address=wallet_address, -# position_address=request.position_address, -# percentage=float(request.percentage) -# ) -# -# transaction_hash = result.get("signature") or result.get("txHash") or result.get("hash") -# if not transaction_hash: -# raise HTTPException(status_code=500, detail="No transaction hash returned from Gateway") -# -# # Get transaction status from Gateway response -# tx_status = get_transaction_status_from_response(result) -# -# # Extract gas fee from Gateway response -# data = result.get("data", {}) -# gas_fee = data.get("fee") -# gas_token = "SOL" if chain == "solana" else "ETH" if chain == "ethereum" else None -# -# # Store REMOVE_LIQUIDITY event in database -# try: -# async with db_manager.get_session_context() as session: -# clmm_repo = GatewayCLMMRepository(session) -# -# # Get position to link event -# position = await clmm_repo.get_position_by_address(request.position_address) -# if position: -# event_data = { -# "position_id": position.id, -# "transaction_hash": transaction_hash, -# "event_type": "REMOVE_LIQUIDITY", -# "percentage": float(request.percentage), -# "gas_fee": float(gas_fee) if gas_fee else None, -# "gas_token": gas_token, -# "status": tx_status -# } -# await clmm_repo.create_event(event_data) -# logger.info(f"Recorded CLMM REMOVE_LIQUIDITY event: {transaction_hash} (status: {tx_status}, gas: {gas_fee} {gas_token})") -# except Exception as db_error: -# logger.error(f"Error recording REMOVE_LIQUIDITY event: {db_error}", exc_info=True) -# -# return { -# "transaction_hash": transaction_hash, -# "position_address": request.position_address, -# "percentage": float(request.percentage), -# "status": "submitted" -# } -# -# except HTTPException: -# raise -# except ValueError as e: -# raise HTTPException(status_code=400, detail=str(e)) -# except Exception as e: -# logger.error(f"Error removing liquidity from CLMM position: {e}", exc_info=True) -# raise HTTPException(status_code=500, detail=f"Error removing liquidity from CLMM position: {str(e)}") -# +@router.post("/clmm/add") +async def add_liquidity_to_clmm_position( + request: CLMMAddLiquidityRequest, + accounts_service: AccountsService = Depends(get_accounts_service), + db_manager: AsyncDatabaseManager = Depends(get_database_manager) +): + """ + Add MORE liquidity to an EXISTING CLMM position. + + Example: + connector: 'meteora' + network: 'solana-mainnet-beta' + position_address: '...' + base_token_amount: 0.5 + quote_token_amount: 50.0 + slippage_pct: 1 + wallet_address: (optional) + + Returns: + Transaction hash + """ + try: + if not await accounts_service.gateway_client.ping(): + raise HTTPException(status_code=503, detail="Gateway service is not available") + + # Parse network_id + chain, network = accounts_service.gateway_client.parse_network_id(request.network) + + # Get wallet address + wallet_address = await accounts_service.gateway_client.get_wallet_address_or_default( + chain=chain, + wallet_address=request.wallet_address + ) + + # Add liquidity to existing position + result = await accounts_service.gateway_client.clmm_add_liquidity( + connector=request.connector, + network=network, + wallet_address=wallet_address, + position_address=request.position_address, + base_token_amount=float(request.base_token_amount) if request.base_token_amount else None, + quote_token_amount=float(request.quote_token_amount) if request.quote_token_amount else None, + slippage_pct=float(request.slippage_pct) if request.slippage_pct else 1.0 + ) + + transaction_hash = result.get("signature") or result.get("txHash") or result.get("hash") + if not transaction_hash: + raise HTTPException(status_code=500, detail="No transaction hash returned from Gateway") + + # Get transaction status from Gateway response + tx_status = get_transaction_status_from_response(result) + + # Extract gas fee from Gateway response + data = result.get("data", {}) + gas_fee = data.get("fee") + gas_token = "SOL" if chain == "solana" else "ETH" if chain == "ethereum" else None + + # Store ADD_LIQUIDITY event in database + try: + async with db_manager.get_session_context() as session: + clmm_repo = GatewayCLMMRepository(session) + + # Get position to link event + position = await clmm_repo.get_position_by_address(request.position_address) + if position: + event_data = { + "position_id": position.id, + "transaction_hash": transaction_hash, + "event_type": "ADD_LIQUIDITY", + "base_token_amount": float(request.base_token_amount) if request.base_token_amount else None, + "quote_token_amount": float(request.quote_token_amount) if request.quote_token_amount else None, + "gas_fee": float(gas_fee) if gas_fee else None, + "gas_token": gas_token, + "status": tx_status + } + await clmm_repo.create_event(event_data) + logger.info(f"Recorded CLMM ADD_LIQUIDITY event: {transaction_hash} (status: {tx_status}, gas: {gas_fee} {gas_token})") + except Exception as db_error: + logger.error(f"Error recording ADD_LIQUIDITY event: {db_error}", exc_info=True) + + return { + "transaction_hash": transaction_hash, + "position_address": request.position_address, + "status": "submitted" + } + + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Error adding liquidity to CLMM position: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Error adding liquidity to CLMM position: {str(e)}") + + +@router.post("/clmm/remove") +async def remove_liquidity_from_clmm_position( + request: CLMMRemoveLiquidityRequest, + accounts_service: AccountsService = Depends(get_accounts_service), + db_manager: AsyncDatabaseManager = Depends(get_database_manager) +): + """ + Remove SOME liquidity from a CLMM position (partial removal). + + Example: + connector: 'meteora' + network: 'solana-mainnet-beta' + position_address: '...' + percentage: 50 + wallet_address: (optional) + + Returns: + Transaction hash + """ + try: + if not await accounts_service.gateway_client.ping(): + raise HTTPException(status_code=503, detail="Gateway service is not available") + + # Parse network_id + chain, network = accounts_service.gateway_client.parse_network_id(request.network) + + # Get wallet address + wallet_address = await accounts_service.gateway_client.get_wallet_address_or_default( + chain=chain, + wallet_address=request.wallet_address + ) + + # Remove liquidity + result = await accounts_service.gateway_client.clmm_remove_liquidity( + connector=request.connector, + network=network, + wallet_address=wallet_address, + position_address=request.position_address, + percentage=float(request.percentage) + ) + + transaction_hash = result.get("signature") or result.get("txHash") or result.get("hash") + if not transaction_hash: + raise HTTPException(status_code=500, detail="No transaction hash returned from Gateway") + + # Get transaction status from Gateway response + tx_status = get_transaction_status_from_response(result) + + # Extract gas fee from Gateway response + data = result.get("data", {}) + gas_fee = data.get("fee") + gas_token = "SOL" if chain == "solana" else "ETH" if chain == "ethereum" else None + + # Store REMOVE_LIQUIDITY event in database + try: + async with db_manager.get_session_context() as session: + clmm_repo = GatewayCLMMRepository(session) + + # Get position to link event + position = await clmm_repo.get_position_by_address(request.position_address) + if position: + event_data = { + "position_id": position.id, + "transaction_hash": transaction_hash, + "event_type": "REMOVE_LIQUIDITY", + "percentage": float(request.percentage), + "gas_fee": float(gas_fee) if gas_fee else None, + "gas_token": gas_token, + "status": tx_status + } + await clmm_repo.create_event(event_data) + logger.info(f"Recorded CLMM REMOVE_LIQUIDITY event: {transaction_hash} (status: {tx_status}, gas: {gas_fee} {gas_token})") + except Exception as db_error: + logger.error(f"Error recording REMOVE_LIQUIDITY event: {db_error}", exc_info=True) + + return { + "transaction_hash": transaction_hash, + "position_address": request.position_address, + "percentage": float(request.percentage), + "status": "submitted" + } + + except HTTPException: + raise + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Error removing liquidity from CLMM position: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Error removing liquidity from CLMM position: {str(e)}") + @router.post("/clmm/close", response_model=CLMMCollectFeesResponse) async def close_clmm_position( diff --git a/services/accounts_service.py b/services/accounts_service.py index 404737c0..4726dc19 100644 --- a/services/accounts_service.py +++ b/services/accounts_service.py @@ -2017,13 +2017,14 @@ async def get_gateway_wallets(self) -> List[Dict]: logger.error(f"Error getting Gateway wallets: {e}") raise HTTPException(status_code=500, detail=f"Failed to get wallets: {str(e)}") - async def add_gateway_wallet(self, chain: str, private_key: str) -> Dict: + async def add_gateway_wallet(self, chain: str, private_key: str, set_default: bool = True) -> Dict: """ Add a wallet to Gateway. Gateway handles encryption internally. Args: chain: Blockchain chain (e.g., 'solana', 'ethereum') private_key: Wallet private key + set_default: Set as default wallet for this chain (default: True) Returns: Dictionary with wallet information from Gateway @@ -2032,7 +2033,7 @@ async def add_gateway_wallet(self, chain: str, private_key: str) -> Dict: raise HTTPException(status_code=503, detail="Gateway service is not available") try: - result = await self.gateway_client.add_wallet(chain, private_key, set_default=True) + result = await self.gateway_client.add_wallet(chain, private_key, set_default=set_default) if "error" in result: raise HTTPException(status_code=400, detail=f"Gateway error: {result['error']}") diff --git a/services/gateway_client.py b/services/gateway_client.py index c43400bd..3188ffc9 100644 --- a/services/gateway_client.py +++ b/services/gateway_client.py @@ -199,6 +199,13 @@ async def remove_wallet(self, chain: str, address: str) -> Dict: "address": address }) + async def set_default_wallet(self, chain: str, address: str) -> Dict: + """Set the default wallet for a chain in Gateway""" + return await self._request("POST", "wallet/setDefault", json={ + "chain": chain, + "address": address + }) + async def get_balances(self, chain: str, network: str, address: str, tokens: Optional[List[str]] = None) -> Dict: """Get token balances for a wallet""" return await self._request("POST", f"chains/{chain}/balances", json={ From 84d69d926d6482023dfb31ae65304133d4caa6f3 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Mar 2026 09:58:40 -0700 Subject: [PATCH 06/15] refactor: move wallet management endpoints to /accounts/gateway/wallet/* Moved from /gateway/wallets/* to /accounts/gateway/wallet/*: - POST /accounts/gateway/wallet/create - POST /accounts/gateway/wallet/show-private-key - POST /accounts/gateway/wallet/send - POST /accounts/gateway/wallet/set-default This consolidates all account/wallet management under the Accounts router. Co-Authored-By: Claude Opus 4.5 --- routers/accounts.py | 200 +++++++++++++++++++++++++++++++++++++++++++- routers/gateway.py | 198 ------------------------------------------- 2 files changed, 199 insertions(+), 199 deletions(-) diff --git a/routers/accounts.py b/routers/accounts.py index 0c8bb785..555dd7f9 100644 --- a/routers/accounts.py +++ b/routers/accounts.py @@ -6,7 +6,15 @@ from services.accounts_service import AccountsService from deps import get_accounts_service -from models import PaginatedResponse, GatewayWalletCredential, GatewayWalletInfo +from models import ( + PaginatedResponse, + GatewayWalletCredential, + GatewayWalletInfo, + CreateWalletRequest, + ShowPrivateKeyRequest, + SendTransactionRequest, + SetDefaultWalletRequest, +) router = APIRouter(tags=["Accounts"], prefix="/accounts") @@ -220,3 +228,193 @@ async def remove_gateway_wallet( raise HTTPException(status_code=500, detail=str(e)) +@router.post("/gateway/wallet/create", status_code=status.HTTP_201_CREATED) +async def create_gateway_wallet( + request: CreateWalletRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +) -> Dict: + """ + Create a new wallet in Gateway. + + Args: + request: Contains chain and set_default flag + + Returns: + Dict with address and chain of the created wallet. + + Example: POST /accounts/gateway/wallet/create + { + "chain": "solana", + "set_default": true + } + """ + try: + 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.create_wallet( + chain=request.chain, + set_default=request.set_default + ) + + if result is None: + raise HTTPException(status_code=502, detail="Failed to create wallet: Gateway returned no response") + + if "error" in result: + raise HTTPException(status_code=400, detail=f"Failed to create wallet: {result.get('error')}") + + return result + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating wallet: {str(e)}") + + +@router.post("/gateway/wallet/show-private-key") +async def show_gateway_wallet_private_key( + request: ShowPrivateKeyRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +) -> Dict: + """ + Show private key for a wallet. + + WARNING: This endpoint exposes sensitive information. Use with caution. + + Args: + request: Contains chain, address, and passphrase + + Returns: + Dict with privateKey field. + + Example: POST /accounts/gateway/wallet/show-private-key + { + "chain": "solana", + "address": "", + "passphrase": "" + } + """ + try: + 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.show_private_key( + chain=request.chain, + address=request.address, + passphrase=request.passphrase + ) + + if result is None: + raise HTTPException(status_code=502, detail="Failed to retrieve private key: Gateway returned no response") + + if "error" in result: + raise HTTPException(status_code=400, detail=f"Failed to retrieve private key: {result.get('error')}") + + return result + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error retrieving private key: {str(e)}") + + +@router.post("/gateway/wallet/send") +async def send_gateway_wallet_transaction( + request: SendTransactionRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +) -> Dict: + """ + Send a native token transaction. + + Args: + request: Contains chain, network, sender address, recipient address, and amount + + Returns: + Dict with transaction signature/hash. + + Example: POST /accounts/gateway/wallet/send + { + "chain": "solana", + "network": "mainnet-beta", + "address": "", + "to_address": "", + "amount": "0.001" + } + """ + try: + 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.send_transaction( + chain=request.chain, + network=request.network, + address=request.address, + to_address=request.to_address, + amount=request.amount + ) + + if result is None: + raise HTTPException(status_code=502, detail="Failed to send transaction: Gateway returned no response") + + if "error" in result: + raise HTTPException(status_code=400, detail=f"Failed to send transaction: {result.get('error')}") + + return result + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error sending transaction: {str(e)}") + + +@router.post("/gateway/wallet/set-default") +async def set_default_gateway_wallet( + request: SetDefaultWalletRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +) -> Dict: + """ + Set the default wallet for a chain in Gateway. + + When multiple wallets are configured for a chain, this endpoint allows + switching which wallet is used as the default for operations. + + Args: + request: Contains chain and wallet address to set as default + + Returns: + Dict with success status and updated wallet info. + + Example: POST /accounts/gateway/wallet/set-default + { + "chain": "solana", + "address": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + } + """ + try: + 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.set_default_wallet( + chain=request.chain, + address=request.address + ) + + if result is None: + raise HTTPException(status_code=502, detail="Failed to set default wallet: Gateway returned no response") + + if "error" in result: + raise HTTPException(status_code=400, detail=f"Failed to set default wallet: {result.get('error')}") + + return { + "success": True, + "message": f"Set {request.address} as default wallet for {request.chain}", + "chain": request.chain, + "address": request.address + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error setting default wallet: {str(e)}") + + diff --git a/routers/gateway.py b/routers/gateway.py index 250b2930..3e622be9 100644 --- a/routers/gateway.py +++ b/routers/gateway.py @@ -7,10 +7,6 @@ GatewayStatus, AddPoolRequest, AddTokenRequest, - CreateWalletRequest, - SetDefaultWalletRequest, - ShowPrivateKeyRequest, - SendTransactionRequest, ) from services.gateway_service import GatewayService from services.accounts_service import AccountsService @@ -672,197 +668,3 @@ async def delete_network_token( raise except Exception as e: raise HTTPException(status_code=500, detail=f"Error deleting token: {str(e)}") - - -# ============================================ -# Wallet Management -# ============================================ - -@router.post("/wallets/create") -async def create_wallet( - request: CreateWalletRequest, - accounts_service: AccountsService = Depends(get_accounts_service) -) -> Dict: - """ - Create a new wallet in Gateway. - - Args: - request: Contains chain and set_default flag - - Returns: - Dict with address and chain of the created wallet. - - Example: POST /gateway/wallets/create - { - "chain": "solana", - "set_default": true - } - """ - try: - 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.create_wallet( - chain=request.chain, - set_default=request.set_default - ) - - if result is None: - raise HTTPException(status_code=502, detail="Failed to create wallet: Gateway returned no response") - - if "error" in result: - raise HTTPException(status_code=400, detail=f"Failed to create wallet: {result.get('error')}") - - return result - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error creating wallet: {str(e)}") - - -@router.post("/wallets/show-private-key") -async def show_private_key( - request: ShowPrivateKeyRequest, - accounts_service: AccountsService = Depends(get_accounts_service) -) -> Dict: - """ - Show private key for a wallet. - - WARNING: This endpoint exposes sensitive information. Use with caution. - - Args: - request: Contains chain, address, and passphrase - - Returns: - Dict with privateKey field. - - Example: POST /gateway/wallets/show-private-key - { - "chain": "solana", - "address": "", - "passphrase": "" - } - """ - try: - 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.show_private_key( - chain=request.chain, - address=request.address, - passphrase=request.passphrase - ) - - if result is None: - raise HTTPException(status_code=502, detail="Failed to retrieve private key: Gateway returned no response") - - if "error" in result: - raise HTTPException(status_code=400, detail=f"Failed to retrieve private key: {result.get('error')}") - - return result - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error retrieving private key: {str(e)}") - - -@router.post("/wallets/send") -async def send_transaction( - request: SendTransactionRequest, - accounts_service: AccountsService = Depends(get_accounts_service) -) -> Dict: - """ - Send a native token transaction. - - Args: - request: Contains chain, network, sender address, recipient address, and amount - - Returns: - Dict with transaction signature/hash. - - Example: POST /gateway/wallets/send - { - "chain": "solana", - "network": "mainnet-beta", - "address": "", - "to_address": "", - "amount": "0.001" - } - """ - try: - 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.send_transaction( - chain=request.chain, - network=request.network, - address=request.address, - to_address=request.to_address, - amount=request.amount - ) - - if result is None: - raise HTTPException(status_code=502, detail="Failed to send transaction: Gateway returned no response") - - if "error" in result: - raise HTTPException(status_code=400, detail=f"Failed to send transaction: {result.get('error')}") - - return result - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error sending transaction: {str(e)}") - - -@router.post("/wallets/set-default") -async def set_default_wallet( - request: SetDefaultWalletRequest, - accounts_service: AccountsService = Depends(get_accounts_service) -) -> Dict: - """ - Set the default wallet for a chain in Gateway. - - When multiple wallets are configured for a chain, this endpoint allows - switching which wallet is used as the default for operations. - - Args: - request: Contains chain and wallet address to set as default - - Returns: - Dict with success status and updated wallet info. - - Example: POST /gateway/wallets/set-default - { - "chain": "solana", - "address": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" - } - """ - try: - 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.set_default_wallet( - chain=request.chain, - address=request.address - ) - - if result is None: - raise HTTPException(status_code=502, detail="Failed to set default wallet: Gateway returned no response") - - if "error" in result: - raise HTTPException(status_code=400, detail=f"Failed to set default wallet: {result.get('error')}") - - return { - "success": True, - "message": f"Set {request.address} as default wallet for {request.chain}", - "chain": request.chain, - "address": request.address - } - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error setting default wallet: {str(e)}") From 33216e5e27983ee48c176bb73be0e60476010116 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Mar 2026 10:00:48 -0700 Subject: [PATCH 07/15] chore: remove LP-FIX debug prints from main.py Replace debug print statements with proper logging. Co-Authored-By: Claude Opus 4.5 --- main.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index b53ede81..0f2d996b 100644 --- a/main.py +++ b/main.py @@ -215,13 +215,10 @@ async def lifespan(app: FastAPI): try: from hummingbot.strategy_v2.executors.lp_executor.data_types import LPExecutorConfig from hummingbot.strategy_v2.executors.lp_executor.lp_executor import LPExecutor - print(f"[LP-FIX] imports OK. Registry before: {list(ExecutorService.EXECUTOR_REGISTRY.keys())}", flush=True) ExecutorService.EXECUTOR_REGISTRY["lp_executor"] = (LPExecutor, LPExecutorConfig) - print(f"[LP-FIX] Registry after: {list(ExecutorService.EXECUTOR_REGISTRY.keys())}", flush=True) + logging.debug("lp_executor registered in ExecutorService") except Exception as e: - import traceback - print(f"[LP-FIX] FAILED: {e}", flush=True) - traceback.print_exc() + logging.warning(f"Failed to register lp_executor: {e}") # ========================================================================= # 5. Other Services From 4cbc6786e69045e67088dd64f037f03b3e237abf Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Mar 2026 10:04:00 -0700 Subject: [PATCH 08/15] refactor: rename /gateway/add-wallet to /gateway/wallet/add Consistent endpoint naming for all wallet operations under /accounts/gateway/wallet/*. Co-Authored-By: Claude Opus 4.5 --- routers/accounts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/routers/accounts.py b/routers/accounts.py index 555dd7f9..0af7373c 100644 --- a/routers/accounts.py +++ b/routers/accounts.py @@ -170,13 +170,14 @@ async def list_gateway_wallets(accounts_service: AccountsService = Depends(get_a raise HTTPException(status_code=500, detail=str(e)) -@router.post("/gateway/add-wallet", status_code=status.HTTP_201_CREATED) +@router.post("/gateway/wallet/add", status_code=status.HTTP_201_CREATED) async def add_gateway_wallet( wallet_credential: GatewayWalletCredential, accounts_service: AccountsService = Depends(get_accounts_service) ): """ - Add a wallet to Gateway. Gateway handles encryption and storage internally. + Add an existing wallet to Gateway using its private key. + Gateway handles encryption and storage internally. Args: wallet_credential: Wallet credentials (chain, private_key, and optional set_default) @@ -416,5 +417,3 @@ async def set_default_gateway_wallet( raise except Exception as e: raise HTTPException(status_code=500, detail=f"Error setting default wallet: {str(e)}") - - From c8fd223fa7a63ad8f2786dec98a88c7eb38d71e3 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Mar 2026 10:15:51 -0700 Subject: [PATCH 09/15] refactor: rename DELETE /gateway/{chain}/{address} to /gateway/wallet/{chain}/{address} Consistent endpoint naming for all wallet operations. Co-Authored-By: Claude Opus 4.5 --- routers/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/accounts.py b/routers/accounts.py index 0af7373c..d4bcdb6c 100644 --- a/routers/accounts.py +++ b/routers/accounts.py @@ -201,7 +201,7 @@ async def add_gateway_wallet( raise HTTPException(status_code=500, detail=str(e)) -@router.delete("/gateway/{chain}/{address}") +@router.delete("/gateway/wallet/{chain}/{address}") async def remove_gateway_wallet( chain: str, address: str, From a15a26d1c5ebdbb44cc23ec9c536a6cb66ea80b4 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Mar 2026 10:17:37 -0700 Subject: [PATCH 10/15] refactor: reorder wallet endpoints - set-default after add Logical grouping: add -> set-default -> delete -> create -> show-private-key -> send Co-Authored-By: Claude Opus 4.5 --- routers/accounts.py | 102 ++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/routers/accounts.py b/routers/accounts.py index d4bcdb6c..74a23926 100644 --- a/routers/accounts.py +++ b/routers/accounts.py @@ -201,6 +201,57 @@ async def add_gateway_wallet( raise HTTPException(status_code=500, detail=str(e)) +@router.post("/gateway/wallet/set-default") +async def set_default_gateway_wallet( + request: SetDefaultWalletRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +) -> Dict: + """ + Set the default wallet for a chain in Gateway. + + When multiple wallets are configured for a chain, this endpoint allows + switching which wallet is used as the default for operations. + + Args: + request: Contains chain and wallet address to set as default + + Returns: + Dict with success status and updated wallet info. + + Example: POST /accounts/gateway/wallet/set-default + { + "chain": "solana", + "address": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + } + """ + try: + 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.set_default_wallet( + chain=request.chain, + address=request.address + ) + + if result is None: + raise HTTPException(status_code=502, detail="Failed to set default wallet: Gateway returned no response") + + if "error" in result: + raise HTTPException(status_code=400, detail=f"Failed to set default wallet: {result.get('error')}") + + return { + "success": True, + "message": f"Set {request.address} as default wallet for {request.chain}", + "chain": request.chain, + "address": request.address + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error setting default wallet: {str(e)}") + + @router.delete("/gateway/wallet/{chain}/{address}") async def remove_gateway_wallet( chain: str, @@ -366,54 +417,3 @@ async def send_gateway_wallet_transaction( raise except Exception as e: raise HTTPException(status_code=500, detail=f"Error sending transaction: {str(e)}") - - -@router.post("/gateway/wallet/set-default") -async def set_default_gateway_wallet( - request: SetDefaultWalletRequest, - accounts_service: AccountsService = Depends(get_accounts_service) -) -> Dict: - """ - Set the default wallet for a chain in Gateway. - - When multiple wallets are configured for a chain, this endpoint allows - switching which wallet is used as the default for operations. - - Args: - request: Contains chain and wallet address to set as default - - Returns: - Dict with success status and updated wallet info. - - Example: POST /accounts/gateway/wallet/set-default - { - "chain": "solana", - "address": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" - } - """ - try: - 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.set_default_wallet( - chain=request.chain, - address=request.address - ) - - if result is None: - raise HTTPException(status_code=502, detail="Failed to set default wallet: Gateway returned no response") - - if "error" in result: - raise HTTPException(status_code=400, detail=f"Failed to set default wallet: {result.get('error')}") - - return { - "success": True, - "message": f"Set {request.address} as default wallet for {request.chain}", - "chain": request.chain, - "address": request.address - } - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error setting default wallet: {str(e)}") From a8774c8633eeb5d1273d9e2167457fccd0e9399f Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Mon, 23 Mar 2026 10:19:30 -0700 Subject: [PATCH 11/15] fix: remove unused network field from GatewayWalletCredential Co-Authored-By: Claude Opus 4.5 --- models/gateway.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/models/gateway.py b/models/gateway.py index de9e5ddd..849acd4c 100644 --- a/models/gateway.py +++ b/models/gateway.py @@ -50,10 +50,9 @@ class SendTransactionRequest(BaseModel): class GatewayWalletCredential(BaseModel): - """Credentials for connecting a Gateway wallet""" + """Credentials for adding an existing wallet to Gateway""" chain: str = Field(description="Blockchain chain (e.g., 'solana', 'ethereum')") private_key: str = Field(description="Wallet private key") - network: Optional[str] = Field(default=None, description="Network to use (defaults to chain's default)") set_default: bool = Field(default=True, description="Set as default wallet for this chain") From 24ee7256326705d20f48fc8301b38d094e9a20cd Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 26 Mar 2026 18:06:13 -0700 Subject: [PATCH 12/15] revert: move wallet endpoints back to original locations Reverts the endpoint moves from PR #131 to save for a different PR: - POST /gateway/wallets/create (stays in gateway.py) - POST /gateway/wallets/show-private-key (stays in gateway.py) - POST /gateway/wallets/send (stays in gateway.py) - GET /accounts/gateway/wallets (back to accounts.py) - POST /accounts/gateway/add-wallet (back to accounts.py) - DELETE /accounts/gateway/{chain}/{address} (back to accounts.py) - POST /accounts/gateway/wallet/set-default (new, in accounts.py) Co-Authored-By: Claude Opus 4.5 --- routers/accounts.py | 180 ++++---------------------------------------- routers/gateway.py | 159 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 169 insertions(+), 170 deletions(-) diff --git a/routers/accounts.py b/routers/accounts.py index 74a23926..a5e4106a 100644 --- a/routers/accounts.py +++ b/routers/accounts.py @@ -1,20 +1,11 @@ -from typing import Dict, List, Optional -from datetime import datetime +from typing import Dict, List -from fastapi import APIRouter, HTTPException, Depends, Query +from fastapi import APIRouter, Depends, HTTPException from starlette import status -from services.accounts_service import AccountsService from deps import get_accounts_service -from models import ( - PaginatedResponse, - GatewayWalletCredential, - GatewayWalletInfo, - CreateWalletRequest, - ShowPrivateKeyRequest, - SendTransactionRequest, - SetDefaultWalletRequest, -) +from models import GatewayWalletCredential, SetDefaultWalletRequest +from services.accounts_service import AccountsService router = APIRouter(tags=["Accounts"], prefix="/accounts") @@ -23,7 +14,7 @@ async def list_accounts(accounts_service: AccountsService = Depends(get_accounts_service)): """ Get a list of all account names in the system. - + Returns: List of account names """ @@ -59,13 +50,13 @@ async def list_account_credentials(account_name: str, async def add_account(account_name: str, accounts_service: AccountsService = Depends(get_accounts_service)): """ Create a new account with default configuration files. - + Args: account_name: Name of the new account to create - + Returns: Success message when account is created - + Raises: HTTPException: 400 if account already exists """ @@ -80,13 +71,13 @@ async def add_account(account_name: str, accounts_service: AccountsService = Dep async def delete_account(account_name: str, accounts_service: AccountsService = Depends(get_accounts_service)): """ Delete an account and all its associated credentials. - + Args: account_name: Name of the account to delete - + Returns: Success message when account is deleted - + Raises: HTTPException: 400 if trying to delete master account, 404 if account not found """ @@ -103,14 +94,14 @@ async def delete_account(account_name: str, accounts_service: AccountsService = async def delete_credential(account_name: str, connector_name: str, accounts_service: AccountsService = Depends(get_accounts_service)): """ Delete a specific connector credential for an account. - + Args: account_name: Name of the account connector_name: Name of the connector to delete credentials for - + Returns: Success message when credential is deleted - + Raises: HTTPException: 404 if credential not found """ @@ -170,7 +161,7 @@ async def list_gateway_wallets(accounts_service: AccountsService = Depends(get_a raise HTTPException(status_code=500, detail=str(e)) -@router.post("/gateway/wallet/add", status_code=status.HTTP_201_CREATED) +@router.post("/gateway/add-wallet", status_code=status.HTTP_201_CREATED) async def add_gateway_wallet( wallet_credential: GatewayWalletCredential, accounts_service: AccountsService = Depends(get_accounts_service) @@ -252,7 +243,7 @@ async def set_default_gateway_wallet( raise HTTPException(status_code=500, detail=f"Error setting default wallet: {str(e)}") -@router.delete("/gateway/wallet/{chain}/{address}") +@router.delete("/gateway/{chain}/{address}") async def remove_gateway_wallet( chain: str, address: str, @@ -278,142 +269,3 @@ async def remove_gateway_wallet( raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) - - -@router.post("/gateway/wallet/create", status_code=status.HTTP_201_CREATED) -async def create_gateway_wallet( - request: CreateWalletRequest, - accounts_service: AccountsService = Depends(get_accounts_service) -) -> Dict: - """ - Create a new wallet in Gateway. - - Args: - request: Contains chain and set_default flag - - Returns: - Dict with address and chain of the created wallet. - - Example: POST /accounts/gateway/wallet/create - { - "chain": "solana", - "set_default": true - } - """ - try: - 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.create_wallet( - chain=request.chain, - set_default=request.set_default - ) - - if result is None: - raise HTTPException(status_code=502, detail="Failed to create wallet: Gateway returned no response") - - if "error" in result: - raise HTTPException(status_code=400, detail=f"Failed to create wallet: {result.get('error')}") - - return result - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error creating wallet: {str(e)}") - - -@router.post("/gateway/wallet/show-private-key") -async def show_gateway_wallet_private_key( - request: ShowPrivateKeyRequest, - accounts_service: AccountsService = Depends(get_accounts_service) -) -> Dict: - """ - Show private key for a wallet. - - WARNING: This endpoint exposes sensitive information. Use with caution. - - Args: - request: Contains chain, address, and passphrase - - Returns: - Dict with privateKey field. - - Example: POST /accounts/gateway/wallet/show-private-key - { - "chain": "solana", - "address": "", - "passphrase": "" - } - """ - try: - 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.show_private_key( - chain=request.chain, - address=request.address, - passphrase=request.passphrase - ) - - if result is None: - raise HTTPException(status_code=502, detail="Failed to retrieve private key: Gateway returned no response") - - if "error" in result: - raise HTTPException(status_code=400, detail=f"Failed to retrieve private key: {result.get('error')}") - - return result - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error retrieving private key: {str(e)}") - - -@router.post("/gateway/wallet/send") -async def send_gateway_wallet_transaction( - request: SendTransactionRequest, - accounts_service: AccountsService = Depends(get_accounts_service) -) -> Dict: - """ - Send a native token transaction. - - Args: - request: Contains chain, network, sender address, recipient address, and amount - - Returns: - Dict with transaction signature/hash. - - Example: POST /accounts/gateway/wallet/send - { - "chain": "solana", - "network": "mainnet-beta", - "address": "", - "to_address": "", - "amount": "0.001" - } - """ - try: - 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.send_transaction( - chain=request.chain, - network=request.network, - address=request.address, - to_address=request.to_address, - amount=request.amount - ) - - if result is None: - raise HTTPException(status_code=502, detail="Failed to send transaction: Gateway returned no response") - - if "error" in result: - raise HTTPException(status_code=400, detail=f"Failed to send transaction: {result.get('error')}") - - return result - - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error sending transaction: {str(e)}") diff --git a/routers/gateway.py b/routers/gateway.py index 3e622be9..bdad519e 100644 --- a/routers/gateway.py +++ b/routers/gateway.py @@ -1,16 +1,20 @@ -from fastapi import APIRouter, HTTPException, Depends, Query -from typing import Optional, Dict, List import re +from typing import Dict, List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query + +from deps import get_accounts_service, get_gateway_service from models import ( - GatewayConfig, - GatewayStatus, AddPoolRequest, AddTokenRequest, + CreateWalletRequest, + GatewayConfig, + GatewayStatus, + SendTransactionRequest, + ShowPrivateKeyRequest, ) -from services.gateway_service import GatewayService from services.accounts_service import AccountsService -from deps import get_gateway_service, get_accounts_service +from services.gateway_service import GatewayService router = APIRouter(tags=["Gateway"], prefix="/gateway") @@ -668,3 +672,146 @@ async def delete_network_token( raise except Exception as e: raise HTTPException(status_code=500, detail=f"Error deleting token: {str(e)}") + + +# ============================================ +# Wallet Management +# ============================================ + +@router.post("/wallets/create") +async def create_gateway_wallet( + request: CreateWalletRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +) -> Dict: + """ + Create a new wallet in Gateway. + + Args: + request: Contains chain and set_default flag + + Returns: + Dict with address and chain of the created wallet. + + Example: POST /gateway/wallets/create + { + "chain": "solana", + "set_default": true + } + """ + try: + 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.create_wallet( + chain=request.chain, + set_default=request.set_default + ) + + if result is None: + raise HTTPException(status_code=502, detail="Failed to create wallet: Gateway returned no response") + + if "error" in result: + raise HTTPException(status_code=400, detail=f"Failed to create wallet: {result.get('error')}") + + return result + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating wallet: {str(e)}") + + +@router.post("/wallets/show-private-key") +async def show_gateway_wallet_private_key( + request: ShowPrivateKeyRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +) -> Dict: + """ + Show private key for a wallet. + + WARNING: This endpoint exposes sensitive information. Use with caution. + + Args: + request: Contains chain, address, and passphrase + + Returns: + Dict with privateKey field. + + Example: POST /gateway/wallets/show-private-key + { + "chain": "solana", + "address": "", + "passphrase": "" + } + """ + try: + 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.show_private_key( + chain=request.chain, + address=request.address, + passphrase=request.passphrase + ) + + if result is None: + raise HTTPException(status_code=502, detail="Failed to retrieve private key: Gateway returned no response") + + if "error" in result: + raise HTTPException(status_code=400, detail=f"Failed to retrieve private key: {result.get('error')}") + + return result + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error retrieving private key: {str(e)}") + + +@router.post("/wallets/send") +async def send_gateway_wallet_transaction( + request: SendTransactionRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +) -> Dict: + """ + Send a native token transaction. + + Args: + request: Contains chain, network, sender address, recipient address, and amount + + Returns: + Dict with transaction signature/hash. + + Example: POST /gateway/wallets/send + { + "chain": "solana", + "network": "mainnet-beta", + "address": "", + "to_address": "", + "amount": "0.001" + } + """ + try: + 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.send_transaction( + chain=request.chain, + network=request.network, + address=request.address, + to_address=request.to_address, + amount=request.amount + ) + + if result is None: + raise HTTPException(status_code=502, detail="Failed to send transaction: Gateway returned no response") + + if "error" in result: + raise HTTPException(status_code=400, detail=f"Failed to send transaction: {result.get('error')}") + + return result + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error sending transaction: {str(e)}") From 99aeea797a3e03213c947613c51ff952a2f1faff Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 26 Mar 2026 18:23:07 -0700 Subject: [PATCH 13/15] revert: keep original function names in gateway router Reverted unnecessary function name changes: - create_gateway_wallet -> create_wallet - show_gateway_wallet_private_key -> show_private_key - send_gateway_wallet_transaction -> send_transaction Co-Authored-By: Claude Opus 4.5 --- routers/gateway.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routers/gateway.py b/routers/gateway.py index bdad519e..7bd7b428 100644 --- a/routers/gateway.py +++ b/routers/gateway.py @@ -679,7 +679,7 @@ async def delete_network_token( # ============================================ @router.post("/wallets/create") -async def create_gateway_wallet( +async def create_wallet( request: CreateWalletRequest, accounts_service: AccountsService = Depends(get_accounts_service) ) -> Dict: @@ -722,7 +722,7 @@ async def create_gateway_wallet( @router.post("/wallets/show-private-key") -async def show_gateway_wallet_private_key( +async def show_private_key( request: ShowPrivateKeyRequest, accounts_service: AccountsService = Depends(get_accounts_service) ) -> Dict: @@ -769,7 +769,7 @@ async def show_gateway_wallet_private_key( @router.post("/wallets/send") -async def send_gateway_wallet_transaction( +async def send_transaction( request: SendTransactionRequest, accounts_service: AccountsService = Depends(get_accounts_service) ) -> Dict: From 02ef8ea8e659c0d0d59128bc72a5a36bf516b061 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Fri, 27 Mar 2026 18:35:01 -0700 Subject: [PATCH 14/15] Add /lphistory endpoint for LP position history - Add get_bot_lp_history() method to BotsOrchestrator - Add GET /{bot_name}/lphistory endpoint to bot_orchestration router - Add "lphistory" to known MQTT command channels Returns LP-specific data: position_address, order_action, fees collected, price ranges, and amounts for AMM/CLMM strategies like Meteora. Fixes #132 Co-Authored-By: Claude Opus 4.5 --- .../master_account/conf_client.yml | 3 +- routers/bot_orchestration.py | 45 +++++++++++++++++++ services/bots_orchestrator.py | 32 ++++++++++++- utils/mqtt_manager.py | 2 +- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/bots/credentials/master_account/conf_client.yml b/bots/credentials/master_account/conf_client.yml index cbba78d4..21e00ebd 100644 --- a/bots/credentials/master_account/conf_client.yml +++ b/bots/credentials/master_account/conf_client.yml @@ -112,7 +112,7 @@ anonymized_metrics_mode: # A source for rate oracle, currently ascend_ex, binance, coin_gecko, coin_cap, kucoin, gate_io rate_oracle_source: - name: binance + name: gate_io # A universal token which to display tokens values in, e.g. USD,EUR,BTC global_token: @@ -133,7 +133,6 @@ tables_format: psql paper_trade: paper_trade_exchanges: - - binance - kucoin - ascend_ex - gate_io diff --git a/routers/bot_orchestration.py b/routers/bot_orchestration.py index 8224c25d..994fe60f 100644 --- a/routers/bot_orchestration.py +++ b/routers/bot_orchestration.py @@ -122,6 +122,51 @@ async def get_bot_history( return {"status": "success", "response": response} +@router.get("/{bot_name}/lphistory") +async def get_bot_lp_history( + bot_name: str, + days: int = 0, + verbose: bool = False, + precision: int = None, + timeout: float = 30.0, + bots_manager: BotsOrchestrator = Depends(get_bots_orchestrator) +): + """ + Get LP (liquidity provider) position history for a bot. + + This endpoint returns LP-specific data including position updates, + fees collected, and liquidity additions/removals. Use this for + AMM/CLMM strategies like Meteora. + + Args: + bot_name: Name of the bot to get LP history for + days: Number of days of history to retrieve (0 for all) + verbose: Whether to include verbose output + precision: Decimal precision for numerical values + timeout: Timeout in seconds for the operation + bots_manager: Bot orchestrator service dependency + + Returns: + Dictionary with LP position history including: + - position_address: The LP position address + - order_action: ADD or REMOVE + - trading_pair: The trading pair (e.g., SOL-USDC) + - base_amount, quote_amount: Amounts added/removed + - base_fee, quote_fee: Fees collected + - lower_price, upper_price: Price range of position + - mid_price: Price at time of operation + - trade_fee: Transaction fees paid + """ + response = await bots_manager.get_bot_lp_history( + bot_name, + days=days, + verbose=verbose, + precision=precision, + timeout=timeout + ) + return {"status": "success", "response": response} + + @router.post("/start-bot") async def start_bot( action: StartBotAction, diff --git a/services/bots_orchestrator.py b/services/bots_orchestrator.py index 85622f14..887700af 100644 --- a/services/bots_orchestrator.py +++ b/services/bots_orchestrator.py @@ -1,7 +1,7 @@ import asyncio import logging -from typing import Optional import re +from typing import Optional import docker @@ -220,6 +220,36 @@ async def get_bot_history(self, bot_name, **kwargs): return {"success": True, "data": response} + async def get_bot_lp_history(self, bot_name, **kwargs): + """ + Request bot LP (liquidity provider) history and wait for the response. + This returns LP position updates from RangePositionUpdate records. + """ + if bot_name not in self.active_bots: + logger.warning(f"Bot {bot_name} not found in active bots") + return {"success": False, "message": f"Bot {bot_name} not found"} + + # Create LPHistoryCommandMessage.Request format + data = { + "days": kwargs.get("days", 0), + "verbose": kwargs.get("verbose", False), + "precision": kwargs.get("precision"), + "async_backend": kwargs.get("async_backend", False), + } + + # Use the new RPC method to wait for response + timeout = kwargs.get("timeout", 30.0) # Default 30 second timeout + response = await self.mqtt_manager.publish_command_and_wait(bot_name, "lphistory", data, timeout=timeout) + + if response is None: + return { + "success": False, + "message": f"No response received from {bot_name} within {timeout} seconds", + "timeout": True, + } + + return {"success": True, "data": response} + @staticmethod def determine_controller_performance(controller_reports): """Process controller reports and extract performance and custom_info. diff --git a/utils/mqtt_manager.py b/utils/mqtt_manager.py index 3495eadb..63286c87 100644 --- a/utils/mqtt_manager.py +++ b/utils/mqtt_manager.py @@ -151,7 +151,7 @@ async def _process_message(self, message): await self._handle_command_response(bot_id, channel, data) elif channel.startswith("external/event/"): await self._handle_external_event(bot_id, channel, data) - elif channel in ["history", "start", "stop", "config", "import_strategy"]: + elif channel in ["history", "lphistory", "start", "stop", "config", "import_strategy"]: # These are command channels - responses should come on response/* topics logger.debug(f"Command channel '{channel}' for bot {bot_id} - waiting for response") else: From 28b15caffaef6c4dc7231ae802f81fc64255a688 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Sat, 28 Mar 2026 14:38:56 -0700 Subject: [PATCH 15/15] Add swap_executor type and improve executor handling - Add swap_executor to EXECUTOR_TYPES for single swaps via Gateway - Add swap_executor example in CreateExecutorRequest - Handle swap_executor using network instead of connector_name - Make trading_pair optional for lp_executor (resolved from pool_address) - Update lp_executor example with simplified config Co-Authored-By: Claude Opus 4.5 --- models/executors.py | 25 +++++++++++++++++++++---- services/executor_service.py | 35 +++++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/models/executors.py b/models/executors.py index dda1b455..d996017f 100644 --- a/models/executors.py +++ b/models/executors.py @@ -211,7 +211,8 @@ class PositionsSummaryResponse(BaseModel): "twap_executor", "xemm_executor", "order_executor", - "lp_executor" + "lp_executor", + "swap_executor" ] @@ -246,14 +247,14 @@ class CreateExecutorRequest(BaseModel): }, { "summary": "LP Executor", - "description": "Create an LP position on a CLMM DEX (Meteora, Raydium)", + "description": "Create an LP position on a CLMM DEX", "value": { "account_name": "master_account", "executor_config": { "type": "lp_executor", - "connector_name": "meteora/clmm", - "trading_pair": "SOL-USDC", + "connector_name": "meteora", "pool_address": "HTvjzsfX3yU6BUodCjZ5vZkUrAxMDTrBs3CJaq43ashR", + "network": "solana-mainnet-beta", "lower_price": "80", "upper_price": "100", "base_amount": "0", @@ -265,6 +266,22 @@ class CreateExecutorRequest(BaseModel): "keep_position": False } } + }, + { + "summary": "Swap Executor", + "description": "Execute a single swap on Gateway AMM connectors (Jupiter, Raydium, etc.)", + "value": { + "account_name": "master_account", + "executor_config": { + "type": "swap_executor", + "network": "solana-mainnet-beta", + "trading_pair": "SOL-USDC", + "side": 2, + "amount": "0.1", + "slippage_pct": "0.5", + "swap_providers": ["jupiter/router", "meteora/clmm", "orca/clmm"] + } + } } ] } diff --git a/services/executor_service.py b/services/executor_service.py index aaec2757..63c0dca1 100644 --- a/services/executor_service.py +++ b/services/executor_service.py @@ -349,15 +349,38 @@ async def create_executor( trading_interface = self._get_trading_interface(account) # Extract connector and trading pair from config + # Note: swap_executor uses 'network' instead of 'connector_name' since it calls Gateway directly connector_name = executor_config.get("connector_name") trading_pair = executor_config.get("trading_pair") - if not connector_name: - raise HTTPException(status_code=400, detail="connector_name is required in executor_config") - if not trading_pair: - raise HTTPException(status_code=400, detail="trading_pair is required in executor_config") - # Ensure connector and market are ready - await trading_interface.add_market(connector_name, trading_pair) + if executor_type == "swap_executor": + # SwapExecutor uses network, not connector_name + network = executor_config.get("network") + if not network: + raise HTTPException(status_code=400, detail="network is required for swap_executor") + if not trading_pair: + raise HTTPException(status_code=400, detail="trading_pair is required in executor_config") + # Use network as connector_name for metadata tracking + connector_name = network + elif executor_type == "lp_executor": + # LPExecutor: trading_pair is optional (resolved from pool_address) + if not connector_name: + raise HTTPException(status_code=400, detail="connector_name is required for lp_executor") + pool_address = executor_config.get("pool_address") + if not pool_address: + raise HTTPException(status_code=400, detail="pool_address is required for lp_executor") + # Ensure connector is ready (trading_pair resolved in executor on_start) + await trading_interface.ensure_connector(connector_name) + # Use pool_address as trading_pair placeholder for metadata if not provided + if not trading_pair: + trading_pair = pool_address + else: + if not connector_name: + raise HTTPException(status_code=400, detail="connector_name is required in executor_config") + if not trading_pair: + raise HTTPException(status_code=400, detail="trading_pair is required in executor_config") + # Ensure connector and market are ready + await trading_interface.add_market(connector_name, trading_pair) # Set timestamp if not provided (required for time-based features like time_limit) if "timestamp" not in executor_config or executor_config["timestamp"] is None: