From 75115e16402b80d3bfd223c9001204f32cd9a71a Mon Sep 17 00:00:00 2001 From: TrezorHannes Date: Wed, 14 Jan 2026 20:27:20 +0100 Subject: [PATCH 1/3] fix(magma): replace bot.polling() with robust infinity_polling wrapper The Telegram polling thread would silently crash due to network timeouts or other exceptions, causing callback buttons (Approve/Reject) to stop working while the scheduled auto-approval continued functioning normally. Changes: - Replace bot.polling(none_stop=True) with bot.infinity_polling() - Add run_telegram_polling() wrapper with automatic restart on failure - Log and notify when polling restarts to aid debugging - Set thread as daemon for clean shutdown - Configure appropriate timeout values (60s connection, 30s long_polling) This ensures the polling thread recovers from transient failures instead of dying silently. --- Magma/magma_sale_process.py | 38 +++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/Magma/magma_sale_process.py b/Magma/magma_sale_process.py index 59753d0..73c1d01 100644 --- a/Magma/magma_sale_process.py +++ b/Magma/magma_sale_process.py @@ -1804,10 +1804,40 @@ def handle_run_command(message): schedule.every(1).minutes.do(check_pending_confirmations_timeouts) schedule.every(POLLING_INTERVAL_MINUTES).minutes.do(execute_bot_behavior) - # Start the Telegram bot polling in a separate thread - logging.info("Starting Telegram bot poller thread.") - # Reduced interval from 30 to 2 seconds for better responsiveness - threading.Thread(target=lambda: bot.polling(none_stop=True, interval=2), name="TelegramPoller").start() + + # Define robust polling wrapper with automatic restart on failure + def run_telegram_polling(): + """Runs Telegram polling with automatic restart on failure. + + Uses infinity_polling() instead of polling() for better error recovery. + If polling crashes (network issues, timeouts, etc.), it will automatically + restart after a short delay. This prevents the silent thread death that + causes callback buttons to stop working. + """ + restart_count = 0 + while True: + try: + if restart_count > 0: + logging.warning(f"Telegram polling restart #{restart_count}") + send_telegram_notification( + f"⚠️ Telegram poller restarted (attempt #{restart_count}). " + "If you see this frequently, check network connectivity.", + level="warning" + ) + logging.info("Starting Telegram bot infinity_polling...") + # infinity_polling handles most transient errors internally + # timeout: connection timeout for requests + # long_polling_timeout: how long Telegram waits before returning empty response + bot.infinity_polling(timeout=60, long_polling_timeout=30) + except Exception as e: + restart_count += 1 + logging.error(f"Telegram polling crashed with error: {e}. Restarting in 10 seconds... (restart #{restart_count})") + time.sleep(10) + + # Start the Telegram bot polling in a separate daemon thread + logging.info("Starting Telegram bot poller thread with infinity_polling.") + telegram_thread = threading.Thread(target=run_telegram_polling, name="TelegramPoller", daemon=True) + telegram_thread.start() # Run scheduled tasks in the main thread logging.info("Entering main scheduling loop.") From d4963cf828a9446b521e430ccfd2c5006e4744e8 Mon Sep 17 00:00:00 2001 From: TrezorHannes Date: Wed, 14 Jan 2026 20:29:16 +0100 Subject: [PATCH 2/3] chore(magma): translate Portuguese comments to English Translate inline comments in calculate_transaction_size() for consistency. --- Magma/magma_sale_process.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Magma/magma_sale_process.py b/Magma/magma_sale_process.py index 73c1d01..8fe108c 100644 --- a/Magma/magma_sale_process.py +++ b/Magma/magma_sale_process.py @@ -897,9 +897,9 @@ def get_lncli_utxos(): def calculate_transaction_size(utxos_needed): - inputs_size = utxos_needed * 57.5 # Cada UTXO é de 57.5 vBytes - outputs_size = 2 * 43 # Dois outputs de 43 vBytes cada - overhead_size = 10.5 # Overhead de 10.5 vBytes + inputs_size = utxos_needed * 57.5 # Each UTXO is 57.5 vBytes + outputs_size = 2 * 43 # Two outputs of 43 vBytes each + overhead_size = 10.5 # Transaction overhead of 10.5 vBytes total_size = inputs_size + outputs_size + overhead_size return total_size From 46aea2abdabcd894d3ab64bfe57c990b0804eed0 Mon Sep 17 00:00:00 2001 From: TrezorHannes Date: Wed, 14 Jan 2026 20:39:14 +0100 Subject: [PATCH 3/3] refactor(magma): address PR feedback from code review 1. Exponential backoff for polling restarts: - Add TELEGRAM_POLL_* constants for configurable backoff - Delay increases 10s -> 20s -> 40s -> ... up to 5 min cap - Resets on successful polling 2. Extract magic numbers to named constants: - P2WPKH_INPUT_VBYTES = 57.5 - P2WPKH_OUTPUT_VBYTES = 43 - TRANSACTION_OVERHEAD_VBYTES = 10.5 3. Move run_telegram_polling() to module level: - Better modularity and testability - No longer nested inside if __name__ == '__main__' Tests: 12/12 passing --- Magma/magma_sale_process.py | 86 +++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/Magma/magma_sale_process.py b/Magma/magma_sale_process.py index 8fe108c..b61784e 100644 --- a/Magma/magma_sale_process.py +++ b/Magma/magma_sale_process.py @@ -161,6 +161,16 @@ ACTIVE_ORDER_POLL_DURATION_MINUTES = 15 # Poll for a total of 15 minutes USER_CONFIRMATION_TIMEOUT_SECONDS = 300 # 5 minutes for user to respond to new offer +# --- Constants for transaction size calculation (SegWit P2WPKH) --- +P2WPKH_INPUT_VBYTES = 57.5 # Virtual bytes per input +P2WPKH_OUTPUT_VBYTES = 43 # Virtual bytes per output +TRANSACTION_OVERHEAD_VBYTES = 10.5 # Transaction overhead + +# --- Constants for Telegram polling retry with exponential backoff --- +TELEGRAM_POLL_INITIAL_DELAY_SECONDS = 10 +TELEGRAM_POLL_MAX_DELAY_SECONDS = 300 # Cap at 5 minutes +TELEGRAM_POLL_BACKOFF_MULTIPLIER = 2 + # --- State for pending user confirmations --- # Structure: {order_id: {"message_id": int, "timestamp": float, "details": dict}} pending_user_confirmations = {} @@ -897,9 +907,10 @@ def get_lncli_utxos(): def calculate_transaction_size(utxos_needed): - inputs_size = utxos_needed * 57.5 # Each UTXO is 57.5 vBytes - outputs_size = 2 * 43 # Two outputs of 43 vBytes each - overhead_size = 10.5 # Transaction overhead of 10.5 vBytes + """Calculate transaction size in virtual bytes for a given number of UTXOs.""" + inputs_size = utxos_needed * P2WPKH_INPUT_VBYTES + outputs_size = 2 * P2WPKH_OUTPUT_VBYTES + overhead_size = TRANSACTION_OVERHEAD_VBYTES total_size = inputs_size + outputs_size + overhead_size return total_size @@ -1727,6 +1738,46 @@ def handle_run_command(message): threading.Thread(target=execute_bot_behavior, name=f"ManualRun-{message.text[1:]}").start() +def run_telegram_polling(): + """Runs Telegram polling with automatic restart on failure using exponential backoff. + + Uses infinity_polling() instead of polling() for better error recovery. + If polling crashes (network issues, timeouts, etc.), it will automatically + restart after a delay that increases exponentially (capped at 5 minutes). + This prevents log spam during extended outages while still recovering quickly + from transient failures. + """ + restart_count = 0 + current_delay = TELEGRAM_POLL_INITIAL_DELAY_SECONDS + + while True: + try: + if restart_count > 0: + logging.warning(f"Telegram polling restart #{restart_count} (next delay: {current_delay}s)") + send_telegram_notification( + f"⚠️ Telegram poller restarted (attempt #{restart_count}). " + f"Next retry delay: {current_delay}s. " + "If you see this frequently, check network connectivity.", + level="warning" + ) + logging.info("Starting Telegram bot infinity_polling...") + # infinity_polling handles most transient errors internally + # timeout: connection timeout for requests + # long_polling_timeout: how long Telegram waits before returning empty response + bot.infinity_polling(timeout=60, long_polling_timeout=30) + # If we get here, polling exited cleanly - reset backoff + restart_count = 0 + current_delay = TELEGRAM_POLL_INITIAL_DELAY_SECONDS + except Exception as e: + restart_count += 1 + logging.error(f"Telegram polling crashed with error: {e}. Restarting in {current_delay} seconds... (restart #{restart_count})") + time.sleep(current_delay) + # Exponential backoff with cap + current_delay = min( + current_delay * TELEGRAM_POLL_BACKOFF_MULTIPLIER, + TELEGRAM_POLL_MAX_DELAY_SECONDS + ) + if __name__ == "__main__": # Ensure logs directory exists before setting up handler logs_dir_for_main = os.path.join(parent_dir, "..", "logs") @@ -1805,35 +1856,6 @@ def handle_run_command(message): schedule.every(POLLING_INTERVAL_MINUTES).minutes.do(execute_bot_behavior) - # Define robust polling wrapper with automatic restart on failure - def run_telegram_polling(): - """Runs Telegram polling with automatic restart on failure. - - Uses infinity_polling() instead of polling() for better error recovery. - If polling crashes (network issues, timeouts, etc.), it will automatically - restart after a short delay. This prevents the silent thread death that - causes callback buttons to stop working. - """ - restart_count = 0 - while True: - try: - if restart_count > 0: - logging.warning(f"Telegram polling restart #{restart_count}") - send_telegram_notification( - f"⚠️ Telegram poller restarted (attempt #{restart_count}). " - "If you see this frequently, check network connectivity.", - level="warning" - ) - logging.info("Starting Telegram bot infinity_polling...") - # infinity_polling handles most transient errors internally - # timeout: connection timeout for requests - # long_polling_timeout: how long Telegram waits before returning empty response - bot.infinity_polling(timeout=60, long_polling_timeout=30) - except Exception as e: - restart_count += 1 - logging.error(f"Telegram polling crashed with error: {e}. Restarting in 10 seconds... (restart #{restart_count})") - time.sleep(10) - # Start the Telegram bot polling in a separate daemon thread logging.info("Starting Telegram bot poller thread with infinity_polling.") telegram_thread = threading.Thread(target=run_telegram_polling, name="TelegramPoller", daemon=True)