Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 31 additions & 43 deletions handlers/bots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@
logger = logging.getLogger(__name__)


def _split_action_payload(action: str) -> tuple[str, str | None]:
main_action, separator, payload = action.partition(":")
return main_action, payload if separator else None


_PAIR_PAYLOAD_HANDLERS = {
"pv1_pair": handle_pv1_wizard_pair,
"pv1_pair_select": handle_pv1_pair_select,
"gs_pair": handle_gs_wizard_pair,
"gs_pair_select": handle_gs_pair_select,
"pmm_pair": handle_pmm_wizard_pair,
"pmm_pair_select": handle_pmm_pair_select,
}


# ============================================
# MAIN BOTS COMMAND
# ============================================
Expand Down Expand Up @@ -258,8 +273,15 @@ async def bots_callback_handler(
action = callback_parts[1] if len(callback_parts) > 1 else query.data

# Parse action and any additional parameters
main_action, action_payload = _split_action_payload(action)
action_parts = action.split(":")
main_action = action_parts[0]

if main_action in _PAIR_PAYLOAD_HANDLERS:
if action_payload is not None:
await _PAIR_PAYLOAD_HANDLERS[main_action](
update, context, action_payload
)
return

# Menu navigation
if main_action == "main_menu":
Expand Down Expand Up @@ -288,18 +310,16 @@ async def bots_callback_handler(
await show_type_selector(update, context)

elif main_action == "cfg_type":
if len(action_parts) > 1:
controller_type = action_parts[1]
await show_configs_by_type(update, context, controller_type)
if action_payload is not None:
await show_configs_by_type(update, context, action_payload)

elif main_action == "cfg_toggle":
if len(action_parts) > 1:
config_id = action_parts[1]
await handle_cfg_toggle(update, context, config_id)
if action_payload is not None:
await handle_cfg_toggle(update, context, action_payload)

elif main_action == "cfg_page":
if len(action_parts) > 1:
page = int(action_parts[1])
if action_payload is not None:
page = int(action_payload)
await handle_cfg_page(update, context, page)

elif main_action == "cfg_clear_selection":
Expand Down Expand Up @@ -368,16 +388,6 @@ async def bots_callback_handler(
connector = action_parts[1]
await handle_pv1_wizard_connector(update, context, connector)

elif main_action == "pv1_pair":
if len(action_parts) > 1:
pair = action_parts[1]
await handle_pv1_wizard_pair(update, context, pair)

elif main_action == "pv1_pair_select":
if len(action_parts) > 1:
pair = action_parts[1]
await handle_pv1_pair_select(update, context, pair)

elif main_action == "pv1_amount":
if len(action_parts) > 1:
amount = action_parts[1]
Expand Down Expand Up @@ -487,10 +497,8 @@ async def bots_callback_handler(
await handle_select_credentials(update, context, creds)

elif main_action == "select_image":
if len(action_parts) > 1:
# Rejoin parts to preserve colons in image tag (e.g., "hummingbot:development")
image = ":".join(action_parts[1:])
await handle_select_image(update, context, image)
if action_payload is not None:
await handle_select_image(update, context, action_payload)

elif main_action == "select_name":
if len(action_parts) > 1:
Expand All @@ -509,16 +517,6 @@ async def bots_callback_handler(
connector = action_parts[1]
await handle_gs_wizard_connector(update, context, connector)

elif main_action == "gs_pair":
if len(action_parts) > 1:
pair = action_parts[1]
await handle_gs_wizard_pair(update, context, pair)

elif main_action == "gs_pair_select":
if len(action_parts) > 1:
pair = action_parts[1]
await handle_gs_pair_select(update, context, pair)

elif main_action == "gs_side":
if len(action_parts) > 1:
side_str = action_parts[1]
Expand Down Expand Up @@ -606,16 +604,6 @@ async def bots_callback_handler(
connector = action_parts[1]
await handle_pmm_wizard_connector(update, context, connector)

elif main_action == "pmm_pair":
if len(action_parts) > 1:
pair = action_parts[1]
await handle_pmm_wizard_pair(update, context, pair)

elif main_action == "pmm_pair_select":
if len(action_parts) > 1:
pair = action_parts[1]
await handle_pmm_pair_select(update, context, pair)

elif main_action == "pmm_leverage":
if len(action_parts) > 1:
leverage = int(action_parts[1])
Expand Down
42 changes: 35 additions & 7 deletions handlers/bots/controller_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import asyncio
import copy
import hashlib
import logging
from typing import List

Expand Down Expand Up @@ -74,6 +75,26 @@
CONFIGS_PER_PAGE = 8 # Reduced to leave space for action buttons


def _config_callback_token(config_id: str) -> str:
"""Short stable token for Telegram callback_data's 64-byte limit."""
return hashlib.blake2s(config_id.encode("utf-8"), digest_size=6).hexdigest()


def _resolve_config_callback_token(context: ContextTypes.DEFAULT_TYPE, token: str) -> str:
"""Resolve a short callback token to the original config ID."""
callback_ids = context.user_data.get("config_callback_ids", {})
return callback_ids.get(token, token)


def _escape_markdown_v2_code(text: object) -> str:
"""Escape text for a MarkdownV2 inline code span."""
return str(text).replace("\\", "\\\\").replace("`", "\\`").replace("\n", "\\n")


def _code_span(text: object) -> str:
return f"`{_escape_markdown_v2_code(text)}`"


def _get_controller_type_display(controller_name: str) -> tuple[str, str]:
"""Get display name and emoji for controller type"""
type_map = {
Expand Down Expand Up @@ -163,6 +184,11 @@ async def show_controller_configs_menu(

# Store all configs
context.user_data["controller_configs_list"] = configs
context.user_data["config_callback_ids"] = {
_config_callback_token(cfg.get("id", "")): cfg.get("id", "")
for cfg in configs
if cfg.get("id")
}

# Get available types from registry (always shows all supported types)
all_types = get_supported_controller_types()
Expand Down Expand Up @@ -258,6 +284,7 @@ async def show_controller_configs_menu(
# Config checkboxes - show just the controller name/ID
for i, cfg in enumerate(page_configs):
config_id = cfg.get("id", f"config_{start_idx + i}")
config_token = _config_callback_token(config_id)
is_selected = selected.get(config_id, False)
checkbox = "✅" if is_selected else "⬜"

Expand All @@ -267,7 +294,7 @@ async def show_controller_configs_menu(
keyboard.append(
[
InlineKeyboardButton(
display, callback_data=f"bots:cfg_toggle:{config_id}"
display, callback_data=f"bots:cfg_toggle:{config_token}"
)
]
)
Expand Down Expand Up @@ -453,6 +480,7 @@ async def handle_cfg_toggle(
update: Update, context: ContextTypes.DEFAULT_TYPE, config_id: str
) -> None:
"""Toggle config selection by config ID"""
config_id = _resolve_config_callback_token(context, config_id)
selected = context.user_data.get("selected_configs", {})

if selected.get(config_id):
Expand Down Expand Up @@ -734,7 +762,7 @@ async def show_cfg_edit_form(
if status_msg:
header += f" — {escape_markdown_v2(status_msg)}"
lines = [header, ""]
lines.append(f"`{escape_markdown_v2(config_id)}`")
lines.append(_code_span(config_id))
lines.append("")

# Add context info for Grid Strike (connector, trading pair, side)
Expand All @@ -751,9 +779,9 @@ async def show_cfg_edit_form(

# Build config text for display (each line copyable)
for key, value in editable_fields.items():
lines.append(f"`{key}={value}`")
lines.append(_code_span(f"{key}={value}"))
lines.append("")
lines.append("✏️ _Send `key=value` to update_")
lines.append(f"✏️ Send {_code_span('key=value')} to update")

# Build keyboard - simplified, no field buttons
keyboard = []
Expand Down Expand Up @@ -994,7 +1022,7 @@ async def process_cfg_edit_input(
config_id = config.get("id", "unknown")

lines = [f"*Edit Config* \\({current_idx + 1}/{total}\\)", ""]
lines.append(f"`{escape_markdown_v2(config_id)}`")
lines.append(_code_span(config_id))
lines.append("")

# Add context info for Grid Strike (connector, trading pair, side)
Expand All @@ -1010,9 +1038,9 @@ async def process_cfg_edit_input(
lines.append("")

for key, value in editable_fields.items():
lines.append(f"`{key}={value}`")
lines.append(_code_span(f"{key}={value}"))
lines.append("")
lines.append("✏️ _Send `key=value` to update_")
lines.append(f"✏️ Send {_code_span('key=value')} to update")

# Build keyboard
keyboard = []
Expand Down
29 changes: 17 additions & 12 deletions handlers/cex/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,8 +671,8 @@ async def validate_trading_pair(
# Log a few sample pairs to debug
sample_pairs = available_pairs[:5] if available_pairs else []
logger.info(f"Sample pairs: {sample_pairs}")
# For input like "XYZ", look for pairs like "ISSUER:XYZ-USDC" or "ISSUER:XYZ-USD"

# For input like "XYZ" or "ISSUER:XYZ", look for pairs like "ISSUER:XYZ-USDC" or "ISSUER:XYZ-USD".
hip3_matches = []
for pair in available_pairs:
if ":" in pair:
Expand All @@ -684,16 +684,21 @@ async def validate_trading_pair(
if ":" in issuer_symbol:
issuer, symbol = issuer_symbol.split(":", 1)
logger.debug(f"Checking pair '{pair}': issuer='{issuer}', symbol='{symbol}', quote='{quote}' vs input='{pair_normalized}'")
# Check if the input matches the symbol part
if symbol.upper() == pair_normalized.upper():
logger.info(f"Found HIP3 symbol match: '{pair}' matches input '{pair_normalized}'")
hip3_matches.append(pair)
# Also check if input matches the full issuer:symbol part
elif (
issuer_symbol.upper().replace(":", "-")
== pair_normalized.upper()
):
logger.info(f"Found HIP3 issuer:symbol match: '{pair}' matches input '{pair_normalized}'")

issuer_symbol_normalized = issuer_symbol.upper().replace(
"_", "-"
).replace("/", "-")
hip3_aliases = {
symbol.upper(),
f"{symbol.upper()}-{quote.upper()}",
issuer_symbol_normalized,
issuer_symbol_normalized.replace(":", "-"),
pair.upper().replace("_", "-").replace("/", "-"),
}
if pair_normalized.upper() in hip3_aliases:
logger.info(
f"Found HIP3 match: '{pair}' matches input '{pair_normalized}'"
)
hip3_matches.append(pair)
except ValueError as e:
logger.debug(f"Failed to parse HIP3 pair '{pair}': {e}")
Expand Down