Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b9db532
Add SwapExecutor support
fengtality Mar 11, 2026
f9f203d
fix: remove misleading restart message for token add/delete
fengtality Mar 12, 2026
ca57a16
Merge fix/gateway-proxy-operation-ids into feature/swap-executor
fengtality Mar 28, 2026
02ef8ea
Add /lphistory endpoint for LP position history
fengtality Mar 28, 2026
28b15ca
Add swap_executor type and improve executor handling
fengtality Mar 28, 2026
2d73083
Add GatewaySwap connector support for swap executor
fengtality Mar 29, 2026
51f48de
Revert conf_client.yml to default settings
fengtality Mar 29, 2026
f5216e3
Update Gateway poll integration for simplified endpoint
fengtality Mar 29, 2026
6aafc3b
Fix swap_executor example: add required connector_name
fengtality Mar 29, 2026
4283448
Simplify Gateway executor validation in executor_service
fengtality Apr 1, 2026
ec06708
sync: update lp_rebalancer from hummingbot
fengtality Apr 1, 2026
70db22c
debug: add logging to order completion handler
fengtality Apr 1, 2026
f21dca2
refactor: update to new gateway connector architecture
fengtality Apr 3, 2026
1921079
fix: update get_price call to use dex and trading_type params
fengtality Apr 3, 2026
13bd7d5
fix: update executor_service and lp_rebalancer for provider refactor
fengtality Apr 7, 2026
9c96838
fix: initialize rate sources for gateway executors
fengtality Apr 8, 2026
1f89d4f
fix: remove broken rate source initialization for gateway executors
fengtality Apr 8, 2026
64dc472
refactor: remove SwapExecutor references, use OrderExecutor for swaps
fengtality Apr 8, 2026
885deed
refactor: remove redundant executor validation, let Pydantic handle it
fengtality Apr 8, 2026
b29e111
cleanup: fix lint issues in orders_recorder, remove verbose debug log…
fengtality Apr 8, 2026
dddf487
Remove lphistory endpoint - not needed for API-managed executors
fengtality Apr 8, 2026
3b096e3
fix: revert lint-only changes, simplify executor connector init
fengtality Apr 8, 2026
e8397d6
refactor: remove fee fallback logic from orders_recorder
fengtality Apr 8, 2026
81e4183
revert: remove all changes from orders_recorder.py
fengtality Apr 8, 2026
65e8c21
revert: remove all changes from bots_orchestrator.py
fengtality Apr 8, 2026
87ab393
Filter out DEX providers from connectors list
fengtality Apr 8, 2026
dc02049
Add redirect_slashes=False for Mintlify docs compatibility
fengtality Apr 8, 2026
648ac32
Revert redirect_slashes change
fengtality Apr 8, 2026
2e98530
Add redirect_slashes=False and remove trailing slashes from routes
fengtality Apr 8, 2026
7d0b524
Fix available_images endpoint returning 500 error
fengtality Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
608 changes: 482 additions & 126 deletions bots/controllers/generic/lp_rebalancer/lp_rebalancer.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ async def lifespan(app: FastAPI):
description="API for managing Hummingbot trading instances",
version=VERSION,
lifespan=lifespan,
redirect_slashes=False,
)

# Add CORS middleware
Expand Down
10 changes: 4 additions & 6 deletions models/executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ class PositionsSummaryResponse(BaseModel):
"twap_executor",
"xemm_executor",
"order_executor",
"lp_executor"
"lp_executor",
]


Expand Down Expand Up @@ -246,21 +246,19 @@ 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": "solana-mainnet-beta",
"lp_provider": "meteora/clmm",
"pool_address": "HTvjzsfX3yU6BUodCjZ5vZkUrAxMDTrBs3CJaq43ashR",
"lower_price": "80",
"upper_price": "100",
"base_amount": "0",
"quote_amount": "10.0",
"side": 1,
"auto_close_above_range_seconds": None,
"auto_close_below_range_seconds": 300,
"extra_params": {"strategyType": 0},
"keep_position": False
}
Expand Down
2 changes: 1 addition & 1 deletion routers/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
router = APIRouter(tags=["Accounts"], prefix="/accounts")


