feat: implement logging throughout the application and enhance error handling

This commit is contained in:
bohd4nx
2026-05-29 00:58:00 +03:00
parent e9dd706fa6
commit 546bcb337c
15 changed files with 321 additions and 41 deletions
+25
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import base64
import logging
from typing import TYPE_CHECKING, Any
from ton_core import NetworkGlobalID
@@ -16,6 +17,9 @@ if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def get_usdt_balance(ton: Any, wallet_address: str) -> float:
"""Return the USDT balance for a Fragment-linked TON wallet."""
try:
@@ -29,9 +33,12 @@ async def get_usdt_balance(ton: Any, wallet_address: str) -> float:
return float(raw_balance) / 1_000_000.0
except ProviderResponseError as exc:
if exc.code == 404:
logger.debug("No USDT jetton wallet found for '%s'; treating balance as 0", wallet_address)
return 0.0
logger.error("Failed to load USDT balance for wallet '%s': %s", wallet_address, exc, exc_info=True)
raise WalletError(WalletError.USDT_BALANCE_CHECK_FAILED.format(exc=exc)) from exc
except Exception as exc:
logger.exception("Failed to load USDT balance for wallet '%s' due to an unexpected error", wallet_address)
raise WalletError(WalletError.USDT_BALANCE_CHECK_FAILED.format(exc=exc)) from exc
@@ -47,6 +54,11 @@ async def check_ton_payment_balance(
required_ton = max(tx_price_ton, MIN_TON_BALANCE)
if balance_ton < required_ton:
logger.error(
"Failed TON balance check: balance=%s TON, required=%s TON",
round(balance_ton, 6),
round(required_ton, 6),
)
raise WalletError(WalletError.LOW_TON_BALANCE.format(balance=balance_ton, required=required_ton))
@@ -58,11 +70,22 @@ async def check_usdt_payment_balance(
) -> None:
"""Validate that the wallet can cover a USDT-denominated payment."""
if balance_ton < MIN_TON_BALANCE:
logger.error(
"Failed TON gas reserve check for USDT payment: balance=%s TON, required=%s TON",
round(balance_ton, 6),
MIN_TON_BALANCE,
)
raise WalletError(WalletError.LOW_TON_BALANCE.format(balance=balance_ton, required=MIN_TON_BALANCE))
usdt_balance = await get_usdt_balance(ton, wallet_address)
required_usdt = required_payment_amount if required_payment_amount is not None else MIN_USDT_BALANCE
if usdt_balance < required_usdt:
logger.error(
"Failed USDT balance check for wallet '%s': balance=%s USDT, required=%s USDT",
wallet_address,
round(usdt_balance, 6),
round(required_usdt, 6),
)
raise WalletError(WalletError.LOW_USDT_BALANCE.format(balance=usdt_balance, required=required_usdt))
@@ -80,6 +103,7 @@ async def get_account_info(client: FragmentClient) -> dict[str, Any]:
"walletStateInit": base64.b64encode(boc).decode(),
}
except Exception as exc:
logger.exception("Failed to build Fragment account info from the configured wallet")
raise WalletError(WalletError.ACCOUNT_INFO_FAILED.format(exc=exc)) from exc
@@ -99,4 +123,5 @@ async def get_wallet_info(client: FragmentClient) -> WalletInfo:
usdt_balance=round(usdt_balance, 4),
)
except Exception as exc:
logger.exception("Failed to fetch wallet info from Tonapi")
raise WalletError(WalletError.WALLET_INFO_FAILED.format(exc=exc)) from exc
+27
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import base64
import logging
import random
import ssl
from typing import TYPE_CHECKING, Any
@@ -19,6 +20,9 @@ if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
def clean_decode(payload: str) -> str | Cell:
"""Decode a base64 BOC comment from Fragment into text when possible.
@@ -43,6 +47,7 @@ def clean_decode(payload: str) -> str | Cell:
except UnicodeDecodeError:
return cell
except Exception as exc:
logger.exception("Failed to decode Fragment payload")
raise ParseError(ParseError.UNPARSEABLE.format(context="payload decode", exc=exc)) from exc
@@ -64,6 +69,7 @@ async def process_transaction(
Normalized transaction hash string.
"""
if "transaction" not in transaction_data or not transaction_data["transaction"].get("messages"):
logger.error("Failed to process transaction: missing transaction payload or messages")
raise TransactionError(TransactionError.INVALID_PAYLOAD)
message = transaction_data["transaction"]["messages"][0]
@@ -87,6 +93,7 @@ async def process_transaction(
except WalletError:
raise
except Exception as exc:
logger.exception("Failed to validate balances before broadcasting transaction")
raise WalletError(WalletError.TON_BALANCE_CHECK_FAILED.format(exc=exc)) from exc
try:
@@ -103,12 +110,24 @@ async def process_transaction(
return str(result.normalized_hash)
except ProviderResponseError as exc:
if exc.code == 429 and attempt == 0:
logger.warning(
"Broadcast rate-limited (429), retrying transaction once: %s",
exc,
exc_info=True,
)
await asyncio.sleep(1 + random.uniform(0, 0.5))
continue
if exc.code == 406 and "seqno" in str(exc).lower():
if attempt < 2:
logger.warning(
"Broadcast seqno conflict (406), retrying attempt %s: %s",
attempt + 2,
exc,
exc_info=True,
)
await asyncio.sleep(2 + random.uniform(0, 1))
continue
logger.error("Failed to broadcast transaction after seqno retries")
raise TransactionError(TransactionError.DUPLICATE_SEQNO) from exc
raise
except (WalletError, TransactionError):
@@ -117,8 +136,16 @@ async def process_transaction(
cause: BaseException | None = exc
while cause is not None:
if isinstance(cause, ssl.SSLError):
logger.exception("Failed to broadcast transaction due to SSL error")
raise TransactionError(TransactionError.BROADCAST_FAILED_SSL.format(exc=exc)) from exc
cause = cause.__cause__ or cause.__context__
logger.exception(
"Failed to broadcast transaction to '%s' for %s nanotons using payment method '%s'",
message["address"],
message["amount"],
payment_method,
)
raise TransactionError(TransactionError.BROADCAST_FAILED.format(exc=exc)) from exc
logger.error("Failed to broadcast transaction: transfer loop exited without result")
raise TransactionError(TransactionError.BROADCAST_FAILED.format(exc="transfer loop exited without result"))
+14
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from ton_core import Address, NetworkGlobalID
@@ -14,6 +15,9 @@ if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def send_ton_transfer(
client: FragmentClient,
destination: str,
@@ -47,6 +51,11 @@ async def send_ton_transfer(
except (TransactionError, WalletError):
raise
except Exception as exc:
logger.exception(
"Failed to send TON transfer to '%s' for %s nanotons",
destination,
amount,
)
raise TransactionError(TransactionError.BROADCAST_FAILED.format(exc=exc)) from exc
@@ -88,4 +97,9 @@ async def send_usdt_transfer(
except (TransactionError, WalletError):
raise
except Exception as exc:
logger.exception(
"Failed to send USDT transfer to '%s' for %s base units",
destination,
usdt_amount,
)
raise TransactionError(TransactionError.BROADCAST_FAILED.format(exc=exc)) from exc