refactor: remove unused transfer functions and streamline imports in wallet and models

This commit is contained in:
bohd4nx
2026-05-29 01:17:57 +03:00
parent d4e3d3491b
commit 6cb6e3fe05
10 changed files with 87 additions and 202 deletions
+3 -3
View File
@@ -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:
-3
View File
@@ -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",
]
+50 -31
View File
@@ -51,40 +51,28 @@ def clean_decode(payload: str) -> str | Cell:
raise ParseError(ParseError.UNPARSEABLE.format(context="payload decode", exc=exc)) from exc
async def process_transaction(
client: FragmentClient,
transaction_data: dict[str, Any],
payment_method: PaymentMethod = "ton",
required_payment_amount: float | None = None,
) -> str:
"""Sign and broadcast a Fragment transaction with the seeded TON wallet.
Args:
client: Authenticated `FragmentClient` instance.
transaction_data: Raw Fragment transaction payload returned by the API.
payment_method: Payment currency to use for the purchase flow.
required_payment_amount: Optional amount returned by Fragment's init request.
Returns:
Normalized transaction hash string.
"""
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
message = transaction_data["transaction"]["messages"][0]
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.
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":
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.
@@ -96,10 +84,9 @@ async def process_transaction(
logger.exception("Failed to validate balances before broadcasting transaction")
raise WalletError(WalletError.TON_BALANCE_CHECK_FAILED.format(exc=exc)) from exc
try:
raw_payload = str(message.get("payload", ""))
payload = clean_decode(raw_payload)
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(
@@ -130,6 +117,41 @@ async def process_transaction(
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],
payment_method: PaymentMethod = "ton",
required_payment_amount: float | None = None,
) -> str:
"""Sign and broadcast a Fragment transaction with the seeded TON wallet.
Args:
client: Authenticated `FragmentClient` instance.
transaction_data: Raw Fragment transaction payload returned by the API.
payment_method: Payment currency to use for the purchase flow.
required_payment_amount: Optional amount returned by Fragment's init request.
Returns:
Normalized transaction hash string.
"""
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)
await _check_payment_balances(wallet, payment_method, amount_ton, required_payment_amount, transaction_data, ton)
payload = clean_decode(str(message.get("payload", "")))
try:
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"))
-105
View File
@@ -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
+1 -3
View File
@@ -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",
]
+1 -21
View File
@@ -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"]
+2 -3
View File
@@ -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
+2 -3
View File
@@ -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 -2
View File
@@ -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 -2
View File
@@ -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