@router.get("/", response_model=List[str])
@router.get("", response_model=List[str])
async def list_accounts(accounts_service: AccountsService = Depends(get_accounts_service)):
"""
Get a list of all account names in the system.
Expand Down
3 changes: 2 additions & 1 deletion routers/archived_bots.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import List, Optional

from fastapi import APIRouter, HTTPException, Query

from utils.file_system import fs_util
Expand All @@ -7,7 +8,7 @@
router = APIRouter(tags=["Archived Bots"], prefix="/archived-bots")


@router.get("/", response_model=List[str])
@router.get("", response_model=List[str])
async def list_databases():
"""
List all available database files in the system.
Expand Down
8 changes: 5 additions & 3 deletions routers/connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
router = APIRouter(tags=["Connectors"], prefix="/connectors")


@router.get("/", response_model=List[str])
@router.get("", response_model=List[str])
async def available_connectors():
"""
Get a list of all available connectors.

Returns:
List of connector names supported by the system
List of connector names supported by the system (excludes DEX providers which use Gateway networks)
"""
return list(AllConnectorSettings.get_connector_settings().keys())
all_connectors = AllConnectorSettings.get_connector_settings().keys()
# Filter out DEX providers (contain '/') - these are accessed via Gateway networks
return [c for c in all_connectors if '/' not in c]


@router.get("/{connector_name}/config-map", response_model=Dict[str, dict])
Expand Down
4 changes: 2 additions & 2 deletions routers/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
router = APIRouter(tags=["Controllers"], prefix="/controllers")


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


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

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

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

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

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


@router.get("/available-images/")
@router.get("/available-images")
async def available_images(image_name: str = None, docker_service: DockerService = Depends(get_docker_service)):
"""
Get available Docker images matching the specified name.
Expand All @@ -39,7 +39,7 @@ async def available_images(image_name: str = None, docker_service: DockerService
available_images = docker_service.get_available_images()
if image_name:
return [tag for image in available_images["images"] for tag in image.tags if image_name in tag]
return [tag for tag in available_images["images"]]
return [tag for image in available_images["images"] for tag in image.tags]


@router.get("/active-containers")
Expand Down Expand Up @@ -161,7 +161,7 @@ async def start_container(container_name: str, docker_service: DockerService = D
return docker_service.start_container(container_name)


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


@router.get("/pull-status/")
@router.get("/pull-status")
async def get_pull_status(docker_service: DockerService = Depends(get_docker_service)):
"""
Get status of all pull operations.
Expand Down
8 changes: 2 additions & 6 deletions routers/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,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,
Expand Down Expand Up @@ -661,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
}
Expand Down
6 changes: 3 additions & 3 deletions routers/scripts.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import yaml
from typing import Dict, List

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

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


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


# Script Configuration endpoints (must come before script name routes)
@router.get("/configs/", response_model=List[Dict])
@router.get("/configs", response_model=List[Dict])
async def list_script_configs():
"""
List all script configurations with metadata.
Expand Down
10 changes: 9 additions & 1 deletion services/accounts_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2190,6 +2190,13 @@ async def _fetch_gateway_prices_immediate(self, chain: str, network: str,
logger.warning(f"No pricing connector configured for chain '{chain}', skipping immediate price fetch")
return prices

# Parse pricing connector into dex and trading_type (e.g., "jupiter/router" -> "jupiter", "router")
if "/" in pricing_connector:
dex_name, trading_type = pricing_connector.split("/", 1)
else:
dex_name = pricing_connector
trading_type = "router"

# Create tasks for all tokens in parallel
tasks = []
task_tokens = []
Expand Down Expand Up @@ -2225,7 +2232,8 @@ async def _fetch_gateway_prices_immediate(self, chain: str, network: str,
task = gateway_client.get_price(
chain=chain,
network=network,
connector=pricing_connector,
dex=dex_name,
trading_type=trading_type,
base_asset=token,
quote_asset=quote_asset,
amount=Decimal("1"),
Expand Down
10 changes: 5 additions & 5 deletions services/executor_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,13 @@ async def create_executor(
# Extract connector and trading pair from config
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 connector_name:
if trading_pair:
await trading_interface.add_market(connector_name, trading_pair)
else:
await trading_interface.ensure_connector(connector_name)

# 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:
Expand Down
9 changes: 2 additions & 7 deletions services/gateway_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
from decimal import Decimal
from typing import Dict, List, Optional

import aiohttp
Expand Down Expand Up @@ -582,20 +581,19 @@ async def poll_transaction(
self,
network_id: str,
tx_hash: str,
wallet_address: Optional[str] = None
) -> Optional[Dict]:
"""
Poll transaction status on blockchain.

