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 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..849acd4c 100644 --- a/models/gateway.py +++ b/models/gateway.py @@ -50,10 +50,10 @@ 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") class GatewayWalletInfo(BaseModel): @@ -63,6 +63,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..a5e4106a 100644 --- a/routers/accounts.py +++ b/routers/accounts.py @@ -1,12 +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 +from models import GatewayWalletCredential, SetDefaultWalletRequest +from services.accounts_service import AccountsService router = APIRouter(tags=["Accounts"], prefix="/accounts") @@ -15,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 """ @@ -51,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 """ @@ -72,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 """ @@ -95,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 """ @@ -168,10 +167,11 @@ async def add_gateway_wallet( 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 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: @@ -191,6 +192,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/{chain}/{address}") async def remove_gateway_wallet( chain: str, @@ -217,5 +269,3 @@ async def remove_gateway_wallet( raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) - - 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..7bd7b428 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") 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/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( diff --git a/services/accounts_service.py b/services/accounts_service.py index 1d951017..4726dc19 100644 --- a/services/accounts_service.py +++ b/services/accounts_service.py @@ -1932,8 +1932,8 @@ 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 (e.g., "ethereum-default-wallet", "solana-default-wallet") - if default_wallet.endswith("-default-wallet"): + # 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 @@ -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 278448a1..b62aec07 100644 --- a/services/gateway_client.py +++ b/services/gateway_client.py @@ -202,6 +202,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={