mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
refactor: remove unused transfer functions and streamline imports in wallet and models
This commit is contained in:
@@ -9,9 +9,9 @@ from pyfragment.exceptions import CookieError
|
||||
from pyfragment.models.cookies import CookieResult
|
||||
|
||||
try:
|
||||
import rookiepy # type: ignore[import-not-found]
|
||||
except Exception:
|
||||
rookiepy = None
|
||||
import rookiepy
|
||||
except Exception: # noqa: BLE001
|
||||
rookiepy = None # type: ignore[assignment]
|
||||
|
||||
|
||||
def get_cookies_from_browser(browser: str = "chrome") -> CookieResult:
|
||||
|
||||
@@ -7,7 +7,6 @@ from pyfragment.domains.tonapi.account import (
|
||||
)
|
||||
from pyfragment.domains.tonapi.service import TonapiService
|
||||
from pyfragment.domains.tonapi.transaction import clean_decode, process_transaction
|
||||
from pyfragment.domains.tonapi.transfer import send_ton_transfer, send_usdt_transfer
|
||||
|
||||
__all__ = [
|
||||
"TonapiService",
|
||||
@@ -18,6 +17,4 @@ __all__ = [
|
||||
"get_usdt_balance",
|
||||
"get_wallet_info",
|
||||
"process_transaction",
|
||||
"send_ton_transfer",
|
||||
"send_usdt_transfer",
|
||||
]
|
||||
|
||||
@@ -51,6 +51,77 @@ def clean_decode(payload: str) -> str | Cell:
|
||||
raise ParseError(ParseError.UNPARSEABLE.format(context="payload decode", exc=exc)) from exc
|
||||
|
||||
|
||||
def _extract_message(transaction_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate and extract the first message from a Fragment transaction payload."""
|
||||
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)
|
||||
result: dict[str, Any] = transaction_data["transaction"]["messages"][0]
|
||||
return result
|
||||
|
||||
|
||||
async def _check_payment_balances(
|
||||
wallet: Any,
|
||||
payment_method: PaymentMethod,
|
||||
amount_ton: float,
|
||||
required_payment_amount: float | None,
|
||||
transaction_data: dict[str, Any],
|
||||
ton: Any,
|
||||
) -> None:
|
||||
"""Refresh wallet and verify sufficient balance before broadcasting."""
|
||||
try:
|
||||
await wallet.refresh()
|
||||
balance_ton = wallet.balance / 1_000_000_000
|
||||
if payment_method == "ton":
|
||||
await check_ton_payment_balance(balance_ton, amount_ton, required_payment_amount)
|
||||
else:
|
||||
# USDT is paid from the Fragment-linked wallet, not the signing wallet.
|
||||
fragment_wallet_address = transaction_data["transaction"].get("from", "")
|
||||
await check_usdt_payment_balance(balance_ton, required_payment_amount, ton, fragment_wallet_address)
|
||||
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
|
||||
|
||||
|
||||
async def _broadcast_with_retry(wallet: Any, message: dict[str, Any], payload: str | Cell) -> str:
|
||||
"""Attempt to broadcast a transaction up to 3 times, handling rate-limit and seqno errors."""
|
||||
for attempt in range(3):
|
||||
try:
|
||||
result = await wallet.transfer(
|
||||
destination=message["address"],
|
||||
amount=int(message["amount"]), # nanotons, not TON
|
||||
body=payload,
|
||||
)
|
||||
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
|
||||
|
||||
logger.error("Failed to broadcast transaction: transfer loop exited without result")
|
||||
raise TransactionError(TransactionError.BROADCAST_FAILED.format(exc="transfer loop exited without result"))
|
||||
|
||||
|
||||
async def process_transaction(
|
||||
client: FragmentClient,
|
||||
transaction_data: dict[str, Any],
|
||||
@@ -68,68 +139,19 @@ async def process_transaction(
|
||||
Returns:
|
||||
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]
|
||||
message = _extract_message(transaction_data)
|
||||
amount_ton = int(message["amount"]) / 1_000_000_000
|
||||
|
||||
async with TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key) as ton:
|
||||
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
||||
wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
||||
|
||||
# Check balance first so we fail before trying to broadcast on-chain.
|
||||
try:
|
||||
await wallet.refresh()
|
||||
balance_ton = wallet.balance / 1_000_000_000
|
||||
if payment_method == "ton":
|
||||
wallet.address.to_str(False, False)
|
||||
await check_ton_payment_balance(balance_ton, amount_ton, required_payment_amount)
|
||||
else:
|
||||
# USDT is paid from the Fragment-linked wallet, not the signing wallet.
|
||||
fragment_wallet_address = transaction_data["transaction"].get("from", "")
|
||||
await check_usdt_payment_balance(balance_ton, required_payment_amount, ton, fragment_wallet_address)
|
||||
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
|
||||
await _check_payment_balances(wallet, payment_method, amount_ton, required_payment_amount, transaction_data, ton)
|
||||
|
||||
payload = clean_decode(str(message.get("payload", "")))
|
||||
|
||||
try:
|
||||
raw_payload = str(message.get("payload", ""))
|
||||
payload = clean_decode(raw_payload)
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
result = await wallet.transfer(
|
||||
destination=message["address"],
|
||||
amount=int(message["amount"]), # nanotons, not TON
|
||||
body=payload,
|
||||
)
|
||||
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
|
||||
return await _broadcast_with_retry(wallet, message, payload)
|
||||
except (WalletError, TransactionError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
@@ -146,6 +168,3 @@ async def process_transaction(
|
||||
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"))
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ton_core import Address, NetworkGlobalID
|
||||
from tonutils.clients import ToncenterClient
|
||||
from tonutils.contracts import JettonTransferBuilder, TONTransferBuilder
|
||||
|
||||
from pyfragment.core.constants import USDT_TON_MASTER_ADDRESS, WALLET_CLASSES
|
||||
from pyfragment.exceptions import TransactionError, WalletError
|
||||
from pyfragment.models.wallet import TonTransferResult, UsdtTransferResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def send_ton_transfer(
|
||||
client: FragmentClient,
|
||||
destination: str,
|
||||
amount: int,
|
||||
body: str | None = None,
|
||||
) -> TonTransferResult:
|
||||
"""Send a direct TON transfer from the seeded wallet.
|
||||
|
||||
Args:
|
||||
client: Authenticated `FragmentClient` instance.
|
||||
destination: Recipient address in any TON-compatible format.
|
||||
amount: Amount in nanotons.
|
||||
body: Optional on-chain comment.
|
||||
"""
|
||||
try:
|
||||
async with ToncenterClient(network=NetworkGlobalID.MAINNET) as ton:
|
||||
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
||||
wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
||||
result = await wallet.transfer_message(
|
||||
TONTransferBuilder(
|
||||
destination=Address(destination),
|
||||
amount=amount,
|
||||
body=body,
|
||||
)
|
||||
)
|
||||
return TonTransferResult(
|
||||
transaction_id=str(result.normalized_hash),
|
||||
destination=destination,
|
||||
amount=amount,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
async def send_usdt_transfer(
|
||||
client: FragmentClient,
|
||||
destination: str,
|
||||
usdt_amount: int,
|
||||
forward_payload: str | None = None,
|
||||
ton_for_gas: int = 50_000_000,
|
||||
) -> UsdtTransferResult:
|
||||
"""Send a direct USDT transfer from the seeded wallet.
|
||||
|
||||
Args:
|
||||
client: Authenticated `FragmentClient` instance.
|
||||
destination: Recipient address in any TON-compatible format.
|
||||
usdt_amount: Amount in USDT base units (6 decimals).
|
||||
forward_payload: Optional comment passed through to the recipient.
|
||||
ton_for_gas: TON attached for gas in nanotons.
|
||||
"""
|
||||
try:
|
||||
async with ToncenterClient(network=NetworkGlobalID.MAINNET) as ton:
|
||||
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
||||
wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
||||
result = await wallet.transfer_message(
|
||||
JettonTransferBuilder(
|
||||
destination=Address(destination),
|
||||
jetton_amount=usdt_amount,
|
||||
jetton_master_address=Address(USDT_TON_MASTER_ADDRESS),
|
||||
forward_payload=forward_payload,
|
||||
forward_amount=1,
|
||||
amount=ton_for_gas,
|
||||
)
|
||||
)
|
||||
return UsdtTransferResult(
|
||||
transaction_id=str(result.normalized_hash),
|
||||
destination=destination,
|
||||
amount=usdt_amount,
|
||||
)
|
||||
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
|
||||
@@ -4,7 +4,7 @@ from pyfragment.models.enums import PaymentMethod, WalletVersion
|
||||
from pyfragment.models.giveaways import PremiumGiveawayResult, StarsGiveawayResult
|
||||
from pyfragment.models.marketplace import GiftsResult, NumbersResult, UsernamesResult
|
||||
from pyfragment.models.payments import AdsRechargeResult, AdsTopupResult, PremiumResult, StarsResult
|
||||
from pyfragment.models.wallet import TonTransferResult, UsdtTransferResult, WalletInfo
|
||||
from pyfragment.models.wallet import WalletInfo
|
||||
|
||||
__all__ = [
|
||||
"AdsRechargeResult",
|
||||
@@ -19,9 +19,7 @@ __all__ = [
|
||||
"StarsGiveawayResult",
|
||||
"StarsResult",
|
||||
"TerminateSessionsResult",
|
||||
"TonTransferResult",
|
||||
"UsernamesResult",
|
||||
"UsdtTransferResult",
|
||||
"WalletInfo",
|
||||
"WalletVersion",
|
||||
]
|
||||
|
||||
@@ -17,24 +17,4 @@ class WalletInfo:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TonTransferResult:
|
||||
transaction_id: str
|
||||
destination: str
|
||||
amount: int
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"TonTransferResult(destination='{self.destination}', amount={self.amount} TON, tx='{self.transaction_id}')"
|
||||
|
||||
|
||||
@dataclass
|
||||
class UsdtTransferResult:
|
||||
transaction_id: str
|
||||
destination: str
|
||||
amount: int
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"UsdtTransferResult(destination='{self.destination}', amount={self.amount} USDT, tx='{self.transaction_id}')"
|
||||
|
||||
|
||||
__all__ = ["TonTransferResult", "UsdtTransferResult", "WalletInfo"]
|
||||
__all__ = ["WalletInfo"]
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Cover stars purchase and giveaway flows, including validation and request wiring."""
|
||||
|
||||
import importlib
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
_purchase_stars_mod = importlib.import_module("pyfragment.domains.purchases.purchase")
|
||||
_giveaway_stars_mod = importlib.import_module("pyfragment.domains.giveaways.giveaway")
|
||||
import pyfragment.domains.giveaways.giveaway as _giveaway_stars_mod
|
||||
import pyfragment.domains.purchases.purchase as _purchase_stars_mod
|
||||
from pyfragment import ConfigurationError, FragmentClient, StarsGiveawayResult, StarsResult, UserNotFoundError
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Cover premium purchase and giveaway flows, including validation and request wiring."""
|
||||
|
||||
import importlib
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
_purchase_premium_mod = importlib.import_module("pyfragment.domains.purchases.purchase")
|
||||
_giveaway_premium_mod = importlib.import_module("pyfragment.domains.giveaways.giveaway")
|
||||
import pyfragment.domains.giveaways.giveaway as _giveaway_premium_mod
|
||||
import pyfragment.domains.purchases.purchase as _purchase_premium_mod
|
||||
from pyfragment import ConfigurationError, FragmentClient, PremiumGiveawayResult, PremiumResult, UserNotFoundError
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Cover TON top-up through Telegram Ads, including recipient lookup and transaction building."""
|
||||
|
||||
import importlib
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
_topup_ton_mod = importlib.import_module("pyfragment.domains.ads.tonup")
|
||||
import pyfragment.domains.ads.tonup as _topup_ton_mod
|
||||
from pyfragment import AdsTopupResult, ConfigurationError, FragmentClient, UserNotFoundError
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Cover Telegram Ads recharge flow, including request preparation and KYC handling."""
|
||||
|
||||
import importlib
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
_recharge_ads_mod = importlib.import_module("pyfragment.domains.ads.recharge")
|
||||
import pyfragment.domains.ads.recharge as _recharge_ads_mod
|
||||
from pyfragment import AdsRechargeResult, ConfigurationError, FragmentClient
|
||||
from tests.shared import FAKE_ACCOUNT, FAKE_ADS_ACCOUNT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||
|
||||
|
||||
Reference in New Issue
Block a user