mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
Refactor tonapi module: Move account and transaction logic to services
- Moved account-related functions and classes from `pyfragment.domains.tonapi.account` to `pyfragment.services.tonapi.account`. - Moved transaction-related functions and classes from `pyfragment.domains.tonapi.transaction` to `pyfragment.services.tonapi.transaction`. - Updated imports across the codebase to reflect the new structure. - Removed unused `tonapi` module files and cleaned up related code. - Introduced `ApiProvider` enum to manage API provider types. - Added validation functions for cookies and wallet versions in a new `validation.py` module.
This commit is contained in:
@@ -7,8 +7,7 @@ from pyfragment.domains.anonymous_numbers.models import LoginCodeResult, Termina
|
||||
from pyfragment.domains.giveaways.models import PremiumGiveawayResult, StarsGiveawayResult
|
||||
from pyfragment.domains.marketplace.models import GiftsResult, NumbersResult, UsernamesResult
|
||||
from pyfragment.domains.purchases.models import PremiumResult, StarsResult
|
||||
from pyfragment.domains.tonapi.models import WalletInfo
|
||||
from pyfragment.enums import PaymentMethod, WalletVersion
|
||||
from pyfragment.enums import ApiProvider, PaymentMethod, WalletVersion
|
||||
from pyfragment.exceptions import (
|
||||
AlreadySubscribedError,
|
||||
AnonymousNumberError,
|
||||
@@ -27,6 +26,7 @@ from pyfragment.exceptions import (
|
||||
WalletError,
|
||||
)
|
||||
from pyfragment.services.cookies import CookieResult, get_cookies_from_browser
|
||||
from pyfragment.services.tonapi.models import WalletInfo
|
||||
|
||||
logging.getLogger("pyfragment").addHandler(logging.NullHandler())
|
||||
|
||||
@@ -66,6 +66,7 @@ __all__ = [
|
||||
"ParseError",
|
||||
"UnexpectedError",
|
||||
# literal types
|
||||
"ApiProvider",
|
||||
"PaymentMethod",
|
||||
"WalletVersion",
|
||||
"get_cookies_from_browser",
|
||||
|
||||
+54
-129
@@ -1,15 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from pyfragment.core.constants import (
|
||||
BASE_HEADERS,
|
||||
DEFAULT_TIMEOUT,
|
||||
FRAGMENT_BASE_URL,
|
||||
MNEMONIC_WORD_COUNTS_VALID,
|
||||
REQUIRED_COOKIE_KEYS,
|
||||
TONAPI_KEY_MIN_LENGTH,
|
||||
from pyfragment.core.constants import BASE_HEADERS, DEFAULT_TIMEOUT, FRAGMENT_BASE_URL
|
||||
from pyfragment.core.validation import (
|
||||
normalize_provider,
|
||||
normalize_wallet_version,
|
||||
parse_cookies,
|
||||
validate_cookie_keys,
|
||||
validate_credentials,
|
||||
)
|
||||
from pyfragment.domains.ads.models import AdsRechargeResult, AdsTopupResult
|
||||
from pyfragment.domains.ads.service import AdsService
|
||||
@@ -22,10 +21,9 @@ from pyfragment.domains.marketplace.models import GiftsResult, NumbersResult, Us
|
||||
from pyfragment.domains.marketplace.service import MarketplaceService
|
||||
from pyfragment.domains.purchases.models import PremiumResult, StarsResult
|
||||
from pyfragment.domains.purchases.service import PurchasesService
|
||||
from pyfragment.domains.tonapi.models import WalletInfo
|
||||
from pyfragment.domains.tonapi.service import TonapiService
|
||||
from pyfragment.enums import PaymentMethod, WalletVersion
|
||||
from pyfragment.exceptions import ConfigurationError, CookieError
|
||||
from pyfragment.enums import ApiProvider, PaymentMethod, WalletVersion
|
||||
from pyfragment.services.tonapi.models import WalletInfo
|
||||
from pyfragment.services.tonapi.service import TonapiService
|
||||
|
||||
|
||||
class FragmentClient:
|
||||
@@ -38,14 +36,17 @@ class FragmentClient:
|
||||
|
||||
Args:
|
||||
seed: 12- or 24-word mnemonic phrase for the GRAM (ex TON) wallet.
|
||||
api_key: Tonapi API key — get one at https://tonconsole.com.
|
||||
api_key: API key for the chosen provider — tonconsole.com (default) or t.me/toncenter.
|
||||
cookies: Fragment session cookies as a dict or JSON string.
|
||||
wallet_version: Wallet contract version — ``"V4R2"`` or ``"V5R1"`` (default).
|
||||
api_provider: Blockchain API provider — ``"tonapi"`` (tonconsole.com, default)
|
||||
or ``"toncenter"`` (t.me/toncenter).
|
||||
timeout: HTTP request timeout in seconds. Defaults to ``30.0``.
|
||||
headers: Custom HTTP request headers. If omitted, :data:`BASE_HEADERS` is used.
|
||||
|
||||
Raises:
|
||||
ConfigurationError: If ``seed``, ``api_key``, or ``wallet_version`` are missing or invalid.
|
||||
ConfigurationError: If ``seed``, ``api_key``, ``wallet_version``, or ``api_provider``
|
||||
are missing or invalid.
|
||||
CookieError: If ``cookies`` cannot be parsed or are missing required keys.
|
||||
|
||||
Example::
|
||||
@@ -60,62 +61,25 @@ class FragmentClient:
|
||||
print(result.transaction_id)
|
||||
"""
|
||||
|
||||
@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)))
|
||||
|
||||
word_count = len(seed.split())
|
||||
if word_count not in MNEMONIC_WORD_COUNTS_VALID:
|
||||
raise ConfigurationError(ConfigurationError.INVALID_MNEMONIC.format(count=word_count))
|
||||
|
||||
if len(api_key.strip()) < TONAPI_KEY_MIN_LENGTH:
|
||||
raise ConfigurationError(ConfigurationError.INVALID_API_KEY.format(length=len(api_key.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()
|
||||
try:
|
||||
return WalletVersion(version)
|
||||
except ValueError:
|
||||
raise ConfigurationError(
|
||||
ConfigurationError.UNSUPPORTED_VERSION.format(
|
||||
version=version, supported=", ".join(sorted(m.value for m in WalletVersion))
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
seed: str,
|
||||
api_key: str,
|
||||
cookies: dict[str, Any] | str,
|
||||
wallet_version: str = "V5R1",
|
||||
api_provider: str = "tonapi",
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> 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)
|
||||
validate_credentials(seed, api_key)
|
||||
provider = normalize_provider(api_provider)
|
||||
parsed_cookies = parse_cookies(cookies)
|
||||
validate_cookie_keys(parsed_cookies)
|
||||
version = normalize_wallet_version(wallet_version)
|
||||
|
||||
self.seed: str = seed.strip()
|
||||
self.api_key: str = api_key.strip()
|
||||
self.api_provider: ApiProvider = provider
|
||||
self.cookies: dict[str, Any] = parsed_cookies
|
||||
self.wallet_version: WalletVersion = version
|
||||
self.timeout: float = timeout
|
||||
@@ -134,7 +98,7 @@ class FragmentClient:
|
||||
pass
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"FragmentClient(wallet_version='{self.wallet_version}', cookies={len(self.cookies)} keys)"
|
||||
return f"FragmentClient(wallet_version='{self.wallet_version}', api_provider='{self.api_provider}', cookies={len(self.cookies)} keys)"
|
||||
|
||||
async def purchase_premium(
|
||||
self,
|
||||
@@ -149,9 +113,7 @@ class FragmentClient:
|
||||
username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``.
|
||||
months: Duration — ``3``, ``6``, or ``12``.
|
||||
show_sender: Show your name as the sender. Defaults to ``True``.
|
||||
payment_method: Payment currency — ``"ton"`` (GRAM (ex TON), default), ``"usdt_ton"`` (USDT on GRAM (ex TON)),
|
||||
``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
|
||||
or ``"usdc_pol"``.
|
||||
payment_method: Payment currency — defaults to ``PaymentMethod.GRAM``.
|
||||
|
||||
Returns:
|
||||
:class:`PremiumResult` with ``transaction_id``, ``username``, and ``amount``.
|
||||
@@ -171,9 +133,7 @@ class FragmentClient:
|
||||
username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``.
|
||||
amount: Number of stars — integer from ``50`` to ``10 000 000``.
|
||||
show_sender: Show your name as the gift sender. Defaults to ``True``.
|
||||
payment_method: Payment currency — ``"ton"`` (GRAM (ex TON), default), ``"usdt_ton"`` (USDT on GRAM (ex TON)),
|
||||
``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
|
||||
or ``"usdc_pol"``.
|
||||
payment_method: Payment currency — defaults to ``PaymentMethod.GRAM``.
|
||||
|
||||
Returns:
|
||||
:class:`StarsResult` with ``transaction_id``, ``username``, and ``amount``.
|
||||
@@ -197,8 +157,7 @@ class FragmentClient:
|
||||
"""Add funds to your own Telegram Ads account.
|
||||
|
||||
Args:
|
||||
account: Your Fragment Ads account identifier — the channel or bot username
|
||||
the Ads account is linked to (e.g. ``"@mychannel"``).
|
||||
account: Channel or bot username the Ads account is linked to (e.g. ``"@mychannel"``).
|
||||
amount: Amount in GRAM (ex TON) — integer from ``1`` to ``1 000 000 000``.
|
||||
|
||||
Returns:
|
||||
@@ -210,9 +169,7 @@ class FragmentClient:
|
||||
"""Return the address, state, and balances of the wallet.
|
||||
|
||||
Returns:
|
||||
:class:`WalletInfo` with ``address`` (``"UQ..."``), ``state``
|
||||
(``"active"``, ``"uninit"``, ``"nonexist"``, or ``"frozen"``),
|
||||
``gram_balance`` in GRAM (ex TON), and ``usdt_balance`` in USDT.
|
||||
:class:`WalletInfo` with ``address``, ``state``, ``gram_balance``, and ``usdt_balance``.
|
||||
"""
|
||||
return await self.tonapi.get_wallet()
|
||||
|
||||
@@ -229,13 +186,10 @@ class FragmentClient:
|
||||
channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``.
|
||||
winners: Number of winners — integer from ``1`` to ``15``.
|
||||
amount: Stars each winner receives — integer from ``500`` to ``1 000 000``.
|
||||
payment_method: Payment currency — ``"ton"`` (GRAM (ex TON), default), ``"usdt_ton"`` (USDT on GRAM (ex TON)),
|
||||
``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
|
||||
or ``"usdc_pol"``.
|
||||
payment_method: Payment currency — defaults to ``PaymentMethod.GRAM``.
|
||||
|
||||
Returns:
|
||||
:class:`StarsGiveawayResult` with ``transaction_id``, ``channel``,
|
||||
``winners``, and ``amount``.
|
||||
:class:`StarsGiveawayResult` with ``transaction_id``, ``channel``, ``winners``, and ``amount``.
|
||||
"""
|
||||
return await self.giveaways.giveaway_stars(channel, winners, amount, payment_method=payment_method)
|
||||
|
||||
@@ -252,13 +206,10 @@ class FragmentClient:
|
||||
channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``.
|
||||
winners: Number of winners — integer from ``1`` to ``24 000``.
|
||||
months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``.
|
||||
payment_method: Payment currency — ``"ton"`` (GRAM (ex TON), default), ``"usdt_ton"`` (USDT on GRAM (ex TON)),
|
||||
``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
|
||||
or ``"usdc_pol"``.
|
||||
payment_method: Payment currency — defaults to ``PaymentMethod.GRAM``.
|
||||
|
||||
Returns:
|
||||
:class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``,
|
||||
``winners``, and ``amount``.
|
||||
:class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``, ``winners``, and ``amount``.
|
||||
"""
|
||||
return await self.giveaways.giveaway_premium(channel, winners, months, payment_method=payment_method)
|
||||
|
||||
@@ -266,7 +217,7 @@ class FragmentClient:
|
||||
"""Fetch the current pending login code for an anonymous number.
|
||||
|
||||
Args:
|
||||
number: Phone number with or without leading ``+`` (e.g. ``"+1234567890"``).
|
||||
number: Phone number with or without leading ``+``.
|
||||
|
||||
Returns:
|
||||
:class:`LoginCodeResult` with ``number``, ``code`` (``None`` if none pending),
|
||||
@@ -293,7 +244,7 @@ class FragmentClient:
|
||||
:class:`TerminateSessionsResult` with ``number`` and ``message``.
|
||||
|
||||
Raises:
|
||||
AnonymousNumberError: If the number is not owned by this account or has no active sessions.
|
||||
AnonymousNumberError: If the number is not owned or has no active sessions.
|
||||
"""
|
||||
return await self.anonymous_numbers.terminate_sessions(number)
|
||||
|
||||
@@ -307,17 +258,13 @@ class FragmentClient:
|
||||
"""Search the Fragment marketplace for Telegram usernames.
|
||||
|
||||
Args:
|
||||
query: Search text (e.g. ``"durov"``). Omit or pass ``""`` to browse all.
|
||||
sort: Sort order — ``"price_desc"``, ``"price_asc"``, ``"listed"``, or
|
||||
``"ending"``. Omit to use Fragment's default ordering.
|
||||
filter: Filter results — ``"auction"``, ``"sale"``, ``"sold"``, or
|
||||
``""`` (available items). Omit to return all.
|
||||
offset_id: Pagination cursor — pass :attr:`UsernamesResult.next_offset_id`
|
||||
from a previous result to fetch the next page.
|
||||
query: Search text. Omit or pass ``""`` to browse all.
|
||||
sort: ``"price_desc"``, ``"price_asc"``, ``"listed"``, or ``"ending"``.
|
||||
filter: ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` (available).
|
||||
offset_id: Pass :attr:`UsernamesResult.next_offset_id` to fetch the next page.
|
||||
|
||||
Returns:
|
||||
:class:`UsernamesResult` with ``items`` (parsed list of item dicts)
|
||||
and ``next_offset_id`` (``None`` on the last page).
|
||||
:class:`UsernamesResult` with ``items`` and ``next_offset_id``.
|
||||
"""
|
||||
return await self.marketplace.search_usernames(query, sort=sort, filter=filter, offset_id=offset_id)
|
||||
|
||||
@@ -331,17 +278,13 @@ class FragmentClient:
|
||||
"""Search the Fragment marketplace for anonymous Telegram numbers.
|
||||
|
||||
Args:
|
||||
query: Search text (e.g. ``"888"``). Omit or pass ``""`` to browse all.
|
||||
sort: Sort order — ``"price_desc"``, ``"price_asc"``, ``"listed"``, or
|
||||
``"ending"``. Omit to use Fragment's default ordering.
|
||||
filter: Filter results — ``"auction"``, ``"sale"``, ``"sold"``, or
|
||||
``""`` (available items). Omit to return all.
|
||||
offset_id: Pagination cursor — pass :attr:`NumbersResult.next_offset_id`
|
||||
from a previous result to fetch the next page.
|
||||
query: Search text. Omit or pass ``""`` to browse all.
|
||||
sort: ``"price_desc"``, ``"price_asc"``, ``"listed"``, or ``"ending"``.
|
||||
filter: ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` (available).
|
||||
offset_id: Pass :attr:`NumbersResult.next_offset_id` to fetch the next page.
|
||||
|
||||
Returns:
|
||||
:class:`NumbersResult` with ``items`` (parsed list of item dicts)
|
||||
and ``next_offset_id`` (``None`` on the last page).
|
||||
:class:`NumbersResult` with ``items`` and ``next_offset_id``.
|
||||
"""
|
||||
return await self.marketplace.search_numbers(query, sort=sort, filter=filter, offset_id=offset_id)
|
||||
|
||||
@@ -358,22 +301,16 @@ class FragmentClient:
|
||||
"""Search the Fragment gifts marketplace.
|
||||
|
||||
Args:
|
||||
query: Search text. Omit or pass ``""`` to browse without filtering by name.
|
||||
collection: Filter by gift collection slug (e.g. ``"artisanbrick"``). Omit for all.
|
||||
sort: Sort order — ``"price_desc"``, ``"price_asc"``, ``"listed"``, or
|
||||
``"ending"``. Omit to use Fragment's default ordering.
|
||||
filter: Filter results — ``"auction"``, ``"sale"``, ``"sold"``, or
|
||||
``""`` (available items). Omit to return all.
|
||||
view: Active attribute tab name (e.g. ``"Model"``, ``"Backdrop"``). Omit for default.
|
||||
attr: Attribute filters — mapping of trait name to accepted values, e.g.
|
||||
``{"Model": ["Foosball"], "Backdrop": ["Celtic Blue", "Orange"]}``.
|
||||
Each key is sent as ``attr[Key]`` with its list of values.
|
||||
offset: Integer page offset from a previous :class:`GiftsResult`.
|
||||
Pass ``next_offset`` to fetch the next page.
|
||||
query: Search text. Omit or pass ``""`` to browse all.
|
||||
collection: Gift collection slug (e.g. ``"artisanbrick"``).
|
||||
sort: ``"price_desc"``, ``"price_asc"``, ``"listed"``, or ``"ending"``.
|
||||
filter: ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` (available).
|
||||
view: Active attribute tab name (e.g. ``"Model"``).
|
||||
attr: Attribute filters — e.g. ``{"Model": ["Foosball"], "Backdrop": ["Celtic Blue"]}``.
|
||||
offset: Pass :attr:`GiftsResult.next_offset` to fetch the next page.
|
||||
|
||||
Returns:
|
||||
:class:`GiftsResult` with ``items`` (parsed list of item dicts)
|
||||
and ``next_offset`` (``None`` on the last page).
|
||||
:class:`GiftsResult` with ``items`` and ``next_offset``.
|
||||
"""
|
||||
return await self.marketplace.search_gifts(
|
||||
query, collection=collection, sort=sort, filter=filter, view=view, attr=attr, offset=offset
|
||||
@@ -384,24 +321,12 @@ class FragmentClient:
|
||||
) -> dict[str, Any]:
|
||||
"""Send a raw request to the Fragment API.
|
||||
|
||||
Useful for accessing undocumented or future Fragment API methods
|
||||
without waiting for a library update.
|
||||
|
||||
Args:
|
||||
method: Fragment API method name, e.g. ``"searchPremiumGiftRecipient"``.
|
||||
data: Additional form-data fields to include in the request body.
|
||||
page_url: Fragment page URL used to derive the API hash and headers.
|
||||
Defaults to ``FRAGMENT_BASE_URL`` (``"https://fragment.com"``).
|
||||
data: Additional form-data fields.
|
||||
page_url: Fragment page URL to derive the API hash. Defaults to ``FRAGMENT_BASE_URL``.
|
||||
|
||||
Returns:
|
||||
Raw parsed JSON response as a dict.
|
||||
|
||||
Example::
|
||||
|
||||
result = await client.call(
|
||||
"searchPremiumGiftRecipient",
|
||||
{"query": "@username", "months": 3},
|
||||
page_url="https://fragment.com/premium/gift",
|
||||
)
|
||||
"""
|
||||
return await raw_api_call(self.cookies, self.timeout, method, data, page_url, self.headers)
|
||||
|
||||
@@ -82,6 +82,3 @@ PREMIUM_MONTHS_VALID: frozenset[int] = frozenset({3, 6, 12})
|
||||
|
||||
# Mnemonic phrase valid word counts
|
||||
MNEMONIC_WORD_COUNTS_VALID: frozenset[int] = frozenset({12, 24})
|
||||
|
||||
# Tonapi API key minimum length (tonapi.io)
|
||||
TONAPI_KEY_MIN_LENGTH: int = 68
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, cast
|
||||
|
||||
from pyfragment.core.constants import MNEMONIC_WORD_COUNTS_VALID, REQUIRED_COOKIE_KEYS
|
||||
from pyfragment.enums import ApiProvider, WalletVersion
|
||||
from pyfragment.exceptions import ConfigurationError, CookieError
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def validate_cookie_keys(cookies: dict[str, Any]) -> None:
|
||||
missing = [k for k in REQUIRED_COOKIE_KEYS if not str(cookies.get(k, "")).strip()]
|
||||
if missing:
|
||||
raise CookieError(CookieError.MISSING_KEYS.format(keys=", ".join(missing)))
|
||||
|
||||
|
||||
def normalize_provider(api_provider: str) -> ApiProvider:
|
||||
try:
|
||||
return ApiProvider(api_provider.strip().lower())
|
||||
except ValueError:
|
||||
raise ConfigurationError(
|
||||
ConfigurationError.UNSUPPORTED_PROVIDER.format(
|
||||
provider=api_provider,
|
||||
supported=", ".join(sorted(p.value for p in ApiProvider)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def normalize_wallet_version(wallet_version: str) -> WalletVersion:
|
||||
version = wallet_version.strip().upper()
|
||||
try:
|
||||
return WalletVersion(version)
|
||||
except ValueError:
|
||||
raise ConfigurationError(
|
||||
ConfigurationError.UNSUPPORTED_VERSION.format(
|
||||
version=version,
|
||||
supported=", ".join(sorted(m.value for m in WalletVersion)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def validate_credentials(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)))
|
||||
|
||||
word_count = len(seed.split())
|
||||
if word_count not in MNEMONIC_WORD_COUNTS_VALID:
|
||||
raise ConfigurationError(ConfigurationError.INVALID_MNEMONIC.format(count=word_count))
|
||||
@@ -6,9 +6,9 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE_INFO, GRAM_TOPUP_MAX, GRAM_TOPUP_MIN
|
||||
from pyfragment.domains.ads.models import AdsRechargeResult
|
||||
from pyfragment.domains.tonapi.account import get_account_info
|
||||
from pyfragment.domains.tonapi.transaction import process_transaction
|
||||
from pyfragment.exceptions import ConfigurationError, FragmentAPIError, FragmentError, UnexpectedError, VerificationError
|
||||
from pyfragment.services.tonapi.account import get_account_info
|
||||
from pyfragment.services.tonapi.transaction import process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
@@ -7,8 +7,6 @@ from typing import TYPE_CHECKING
|
||||
from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE_INFO, GRAM_TOPUP_MAX, GRAM_TOPUP_MIN
|
||||
from pyfragment.domains.ads.models import AdsTopupResult
|
||||
from pyfragment.domains.payments import parse_required_payment_amount
|
||||
from pyfragment.domains.tonapi.account import get_account_info
|
||||
from pyfragment.domains.tonapi.transaction import process_transaction
|
||||
from pyfragment.exceptions import (
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
@@ -17,6 +15,8 @@ from pyfragment.exceptions import (
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.services.tonapi.account import get_account_info
|
||||
from pyfragment.services.tonapi.transaction import process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
@@ -19,8 +19,6 @@ from pyfragment.core.constants import (
|
||||
)
|
||||
from pyfragment.domains.giveaways.models import PremiumGiveawayResult, StarsGiveawayResult
|
||||
from pyfragment.domains.payments import parse_required_payment_amount
|
||||
from pyfragment.domains.tonapi.account import get_account_info
|
||||
from pyfragment.domains.tonapi.transaction import process_transaction
|
||||
from pyfragment.enums import PaymentMethod
|
||||
from pyfragment.exceptions import (
|
||||
ConfigurationError,
|
||||
@@ -30,6 +28,8 @@ from pyfragment.exceptions import (
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.services.tonapi.account import get_account_info
|
||||
from pyfragment.services.tonapi.transaction import process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
@@ -15,8 +15,6 @@ from pyfragment.core.constants import (
|
||||
)
|
||||
from pyfragment.domains.payments import parse_required_payment_amount
|
||||
from pyfragment.domains.purchases.models import PremiumResult, StarsResult
|
||||
from pyfragment.domains.tonapi.account import get_account_info
|
||||
from pyfragment.domains.tonapi.transaction import process_transaction
|
||||
from pyfragment.enums import PaymentMethod
|
||||
from pyfragment.exceptions import (
|
||||
AlreadySubscribedError,
|
||||
@@ -27,6 +25,8 @@ from pyfragment.exceptions import (
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.services.tonapi.account import get_account_info
|
||||
from pyfragment.services.tonapi.transaction import process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
from pyfragment.domains.tonapi.service import TonapiService
|
||||
|
||||
__all__ = [
|
||||
"TonapiService",
|
||||
]
|
||||
@@ -41,6 +41,11 @@ WALLET_CLASSES: dict[WalletVersion, Any] = {
|
||||
}
|
||||
|
||||
|
||||
class ApiProvider(StrEnum):
|
||||
TONAPI = "tonapi" # tonconsole.com — default
|
||||
TONCENTER = "toncenter" # t.me/toncenter
|
||||
|
||||
|
||||
class SupportedBrowser(StrEnum):
|
||||
ARC = "arc"
|
||||
BRAVE = "brave"
|
||||
|
||||
@@ -13,7 +13,6 @@ from pyfragment.core.constants import (
|
||||
STARS_PURCHASE_MIN,
|
||||
STARS_WINNERS_MAX,
|
||||
STARS_WINNERS_MIN,
|
||||
TONAPI_KEY_MIN_LENGTH,
|
||||
)
|
||||
|
||||
|
||||
@@ -31,10 +30,7 @@ class ConfigurationError(ClientError):
|
||||
MISSING_VARS = "Missing required parameter(s): {keys}."
|
||||
UNSUPPORTED_VERSION = "Unsupported wallet version '{version}'. Supported values: {supported}."
|
||||
INVALID_MNEMONIC = f"Invalid mnemonic phrase: expected {', '.join(str(n) for n in sorted(MNEMONIC_WORD_COUNTS_VALID))} words, got {{count}}."
|
||||
INVALID_API_KEY = (
|
||||
f"Invalid Tonapi API key: expected at least {TONAPI_KEY_MIN_LENGTH} characters, got {{length}}. "
|
||||
"Get a key at https://tonconsole.com."
|
||||
)
|
||||
UNSUPPORTED_PROVIDER = "Unsupported API provider '{provider}'. Supported values: {supported}."
|
||||
INVALID_MONTHS = f"Invalid Premium duration: choose {', '.join(str(m) for m in sorted(PREMIUM_MONTHS_VALID))} months."
|
||||
INVALID_STARS_AMOUNT = (
|
||||
f"Invalid Stars amount: must be an integer between {STARS_PURCHASE_MIN:,} and {STARS_PURCHASE_MAX:,}."
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from pyfragment.services.tonapi.service import TonapiService
|
||||
|
||||
__all__ = [
|
||||
"TonapiService",
|
||||
]
|
||||
@@ -5,14 +5,14 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ton_core import NetworkGlobalID
|
||||
from tonutils.clients import TonapiClient
|
||||
from tonutils.clients import TonapiClient, ToncenterClient
|
||||
from tonutils.contracts.jetton import get_wallet_address_get_method, get_wallet_data_get_method
|
||||
from tonutils.exceptions import ProviderResponseError
|
||||
|
||||
from pyfragment.core.constants import MIN_GRAM_BALANCE, MIN_USDT_BALANCE, USDT_GRAM_MASTER_ADDRESS
|
||||
from pyfragment.domains.tonapi.models import WalletInfo
|
||||
from pyfragment.enums import WALLET_CLASSES
|
||||
from pyfragment.enums import WALLET_CLASSES, ApiProvider
|
||||
from pyfragment.exceptions import WalletError
|
||||
from pyfragment.services.tonapi.models import WalletInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
@@ -21,6 +21,13 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _make_ton_client(client: FragmentClient) -> Any:
|
||||
"""Return the appropriate tonutils client based on the configured api_provider."""
|
||||
if client.api_provider == ApiProvider.TONCENTER:
|
||||
return ToncenterClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key)
|
||||
return TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key)
|
||||
|
||||
|
||||
async def get_usdt_balance(ton: Any, wallet_address: str) -> float:
|
||||
"""Return the USDT balance for a Fragment-linked GRAM (ex TON) wallet."""
|
||||
try:
|
||||
@@ -92,7 +99,7 @@ async def check_usdt_payment_balance(
|
||||
|
||||
async def get_account_info(client: FragmentClient) -> dict[str, Any]:
|
||||
"""Build the wallet payload Fragment needs to prepare a transaction."""
|
||||
async with TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key) as ton:
|
||||
async with _make_ton_client(client) as ton:
|
||||
try:
|
||||
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
||||
wallet, pub_key, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
||||
@@ -110,7 +117,7 @@ async def get_account_info(client: FragmentClient) -> dict[str, Any]:
|
||||
|
||||
async def get_wallet_info(client: FragmentClient) -> WalletInfo:
|
||||
"""Fetch the wallet address, chain state, and GRAM (ex TON)/USDT balances."""
|
||||
async with TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key) as ton:
|
||||
async with _make_ton_client(client) as ton:
|
||||
try:
|
||||
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
||||
wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
||||
@@ -3,8 +3,8 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pyfragment.domains.base import BaseService
|
||||
from pyfragment.domains.tonapi.account import get_wallet_info
|
||||
from pyfragment.domains.tonapi.models import WalletInfo
|
||||
from pyfragment.services.tonapi.account import get_wallet_info
|
||||
from pyfragment.services.tonapi.models import WalletInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
+3
-4
@@ -7,13 +7,12 @@ import random
|
||||
import ssl
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ton_core import Cell, NetworkGlobalID
|
||||
from tonutils.clients import TonapiClient
|
||||
from ton_core import Cell
|
||||
from tonutils.exceptions import ProviderResponseError
|
||||
|
||||
from pyfragment.domains.tonapi.account import check_gram_payment_balance, check_usdt_payment_balance
|
||||
from pyfragment.enums import WALLET_CLASSES, PaymentMethod
|
||||
from pyfragment.exceptions import ParseError, TransactionError, WalletError
|
||||
from pyfragment.services.tonapi.account import _make_ton_client, check_gram_payment_balance, check_usdt_payment_balance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyfragment.client import FragmentClient
|
||||
@@ -141,7 +140,7 @@ async def process_transaction(
|
||||
message = _extract_message(transaction_data)
|
||||
amount_gram = int(message["amount"]) / 1_000_000_000
|
||||
|
||||
async with TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key) as ton:
|
||||
async with _make_ton_client(client) as ton:
|
||||
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
||||
wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
||||
|
||||
Reference in New Issue
Block a user