feat: reorganize constants into separate modules; update validation logic for mnemonic and API key lengths

This commit is contained in:
bohd4nx
2026-05-29 22:22:09 +03:00
parent 66bcd22198
commit 5124af17ef
14 changed files with 223 additions and 89 deletions
+9 -3
View File
@@ -3,7 +3,13 @@ from __future__ import annotations
import json import json
from typing import Any, cast 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.ads.service import AdsService
from pyfragment.domains.anonymous_numbers.service import AnonymousNumbersService from pyfragment.domains.anonymous_numbers.service import AnonymousNumbersService
from pyfragment.domains.base import raw_api_call from pyfragment.domains.base import raw_api_call
@@ -67,10 +73,10 @@ class FragmentClient:
raise ConfigurationError(ConfigurationError.MISSING_VARS.format(keys=", ".join(missing))) raise ConfigurationError(ConfigurationError.MISSING_VARS.format(keys=", ".join(missing)))
word_count = len(seed.split()) 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)) 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()))) raise ConfigurationError(ConfigurationError.INVALID_API_KEY.format(length=len(api_key.strip())))
@staticmethod @staticmethod
-68
View File
@@ -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",
}
+60
View File
@@ -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",
]
+26
View File
@@ -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",
}
+30
View File
@@ -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
+17
View File
@@ -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"]},
],
}
+12
View File
@@ -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"
+14 -3
View File
@@ -5,7 +5,18 @@ import logging
import random import random
from typing import TYPE_CHECKING 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.payments import parse_required_payment_amount
from pyfragment.domains.tonapi.account import get_account_info from pyfragment.domains.tonapi.account import get_account_info
from pyfragment.domains.tonapi.transaction import process_transaction from pyfragment.domains.tonapi.transaction import process_transaction
@@ -124,9 +135,9 @@ async def giveaway_premium(
months: int = 3, months: int = 3,
payment_method: PaymentMethod = "ton", payment_method: PaymentMethod = "ton",
) -> PremiumGiveawayResult: ) -> 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) 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) raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
if payment_method not in PaymentMethod._value2member_map_: if payment_method not in PaymentMethod._value2member_map_:
raise ConfigurationError( raise ConfigurationError(
+9 -2
View File
@@ -5,7 +5,14 @@ import logging
import time import time
from typing import TYPE_CHECKING 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.payments import parse_required_payment_amount
from pyfragment.domains.tonapi.account import get_account_info from pyfragment.domains.tonapi.account import get_account_info
from pyfragment.domains.tonapi.transaction import process_transaction from pyfragment.domains.tonapi.transaction import process_transaction
@@ -115,7 +122,7 @@ async def purchase_premium(
show_sender: bool = True, show_sender: bool = True,
payment_method: PaymentMethod = "ton", payment_method: PaymentMethod = "ton",
) -> PremiumResult: ) -> PremiumResult:
if months not in (3, 6, 12): if months not in PREMIUM_MONTHS_VALID:
raise ConfigurationError(ConfigurationError.INVALID_MONTHS) raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
if payment_method not in PaymentMethod._value2member_map_: if payment_method not in PaymentMethod._value2member_map_:
raise ConfigurationError( raise ConfigurationError(
+2 -1
View File
@@ -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.contracts.jetton import get_wallet_address_get_method, get_wallet_data_get_method
from tonutils.exceptions import ProviderResponseError 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.exceptions import WalletError
from pyfragment.models.enums import WALLET_CLASSES
from pyfragment.models.wallet import WalletInfo from pyfragment.models.wallet import WalletInfo
if TYPE_CHECKING: if TYPE_CHECKING:
+1 -2
View File
@@ -11,10 +11,9 @@ from ton_core import Cell, NetworkGlobalID
from tonutils.clients import TonapiClient from tonutils.clients import TonapiClient
from tonutils.exceptions import ProviderResponseError 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.domains.tonapi.account import check_ton_payment_balance, check_usdt_payment_balance
from pyfragment.exceptions import ParseError, TransactionError, WalletError from pyfragment.exceptions import ParseError, TransactionError, WalletError
from pyfragment.models.enums import PaymentMethod from pyfragment.models.enums import WALLET_CLASSES, PaymentMethod
if TYPE_CHECKING: if TYPE_CHECKING:
from pyfragment.client import FragmentClient from pyfragment.client import FragmentClient
+30 -7
View File
@@ -1,5 +1,19 @@
from __future__ import annotations 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): class FragmentError(Exception):
"""Base exception for all pyfragment errors.""" """Base exception for all pyfragment errors."""
@@ -14,20 +28,29 @@ class ConfigurationError(ClientError):
MISSING_VARS = "Missing required parameter(s): {keys}." MISSING_VARS = "Missing required parameter(s): {keys}."
UNSUPPORTED_VERSION = "Unsupported wallet version '{version}'. Supported values: {supported}." 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_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_TON_AMOUNT = "Invalid TON amount: must be an integer between 1 and 1,000,000,000."
INVALID_USERNAME = ( INVALID_USERNAME = (
"Invalid username '{username}'. " "Invalid username '{username}'. "
"Must be 5-32 characters and contain only letters (A-Z, a-z), digits (0-9), or underscores (_)." "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_STARS = (
INVALID_WINNERS_PREMIUM = "Invalid winners count: must be an integer between 1 and 24,000." f"Invalid winners count: must be an integer between {STARS_WINNERS_MIN:,} and {STARS_WINNERS_MAX:,}."
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_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}." INVALID_PAYMENT_METHOD = "Invalid payment method '{method}'. Supported values: {supported}."
+10 -1
View File
@@ -1,6 +1,9 @@
from __future__ import annotations from __future__ import annotations
from enum import StrEnum from enum import StrEnum
from typing import Any
from tonutils.contracts.wallet import WalletV4R2, WalletV5R1
class PaymentMethod(StrEnum): class PaymentMethod(StrEnum):
@@ -13,6 +16,12 @@ class WalletVersion(StrEnum):
V5R1 = "V5R1" V5R1 = "V5R1"
WALLET_CLASSES: dict[WalletVersion, Any] = {
WalletVersion.V4R2: WalletV4R2,
WalletVersion.V5R1: WalletV5R1,
}
class SupportedBrowser(StrEnum): class SupportedBrowser(StrEnum):
ARC = "arc" ARC = "arc"
BRAVE = "brave" BRAVE = "brave"
@@ -29,4 +38,4 @@ class SupportedBrowser(StrEnum):
VIVALDI = "vivaldi" VIVALDI = "vivaldi"
__all__ = ["PaymentMethod", "SupportedBrowser", "WalletVersion"] __all__ = ["PaymentMethod", "SupportedBrowser", "WALLET_CLASSES", "WalletVersion"]
+3 -2
View File
@@ -5,6 +5,7 @@ import json
import pytest import pytest
from pyfragment import ConfigurationError, CookieError, FragmentClient 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 from tests.shared import VALID_API_KEY, VALID_COOKIES, VALID_SEED
# Client init tests # Client init tests
@@ -55,7 +56,7 @@ def test_invalid_mnemonic_length_raises() -> None:
def test_valid_mnemonic_lengths() -> 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"]) seed = " ".join(["abandon"] * (length - 1) + ["about"])
client = FragmentClient(seed=seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES) client = FragmentClient(seed=seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
assert len(client.seed.split()) == length assert len(client.seed.split()) == length
@@ -71,7 +72,7 @@ def test_missing_api_key_raises() -> None:
def test_short_api_key_raises() -> None: def test_short_api_key_raises() -> None:
with pytest.raises(ConfigurationError): 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 # Cookie validation tests