diff --git a/pyfragment/client.py b/pyfragment/client.py index 0597175..d0d5d26 100644 --- a/pyfragment/client.py +++ b/pyfragment/client.py @@ -3,7 +3,13 @@ from __future__ import annotations import json from typing import Any, cast -from pyfragment.core.constants import DEFAULT_TIMEOUT, FRAGMENT_BASE_URL, REQUIRED_COOKIE_KEYS +from pyfragment.core.constants import ( + DEFAULT_TIMEOUT, + FRAGMENT_BASE_URL, + MNEMONIC_WORD_COUNTS_VALID, + REQUIRED_COOKIE_KEYS, + TONAPI_KEY_MIN_LENGTH, +) from pyfragment.domains.ads.service import AdsService from pyfragment.domains.anonymous_numbers.service import AnonymousNumbersService from pyfragment.domains.base import raw_api_call @@ -67,10 +73,10 @@ class FragmentClient: raise ConfigurationError(ConfigurationError.MISSING_VARS.format(keys=", ".join(missing))) word_count = len(seed.split()) - if word_count not in (12, 18, 24): + if word_count not in MNEMONIC_WORD_COUNTS_VALID: raise ConfigurationError(ConfigurationError.INVALID_MNEMONIC.format(count=word_count)) - if len(api_key.strip()) < 68: + if len(api_key.strip()) < TONAPI_KEY_MIN_LENGTH: raise ConfigurationError(ConfigurationError.INVALID_API_KEY.format(length=len(api_key.strip()))) @staticmethod diff --git a/pyfragment/core/constants.py b/pyfragment/core/constants.py deleted file mode 100644 index 4fc2ba7..0000000 --- a/pyfragment/core/constants.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import json -from typing import Any - -from tonutils.contracts.wallet import WalletV4R2, WalletV5R1 - -WALLET_CLASSES: dict[str, Any] = {"V4R2": WalletV4R2, "V5R1": WalletV5R1} - -# Stars limits -STARS_PURCHASE_MIN: int = 50 -STARS_PURCHASE_MAX: int = 10_000 -STARS_GIVEAWAY_MIN: int = 500 -STARS_GIVEAWAY_MAX: int = 1_000_000 -STARS_WINNERS_MIN: int = 1 -STARS_WINNERS_MAX: int = 5 - -MIN_TON_BALANCE: float = 0.33 -USDT_TON_MASTER_ADDRESS: str = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" -MIN_USDT_BALANCE: float = 0.75 - -DEFAULT_TIMEOUT: float = 30.0 - -REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token") - -FRAGMENT_DOMAIN: str = "fragment.com" -FRAGMENT_BASE_URL: str = f"https://{FRAGMENT_DOMAIN}" -STARS_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/buy" -STARS_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/giveaway" -PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift" -PREMIUM_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/giveaway" -ADS_TOPUP_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup" -NUMBERS_PAGE: str = f"{FRAGMENT_BASE_URL}/numbers" -GIFTS_PAGE: str = f"{FRAGMENT_BASE_URL}/gifts" - - -DEVICE: str = json.dumps( - { - "platform": "iphone", - "appName": "Tonkeeper", - "appVersion": "26.04.0", - "maxProtocolVersion": 2, - "features": [ - "SendTransaction", - {"name": "SendTransaction", "maxMessages": 255}, - {"name": "SignData", "types": ["text", "binary", "cell"]}, - ], - } -) - -BASE_HEADERS: dict[str, str] = { - "accept": "application/json, text/javascript, */*; q=0.01", - "accept-language": "en-US,en;q=0.9,uk;q=0.8,ru;q=0.7", - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", - "origin": FRAGMENT_BASE_URL, - "priority": "u=1, i", - "sec-ch-ua": '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"', - "sec-ch-ua-mobile": "?1", - "sec-ch-ua-platform": '"Android"', - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "same-origin", - "user-agent": ( - "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36" - ), - "x-requested-with": "XMLHttpRequest", -} diff --git a/pyfragment/core/constants/__init__.py b/pyfragment/core/constants/__init__.py new file mode 100644 index 0000000..a49d4a3 --- /dev/null +++ b/pyfragment/core/constants/__init__.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from pyfragment.core.constants.headers import BASE_HEADERS, DEFAULT_TIMEOUT, REQUIRED_COOKIE_KEYS +from pyfragment.core.constants.limits import ( + MIN_TON_BALANCE, + MIN_USDT_BALANCE, + MNEMONIC_WORD_COUNTS_VALID, + PREMIUM_MONTHS_VALID, + PREMIUM_WINNERS_MAX, + PREMIUM_WINNERS_MIN, + STARS_GIVEAWAY_MAX, + STARS_GIVEAWAY_MIN, + STARS_PURCHASE_MAX, + STARS_PURCHASE_MIN, + STARS_WINNERS_MAX, + STARS_WINNERS_MIN, + TONAPI_KEY_MIN_LENGTH, +) +from pyfragment.core.constants.ton import DEVICE_INFO, USDT_TON_MASTER_ADDRESS +from pyfragment.core.constants.urls import ( + ADS_TOPUP_PAGE, + FRAGMENT_BASE_URL, + FRAGMENT_DOMAIN, + GIFTS_PAGE, + NUMBERS_PAGE, + PREMIUM_GIVEAWAY_PAGE, + PREMIUM_PAGE, + STARS_GIVEAWAY_PAGE, + STARS_PAGE, +) + +__all__ = [ + "ADS_TOPUP_PAGE", + "BASE_HEADERS", + "DEFAULT_TIMEOUT", + "DEVICE_INFO", + "FRAGMENT_BASE_URL", + "FRAGMENT_DOMAIN", + "GIFTS_PAGE", + "MIN_TON_BALANCE", + "MIN_USDT_BALANCE", + "MNEMONIC_WORD_COUNTS_VALID", + "NUMBERS_PAGE", + "PREMIUM_GIVEAWAY_PAGE", + "PREMIUM_MONTHS_VALID", + "PREMIUM_PAGE", + "PREMIUM_WINNERS_MAX", + "PREMIUM_WINNERS_MIN", + "REQUIRED_COOKIE_KEYS", + "STARS_GIVEAWAY_MAX", + "STARS_GIVEAWAY_MIN", + "STARS_GIVEAWAY_PAGE", + "STARS_PAGE", + "STARS_PURCHASE_MAX", + "STARS_PURCHASE_MIN", + "STARS_WINNERS_MAX", + "STARS_WINNERS_MIN", + "TONAPI_KEY_MIN_LENGTH", + "USDT_TON_MASTER_ADDRESS", +] diff --git a/pyfragment/core/constants/headers.py b/pyfragment/core/constants/headers.py new file mode 100644 index 0000000..f0462fd --- /dev/null +++ b/pyfragment/core/constants/headers.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from pyfragment.core.constants.urls import FRAGMENT_BASE_URL + +DEFAULT_TIMEOUT: float = 30.0 + +REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token") + +BASE_HEADERS: dict[str, str] = { + "accept": "application/json, text/javascript, */*; q=0.01", + "accept-language": "en-US,en;q=0.9,uk;q=0.8,ru;q=0.7", + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "origin": FRAGMENT_BASE_URL, + "priority": "u=1, i", + "sec-ch-ua": '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"', + "sec-ch-ua-mobile": "?1", + "sec-ch-ua-platform": '"Android"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "user-agent": ( + "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36" + ), + "x-requested-with": "XMLHttpRequest", +} diff --git a/pyfragment/core/constants/limits.py b/pyfragment/core/constants/limits.py new file mode 100644 index 0000000..1b71c3e --- /dev/null +++ b/pyfragment/core/constants/limits.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +# Stars: direct purchase +STARS_PURCHASE_MIN: int = 50 +STARS_PURCHASE_MAX: int = 10_000 + +# Stars: giveaway per winner +STARS_GIVEAWAY_MIN: int = 500 +STARS_GIVEAWAY_MAX: int = 1_000_000 + +# Stars giveaway winners count +STARS_WINNERS_MIN: int = 1 +STARS_WINNERS_MAX: int = 5 + +# Premium giveaway winners count +PREMIUM_WINNERS_MIN: int = 1 +PREMIUM_WINNERS_MAX: int = 24_000 + +# Premium subscription durations (months) +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_KEY_MIN_LENGTH: int = 68 + +# Wallet minimum balances +MIN_TON_BALANCE: float = 0.33 +MIN_USDT_BALANCE: float = 0.75 diff --git a/pyfragment/core/constants/ton.py b/pyfragment/core/constants/ton.py new file mode 100644 index 0000000..e76d645 --- /dev/null +++ b/pyfragment/core/constants/ton.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Any + +USDT_TON_MASTER_ADDRESS: str = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" + +DEVICE_INFO: dict[str, Any] = { + "platform": "iphone", + "appName": "Tonkeeper", + "appVersion": "26.04.0", + "maxProtocolVersion": 2, + "features": [ + "SendTransaction", + {"name": "SendTransaction", "maxMessages": 255}, + {"name": "SignData", "types": ["text", "binary", "cell"]}, + ], +} diff --git a/pyfragment/core/constants/urls.py b/pyfragment/core/constants/urls.py new file mode 100644 index 0000000..cba12b0 --- /dev/null +++ b/pyfragment/core/constants/urls.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +FRAGMENT_DOMAIN: str = "fragment.com" +FRAGMENT_BASE_URL: str = f"https://{FRAGMENT_DOMAIN}" + +STARS_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/buy" +STARS_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/giveaway" +PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift" +PREMIUM_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/giveaway" +ADS_TOPUP_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup" +NUMBERS_PAGE: str = f"{FRAGMENT_BASE_URL}/numbers" +GIFTS_PAGE: str = f"{FRAGMENT_BASE_URL}/gifts" diff --git a/pyfragment/domains/giveaways/giveaway.py b/pyfragment/domains/giveaways/giveaway.py index ffe8091..642133f 100644 --- a/pyfragment/domains/giveaways/giveaway.py +++ b/pyfragment/domains/giveaways/giveaway.py @@ -5,7 +5,18 @@ import logging import random from typing import TYPE_CHECKING -from pyfragment.core.constants import DEVICE, PREMIUM_GIVEAWAY_PAGE, STARS_GIVEAWAY_PAGE, STARS_GIVEAWAY_MIN, STARS_GIVEAWAY_MAX, STARS_WINNERS_MIN, STARS_WINNERS_MAX +from pyfragment.core.constants import ( + DEVICE, + PREMIUM_GIVEAWAY_PAGE, + PREMIUM_MONTHS_VALID, + PREMIUM_WINNERS_MAX, + PREMIUM_WINNERS_MIN, + STARS_GIVEAWAY_MAX, + STARS_GIVEAWAY_MIN, + STARS_GIVEAWAY_PAGE, + STARS_WINNERS_MAX, + STARS_WINNERS_MIN, +) 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 @@ -124,9 +135,9 @@ async def giveaway_premium( months: int = 3, payment_method: PaymentMethod = "ton", ) -> PremiumGiveawayResult: - if not isinstance(winners, int) or not (1 <= winners <= 24_000): + if not isinstance(winners, int) or not (PREMIUM_WINNERS_MIN <= winners <= PREMIUM_WINNERS_MAX): raise ConfigurationError(ConfigurationError.INVALID_WINNERS_PREMIUM) - if months not in (3, 6, 12): + if months not in PREMIUM_MONTHS_VALID: raise ConfigurationError(ConfigurationError.INVALID_MONTHS) if payment_method not in PaymentMethod._value2member_map_: raise ConfigurationError( diff --git a/pyfragment/domains/purchases/purchase.py b/pyfragment/domains/purchases/purchase.py index b8aaf63..7779269 100644 --- a/pyfragment/domains/purchases/purchase.py +++ b/pyfragment/domains/purchases/purchase.py @@ -5,7 +5,14 @@ import logging import time from typing import TYPE_CHECKING -from pyfragment.core.constants import DEVICE, PREMIUM_PAGE, STARS_PAGE, STARS_PURCHASE_MIN, STARS_PURCHASE_MAX +from pyfragment.core.constants import ( + DEVICE, + PREMIUM_MONTHS_VALID, + PREMIUM_PAGE, + STARS_PAGE, + STARS_PURCHASE_MAX, + STARS_PURCHASE_MIN, +) 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 @@ -115,7 +122,7 @@ async def purchase_premium( show_sender: bool = True, payment_method: PaymentMethod = "ton", ) -> PremiumResult: - if months not in (3, 6, 12): + if months not in PREMIUM_MONTHS_VALID: raise ConfigurationError(ConfigurationError.INVALID_MONTHS) if payment_method not in PaymentMethod._value2member_map_: raise ConfigurationError( diff --git a/pyfragment/domains/tonapi/account.py b/pyfragment/domains/tonapi/account.py index c532d35..52aab22 100644 --- a/pyfragment/domains/tonapi/account.py +++ b/pyfragment/domains/tonapi/account.py @@ -9,8 +9,9 @@ from tonutils.clients import TonapiClient 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_TON_BALANCE, MIN_USDT_BALANCE, USDT_TON_MASTER_ADDRESS, WALLET_CLASSES +from pyfragment.core.constants import MIN_TON_BALANCE, MIN_USDT_BALANCE, USDT_TON_MASTER_ADDRESS from pyfragment.exceptions import WalletError +from pyfragment.models.enums import WALLET_CLASSES from pyfragment.models.wallet import WalletInfo if TYPE_CHECKING: diff --git a/pyfragment/domains/tonapi/transaction.py b/pyfragment/domains/tonapi/transaction.py index 7cb5263..1740ca2 100644 --- a/pyfragment/domains/tonapi/transaction.py +++ b/pyfragment/domains/tonapi/transaction.py @@ -11,10 +11,9 @@ from ton_core import Cell, NetworkGlobalID from tonutils.clients import TonapiClient from tonutils.exceptions import ProviderResponseError -from pyfragment.core.constants import WALLET_CLASSES from pyfragment.domains.tonapi.account import check_ton_payment_balance, check_usdt_payment_balance from pyfragment.exceptions import ParseError, TransactionError, WalletError -from pyfragment.models.enums import PaymentMethod +from pyfragment.models.enums import WALLET_CLASSES, PaymentMethod if TYPE_CHECKING: from pyfragment.client import FragmentClient diff --git a/pyfragment/exceptions.py b/pyfragment/exceptions.py index 21c7ca3..75f4892 100644 --- a/pyfragment/exceptions.py +++ b/pyfragment/exceptions.py @@ -1,5 +1,19 @@ from __future__ import annotations +from pyfragment.core.constants.limits import ( + MNEMONIC_WORD_COUNTS_VALID, + PREMIUM_MONTHS_VALID, + PREMIUM_WINNERS_MAX, + PREMIUM_WINNERS_MIN, + STARS_GIVEAWAY_MAX, + STARS_GIVEAWAY_MIN, + STARS_PURCHASE_MAX, + STARS_PURCHASE_MIN, + STARS_WINNERS_MAX, + STARS_WINNERS_MIN, + TONAPI_KEY_MIN_LENGTH, +) + class FragmentError(Exception): """Base exception for all pyfragment errors.""" @@ -14,20 +28,29 @@ class ConfigurationError(ClientError): MISSING_VARS = "Missing required parameter(s): {keys}." UNSUPPORTED_VERSION = "Unsupported wallet version '{version}'. Supported values: {supported}." - INVALID_MNEMONIC = "Invalid mnemonic phrase: expected 12, 18, or 24 words, got {count}." + INVALID_MNEMONIC = f"Invalid mnemonic phrase: expected {', '.join(str(n) for n in sorted(MNEMONIC_WORD_COUNTS_VALID))} words, got {{count}}." INVALID_API_KEY = ( - "Invalid Tonapi API key: expected at least 68 characters, got {length}. Get a key at https://tonconsole.com." + f"Invalid Tonapi API key: expected at least {TONAPI_KEY_MIN_LENGTH} characters, got {{length}}. " + "Get a key at https://tonconsole.com." + ) + 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:,}." ) - INVALID_MONTHS = "Invalid Premium duration: choose 3, 6, or 12 months." - INVALID_STARS_AMOUNT = "Invalid Stars amount: must be an integer between 50 and 10,000." INVALID_TON_AMOUNT = "Invalid TON amount: must be an integer between 1 and 1,000,000,000." INVALID_USERNAME = ( "Invalid username '{username}'. " "Must be 5-32 characters and contain only letters (A-Z, a-z), digits (0-9), or underscores (_)." ) - INVALID_WINNERS_STARS = "Invalid winners count: must be an integer between 1 and 5." - INVALID_WINNERS_PREMIUM = "Invalid winners count: must be an integer between 1 and 24,000." - INVALID_STARS_PER_WINNER = "Invalid Stars per winner: must be an integer between 500 and 1,000,000 (max total: 1,000,000)." + INVALID_WINNERS_STARS = ( + f"Invalid winners count: must be an integer between {STARS_WINNERS_MIN:,} and {STARS_WINNERS_MAX:,}." + ) + INVALID_WINNERS_PREMIUM = ( + f"Invalid winners count: must be an integer between {PREMIUM_WINNERS_MIN:,} and {PREMIUM_WINNERS_MAX:,}." + ) + INVALID_STARS_PER_WINNER = ( + f"Invalid Stars per winner: must be an integer between {STARS_GIVEAWAY_MIN:,} and {STARS_GIVEAWAY_MAX:,}." + ) INVALID_PAYMENT_METHOD = "Invalid payment method '{method}'. Supported values: {supported}." diff --git a/pyfragment/models/enums.py b/pyfragment/models/enums.py index a7f1c52..6322b22 100644 --- a/pyfragment/models/enums.py +++ b/pyfragment/models/enums.py @@ -1,6 +1,9 @@ from __future__ import annotations from enum import StrEnum +from typing import Any + +from tonutils.contracts.wallet import WalletV4R2, WalletV5R1 class PaymentMethod(StrEnum): @@ -13,6 +16,12 @@ class WalletVersion(StrEnum): V5R1 = "V5R1" +WALLET_CLASSES: dict[WalletVersion, Any] = { + WalletVersion.V4R2: WalletV4R2, + WalletVersion.V5R1: WalletV5R1, +} + + class SupportedBrowser(StrEnum): ARC = "arc" BRAVE = "brave" @@ -29,4 +38,4 @@ class SupportedBrowser(StrEnum): VIVALDI = "vivaldi" -__all__ = ["PaymentMethod", "SupportedBrowser", "WalletVersion"] +__all__ = ["PaymentMethod", "SupportedBrowser", "WALLET_CLASSES", "WalletVersion"] diff --git a/tests/002_test_client.py b/tests/002_test_client.py index c8692ec..c813236 100644 --- a/tests/002_test_client.py +++ b/tests/002_test_client.py @@ -5,6 +5,7 @@ import json import pytest from pyfragment import ConfigurationError, CookieError, FragmentClient +from pyfragment.core.constants.limits import MNEMONIC_WORD_COUNTS_VALID, TONAPI_KEY_MIN_LENGTH from tests.shared import VALID_API_KEY, VALID_COOKIES, VALID_SEED # Client init tests @@ -55,7 +56,7 @@ def test_invalid_mnemonic_length_raises() -> None: def test_valid_mnemonic_lengths() -> None: - for length in (12, 18, 24): + for length in sorted(MNEMONIC_WORD_COUNTS_VALID): seed = " ".join(["abandon"] * (length - 1) + ["about"]) client = FragmentClient(seed=seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES) assert len(client.seed.split()) == length @@ -71,7 +72,7 @@ def test_missing_api_key_raises() -> None: def test_short_api_key_raises() -> None: with pytest.raises(ConfigurationError): - FragmentClient(seed=VALID_SEED, api_key="A" * 42, cookies=VALID_COOKIES) + FragmentClient(seed=VALID_SEED, api_key="A" * (TONAPI_KEY_MIN_LENGTH - 1), cookies=VALID_COOKIES) # Cookie validation tests