Args:
network_id: Network ID in format 'chain-network' (e.g., 'solana-mainnet-beta', 'ethereum-mainnet')
tx_hash: Transaction hash/signature
wallet_address: Optional wallet address for verification

Returns:
Transaction status dict with fields:
- txStatus: 1 for confirmed, 0 for failed/pending
- txStatus: 1 for confirmed, 0 for pending, -1 for failed
- fee: Transaction fee amount
- error: Parsed error message if transaction failed (e.g., "SLIPPAGE_EXCEEDED (0x1771): ...")
- txData: Full transaction data including meta.err
Returns None if Gateway is unavailable or request fails.
"""
Expand All @@ -612,11 +610,8 @@ async def poll_transaction(
"network": network,
"signature": tx_hash
}
if wallet_address:
payload["walletAddress"] = wallet_address

return await self._request("POST", f"chains/{chain}/poll", json=payload)
except Exception as e:
logger.error(f"Error polling transaction {tx_hash}: {e}")
return None

27 changes: 15 additions & 12 deletions services/gateway_transaction_poller.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
"""
import asyncio
import logging
from typing import Optional, Dict, List
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Dict, List, Optional

from sqlalchemy import select
from sqlalchemy.orm import selectinload

from database import AsyncDatabaseManager
from database.repositories import GatewaySwapRepository, GatewayCLMMRepository
from database.models import GatewayCLMMEvent, GatewayCLMMPosition
from database.repositories import GatewayCLMMRepository, GatewaySwapRepository
from services.gateway_client import GatewayClient

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -312,9 +312,6 @@ async def _check_transaction_status(

# Parse the response with defensive checks
tx_status = result.get("txStatus")
tx_data = result.get("txData") or {}
meta = tx_data.get("meta") if isinstance(tx_data, dict) else {}
error = meta.get("err") if isinstance(meta, dict) else None

# Determine gas token based on chain
gas_token = {
Expand All @@ -326,18 +323,25 @@ async def _check_transaction_status(
"avalanche": "AVAX"
}.get(chain, "UNKNOWN")

# Transaction is confirmed if txStatus == 1 and no error
if tx_status == 1 and error is None:
# Transaction is confirmed if txStatus == 1
if tx_status == 1:
return {
"status": "CONFIRMED",
"gas_fee": result.get("fee", 0),
"gas_token": gas_token,
"error_message": None
}

# Transaction failed if there's an error
if error is not None:
error_msg = str(error) if error else "Transaction failed on-chain"
# Transaction failed if txStatus == -1 or there's an error field
# Gateway now returns parsed error messages like "SLIPPAGE_EXCEEDED (0x1771): ..."
error_msg = result.get("error")
if tx_status == -1 or error_msg:
if not error_msg:
# Fallback to meta.err if no parsed error
tx_data = result.get("txData") or {}
meta = tx_data.get("meta") if isinstance(tx_data, dict) else {}
raw_error = meta.get("err") if isinstance(meta, dict) else None
error_msg = str(raw_error) if raw_error else "Transaction failed on-chain"
return {
"status": "FAILED",
"gas_fee": result.get("fee", 0),
Expand All @@ -352,14 +356,13 @@ async def _check_transaction_status(
logger.error(f"Error checking transaction status for {tx_hash}: {e}")
return None

async def poll_transaction_once(self, tx_hash: str, network_id: str, wallet_address: Optional[str] = None) -> Optional[Dict]:
async def poll_transaction_once(self, tx_hash: str, network_id: str) -> Optional[Dict]:
"""
Poll a specific transaction once (useful for immediate status checks).

Args:
tx_hash: Transaction hash
network_id: Network ID in format 'chain-network' (e.g., 'solana-mainnet-beta')
wallet_address: Optional wallet address for verification

Returns:
Transaction status dict or None if pending
Expand Down
Loading