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
+14
View File
@@ -49,6 +49,20 @@ Requires Python 3.10+.
---
## Logging
`pyfragment` uses standard Python logging under the `pyfragment` namespace and is silent by default.
To enable logs, configure your app's logging and set the level:
```python
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("pyfragment").setLevel(logging.DEBUG) # DEBUG for detailed request logs
```
---
## Credentials
**Fragment cookies** — log in to [fragment.com](https://fragment.com) and connect your TON wallet. You can get cookies in two ways:
+3
View File
@@ -1,3 +1,4 @@
import logging
from importlib.metadata import version
from pyfragment.client import FragmentClient
@@ -26,6 +27,8 @@ from pyfragment.models.marketplace import GiftsResult, NumbersResult, UsernamesR
from pyfragment.models.payments import AdsRechargeResult, AdsTopupResult, PremiumResult, StarsResult
from pyfragment.models.wallet import WalletInfo
logging.getLogger("pyfragment").addHandler(logging.NullHandler())
__version__: str = version("pyfragment")
__all__ = [
+36 -26
View File
@@ -3,12 +3,10 @@ from __future__ import annotations
import json
from typing import Any, cast, get_args
import httpx
from pyfragment.core.constants import BASE_HEADERS, DEFAULT_TIMEOUT, FRAGMENT_BASE_URL, REQUIRED_COOKIE_KEYS
from pyfragment.core.transport import fragment_request, get_fragment_hash
from pyfragment.core.constants import DEFAULT_TIMEOUT, FRAGMENT_BASE_URL, REQUIRED_COOKIE_KEYS
from pyfragment.domains.ads.service import AdsService
from pyfragment.domains.anonymous_numbers.service import AnonymousNumbersService
from pyfragment.domains.base import raw_api_call
from pyfragment.domains.giveaways.service import GiveawaysService
from pyfragment.domains.marketplace.service import MarketplaceService
from pyfragment.domains.purchases.service import PurchasesService
@@ -31,7 +29,7 @@ class FragmentClient:
connected with Fragment or Telegram.
Args:
seed: 24-word mnemonic phrase for the TON wallet.
seed: 12-, 18-, or 24-word mnemonic phrase for the TON wallet.
api_key: Tonapi API key — get one at https://tonconsole.com.
cookies: Fragment session cookies as a dict or JSON string.
wallet_version: Wallet contract version — ``"V4R2"`` or ``"V5R1"`` (default).
@@ -53,14 +51,17 @@ class FragmentClient:
print(result.transaction_id)
"""
def __init__(
self,
seed: str,
api_key: str,
cookies: dict[str, Any] | str,
wallet_version: str = "V5R1",
timeout: float = DEFAULT_TIMEOUT,
) -> None:
@staticmethod
def _parse_cookies(cookies: dict[str, Any] | str) -> dict[str, Any]:
if isinstance(cookies, str):
try:
cookies = json.loads(cookies)
except Exception as exc:
raise CookieError(CookieError.READ_FAILED.format(exc=exc)) from exc
return cast(dict[str, Any], cookies)
@staticmethod
def _validate_required(seed: str, api_key: str) -> None:
missing = [name for name, val in (("seed", seed), ("api_key", api_key)) if not val or not str(val).strip()]
if missing:
raise ConfigurationError(ConfigurationError.MISSING_VARS.format(keys=", ".join(missing)))
@@ -72,16 +73,14 @@ class FragmentClient:
if len(api_key.strip()) < 68:
raise ConfigurationError(ConfigurationError.INVALID_API_KEY.format(length=len(api_key.strip())))
if isinstance(cookies, str):
try:
cookies = json.loads(cookies)
except Exception as exc:
raise CookieError(CookieError.READ_FAILED.format(exc=exc)) from exc
missing_keys = [k for k in REQUIRED_COOKIE_KEYS if not str(cast(dict[str, Any], cookies).get(k, "")).strip()]
@staticmethod
def _validate_cookie_keys(cookies: dict[str, Any]) -> None:
missing_keys = [k for k in REQUIRED_COOKIE_KEYS if not str(cookies.get(k, "")).strip()]
if missing_keys:
raise CookieError(CookieError.MISSING_KEYS.format(keys=", ".join(missing_keys)))
@staticmethod
def _normalize_wallet_version(wallet_version: str) -> WalletVersion:
version = wallet_version.strip().upper()
if version not in get_args(WalletVersion):
raise ConfigurationError(
@@ -89,11 +88,25 @@ class FragmentClient:
version=version, supported=", ".join(sorted(get_args(WalletVersion)))
)
)
return cast(WalletVersion, version)
def __init__(
self,
seed: str,
api_key: str,
cookies: dict[str, Any] | str,
wallet_version: str = "V5R1",
timeout: float = DEFAULT_TIMEOUT,
) -> None:
self._validate_required(seed, api_key)
parsed_cookies = self._parse_cookies(cookies)
self._validate_cookie_keys(parsed_cookies)
version = self._normalize_wallet_version(wallet_version)
self.seed: str = seed.strip()
self.api_key: str = api_key.strip()
self.cookies: dict[str, Any] = cast(dict[str, Any], cookies)
self.wallet_version: WalletVersion = version # type: ignore[assignment]
self.cookies: dict[str, Any] = parsed_cookies
self.wallet_version: WalletVersion = version
self.timeout: float = timeout
self.marketplace = MarketplaceService(self)
self.purchases = PurchasesService(self)
@@ -371,7 +384,4 @@ class FragmentClient:
page_url="https://fragment.com/premium/gift",
)
"""
headers = {**BASE_HEADERS, "referer": page_url, "x-aj-referer": page_url}
async with httpx.AsyncClient(cookies=self.cookies, timeout=self.timeout) as session:
fragment_hash = await get_fragment_hash(self.cookies, headers, page_url, self.timeout)
return await fragment_request(session, fragment_hash, headers, {"method": method, **(data or {})})
return await raw_api_call(self.cookies, self.timeout, method, data, page_url)
+7 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import logging
from typing import TYPE_CHECKING
from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE
@@ -13,6 +14,9 @@ if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def recharge_ads(client: FragmentClient, account: str, amount: int) -> AdsRechargeResult:
if not isinstance(amount, int) or not (1 <= amount <= 1_000_000_000):
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT)
@@ -42,7 +46,9 @@ async def recharge_ads(client: FragmentClient, account: str, amount: int) -> Ads
tx_hash = await process_transaction(client, transaction)
return AdsRechargeResult(transaction_id=tx_hash, amount=amount)
except FragmentError:
except FragmentError as exc:
logger.error("Failed to recharge Ads account '%s' for %s TON: %s", account, amount, exc, exc_info=True)
raise
except Exception as exc:
logger.exception("Failed to recharge Ads account '%s' for %s TON due to an unexpected error", account, amount)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+7 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import logging
from typing import TYPE_CHECKING
from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE
@@ -21,6 +22,9 @@ if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def topup_ton(client: FragmentClient, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
if not isinstance(amount, int) or not (1 <= amount <= 1_000_000_000):
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT)
@@ -57,7 +61,9 @@ async def topup_ton(client: FragmentClient, username: str, amount: int, show_sen
tx_hash = await process_transaction(client, transaction, required_payment_amount=required_payment_amount)
return AdsTopupResult(transaction_id=tx_hash, username=username, amount=amount)
except FragmentError:
except FragmentError as exc:
logger.error("Failed to top up TON for user '%s' with %s TON: %s", username, amount, exc, exc_info=True)
raise
except Exception as exc:
logger.exception("Failed to top up TON for user '%s' with %s TON due to an unexpected error", username, amount)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+23 -3
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import html
import logging
from typing import TYPE_CHECKING
from pyfragment.core.constants import NUMBERS_PAGE
@@ -12,6 +13,9 @@ if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
def _strip_plus(number: str) -> str:
return number.lstrip("+") if isinstance(number, str) else number
@@ -32,9 +36,11 @@ async def get_login_code(client: FragmentClient, number: str) -> LoginCodeResult
return LoginCodeResult(number=number, code=code, active_sessions=active_sessions)
except FragmentError:
except FragmentError as exc:
logger.error("Failed to get login code for number '%s': %s", number, exc, exc_info=True)
raise
except Exception as exc:
logger.exception("Failed to get login code for number '%s' due to an unexpected error", number)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
@@ -50,9 +56,21 @@ async def toggle_login_codes(client: FragmentClient, number: str, can_receive: b
if result.get("error"):
raise FragmentAPIError(html.unescape(result["error"]))
except FragmentError:
except FragmentError as exc:
logger.error(
"Failed to toggle login code delivery for number '%s' (can_receive=%s): %s",
number,
can_receive,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to toggle login code delivery for number '%s' (can_receive=%s) due to an unexpected error",
number,
can_receive,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
@@ -88,7 +106,9 @@ async def terminate_sessions(client: FragmentClient, number: str) -> TerminateSe
return TerminateSessionsResult(number=number, message=result.get("msg"))
except FragmentError:
except FragmentError as exc:
logger.error("Failed to terminate sessions for number '%s': %s", number, exc, exc_info=True)
raise
except Exception as exc:
logger.exception("Failed to terminate sessions for number '%s' due to an unexpected error", number)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+30 -1
View File
@@ -1,10 +1,39 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import logging
from typing import TYPE_CHECKING, Any
import httpx
from pyfragment.core.constants import BASE_HEADERS
from pyfragment.core.transport import fragment_request, get_fragment_hash
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def raw_api_call(
cookies: dict[str, Any],
timeout: float,
method: str,
data: dict[str, Any] | None,
page_url: str,
) -> dict[str, Any]:
payload = {"method": method, **(data or {})}
headers = {**BASE_HEADERS, "referer": page_url, "x-aj-referer": page_url}
logger.debug("Starting Fragment API call '%s' on %s", method, page_url)
try:
async with httpx.AsyncClient(cookies=cookies, timeout=timeout) as session:
fragment_hash = await get_fragment_hash(cookies, headers, page_url, timeout)
response = await fragment_request(session, fragment_hash, headers, payload)
logger.debug("Completed Fragment API call '%s' with response keys: %s", method, sorted(response.keys()))
return response
except Exception:
logger.exception("Failed to call Fragment API method '%s' on %s", method, page_url)
raise
class BaseService:
def __init__(self, client: FragmentClient) -> None:
+56 -2
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
import json
import logging
import random
from typing import TYPE_CHECKING, get_args
from pyfragment.core.constants import DEVICE, PREMIUM_GIVEAWAY_PAGE, STARS_GIVEAWAY_PAGE
@@ -22,6 +24,9 @@ if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def giveaway_stars(
client: FragmentClient,
channel: str,
@@ -47,6 +52,12 @@ async def giveaway_stars(
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
await client.call(
"updateStarsGiveawayState",
{"mode": "new", "lv": "false", "dh": str(random.randint(100_000_000, 999_999_999))},
page_url=STARS_GIVEAWAY_PAGE,
)
result = await client.call(
"initGiveawayStarsRequest",
{
@@ -84,9 +95,25 @@ async def giveaway_stars(
)
return StarsGiveawayResult(transaction_id=tx_hash, channel=channel, winners=winners, amount=amount)
except FragmentError:
except FragmentError as exc:
logger.error(
"Failed to run Stars giveaway for channel '%s' (winners=%s, amount=%s, payment_method='%s'): %s",
channel,
winners,
amount,
payment_method,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to run Stars giveaway for channel '%s' (winners=%s, amount=%s, payment_method='%s') due to an unexpected error",
channel,
winners,
amount,
payment_method,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
@@ -119,6 +146,17 @@ async def giveaway_premium(
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
await client.call(
"updatePremiumGiveawayState",
{
"mode": "new",
"lv": "false",
"dh": str(random.randint(100_000_000, 999_999_999)),
"quantity": "",
},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
result = await client.call(
"initGiveawayPremiumRequest",
{
@@ -156,7 +194,23 @@ async def giveaway_premium(
)
return PremiumGiveawayResult(transaction_id=tx_hash, channel=channel, winners=winners, amount=months)
except FragmentError:
except FragmentError as exc:
logger.error(
"Failed to run Premium giveaway for channel '%s' (winners=%s, months=%s, payment_method='%s'): %s",
channel,
winners,
months,
payment_method,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to run Premium giveaway for channel '%s' (winners=%s, months=%s, payment_method='%s') due to an unexpected error",
channel,
winners,
months,
payment_method,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+39 -3
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from pyfragment.core.constants import FRAGMENT_BASE_URL, GIFTS_PAGE, NUMBERS_PAGE
@@ -11,6 +12,9 @@ if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def search_usernames(
client: FragmentClient,
query: str = "",
@@ -36,9 +40,19 @@ async def search_usernames(
next_offset_id = str(raw_noi) if raw_noi else None
return UsernamesResult(items=items, next_offset_id=next_offset_id)
except FragmentError:
except FragmentError as exc:
logger.error(
"Failed to search usernames (query='%s', sort='%s', filter='%s', offset_id='%s'): %s",
query,
sort,
filter,
offset_id,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception("Failed to search usernames for query '%s' due to an unexpected error", query)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
@@ -67,9 +81,19 @@ async def search_numbers(
next_offset_id = str(raw_noi) if raw_noi else None
return NumbersResult(items=items, next_offset_id=next_offset_id)
except FragmentError:
except FragmentError as exc:
logger.error(
"Failed to search numbers (query='%s', sort='%s', filter='%s', offset_id='%s'): %s",
query,
sort,
filter,
offset_id,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception("Failed to search numbers for query '%s' due to an unexpected error", query)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
@@ -106,7 +130,19 @@ async def search_gifts(
items, next_offset = parse_gift_items(result.get("html") or "")
return GiftsResult(items=items, next_offset=next_offset)
except FragmentError:
except FragmentError as exc:
logger.error(
"Failed to search gifts (query='%s', collection='%s', sort='%s', filter='%s', view='%s', offset='%s'): %s",
query,
collection,
sort,
filter,
view,
offset,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception("Failed to search gifts for query '%s' due to an unexpected error", query)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+34 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import logging
import time
from typing import TYPE_CHECKING, get_args
@@ -23,6 +24,9 @@ if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def purchase_stars(
client: FragmentClient,
username: str,
@@ -84,9 +88,23 @@ async def purchase_stars(
)
return StarsResult(transaction_id=tx_hash, username=username, amount=amount)
except FragmentError:
except FragmentError as exc:
logger.error(
"Failed to purchase %s Stars for user '%s' using '%s': %s",
amount,
username,
payment_method,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to purchase %s Stars for user '%s' using '%s' due to an unexpected error",
amount,
username,
payment_method,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
@@ -151,7 +169,21 @@ async def purchase_premium(
)
return PremiumResult(transaction_id=tx_hash, username=username, amount=months)
except FragmentError:
except FragmentError as exc:
logger.error(
"Failed to purchase %s months of Premium for user '%s' using '%s': %s",
months,
username,
payment_method,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to purchase %s months of Premium for user '%s' using '%s' due to an unexpected error",
months,
username,
payment_method,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+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
+3 -1
View File
@@ -165,6 +165,7 @@ async def test_giveaway_stars_success(client: FragmentClient) -> None:
AsyncMock(
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{},
{"req_id": FAKE_REQ_ID},
FAKE_TRANSACTION,
]
@@ -187,6 +188,7 @@ async def test_giveaway_stars_passes_payment_method(client: FragmentClient) -> N
call_mock = AsyncMock(
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{},
{"req_id": FAKE_REQ_ID},
FAKE_TRANSACTION,
]
@@ -199,7 +201,7 @@ async def test_giveaway_stars_passes_payment_method(client: FragmentClient) -> N
):
await client.giveaway_stars("@channel", winners=3, amount=1000, payment_method="usdt_ton")
init_call = call_mock.await_args_list[1]
init_call = call_mock.await_args_list[2]
assert init_call.args[0] == "initGiveawayStarsRequest"
assert init_call.args[1]["payment_method"] == "usdt_ton"
assert proc_mock.await_args is not None
+3 -1
View File
@@ -150,6 +150,7 @@ async def test_giveaway_premium_success(client: FragmentClient) -> None:
AsyncMock(
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{},
{"req_id": FAKE_REQ_ID},
FAKE_TRANSACTION,
]
@@ -172,6 +173,7 @@ async def test_giveaway_premium_passes_payment_method(client: FragmentClient) ->
call_mock = AsyncMock(
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{},
{"req_id": FAKE_REQ_ID},
FAKE_TRANSACTION,
]
@@ -184,7 +186,7 @@ async def test_giveaway_premium_passes_payment_method(client: FragmentClient) ->
):
await client.giveaway_premium("@channel", winners=10, months=6, payment_method="usdt_ton")
init_call = call_mock.await_args_list[1]
init_call = call_mock.await_args_list[2]
assert init_call.args[0] == "initGiveawayPremiumRequest"
assert init_call.args[1]["payment_method"] == "usdt_ton"
assert proc_mock.await_args is not None