mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
refactor: restructure as installable PyPI package
- Rename app/ → fragmentapi/ for proper package naming - Add FragmentClient class with gift_premium, gift_stars, topup_ton methods - Restructure core/ → types/ (exceptions, results, constants) - Merge utils/hash.py into utils/client.py - Replace _version.py with importlib.metadata - Add input validation with min/max bounds - Add unit tests: decode, client init/cookies - Rename 002_test_hash → 003_test_hash - Clean up pyproject.toml: pin deps, production classifiers
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
from fragmentapi.types.constants import (
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
PREMIUM_PAGE,
|
||||
REQUIRED_COOKIE_KEYS,
|
||||
STARS_PAGE,
|
||||
SUPPORTED_WALLET_VERSIONS,
|
||||
TON_PAGE,
|
||||
WALLET_CLASSES,
|
||||
WalletVersion,
|
||||
)
|
||||
from fragmentapi.types.exceptions import (
|
||||
ClientError,
|
||||
ConfigError,
|
||||
CookiesError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
HashFetchError,
|
||||
OperationError,
|
||||
RequestError,
|
||||
TransactionError,
|
||||
UnexpectedError,
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
WalletError,
|
||||
)
|
||||
from fragmentapi.types.results import AdsTopupResult, PremiumResult, StarsResult
|
||||
|
||||
__all__ = [
|
||||
# constants
|
||||
"BASE_HEADERS",
|
||||
"DEVICE",
|
||||
"PREMIUM_PAGE",
|
||||
"REQUIRED_COOKIE_KEYS",
|
||||
"STARS_PAGE",
|
||||
"SUPPORTED_WALLET_VERSIONS",
|
||||
"TON_PAGE",
|
||||
"WALLET_CLASSES",
|
||||
"WalletVersion",
|
||||
# client exceptions
|
||||
"ClientError",
|
||||
"ConfigError",
|
||||
"CookiesError",
|
||||
# fragment exceptions
|
||||
"FragmentAPIError",
|
||||
"FragmentError",
|
||||
"HashFetchError",
|
||||
"OperationError",
|
||||
"RequestError",
|
||||
"TransactionError",
|
||||
"UnexpectedError",
|
||||
"UserNotFoundError",
|
||||
"VerificationError",
|
||||
"WalletError",
|
||||
# result types
|
||||
"AdsTopupResult",
|
||||
"PremiumResult",
|
||||
"StarsResult",
|
||||
]
|
||||
@@ -0,0 +1,52 @@
|
||||
import json
|
||||
from typing import Literal, get_args
|
||||
|
||||
from tonutils.contracts.wallet import WalletV4R2, WalletV5R1
|
||||
|
||||
# Single source of truth for supported wallet versions
|
||||
WalletVersion = Literal["V4R2", "V5R1"]
|
||||
SUPPORTED_WALLET_VERSIONS: frozenset[str] = frozenset(get_args(WalletVersion))
|
||||
|
||||
# Wallet class map — used to resolve the correct contract from WALLET_VERSION
|
||||
WALLET_CLASSES: dict[str, type] = {"V4R2": WalletV4R2, "V5R1": WalletV5R1}
|
||||
|
||||
# Required Fragment session cookie keys
|
||||
REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token")
|
||||
|
||||
# Fragment page URLs
|
||||
STARS_PAGE: str = "https://fragment.com/stars/buy"
|
||||
PREMIUM_PAGE: str = "https://fragment.com/premium/gift"
|
||||
TON_PAGE: str = "https://fragment.com/ads/topup"
|
||||
|
||||
# Tonkeeper device fingerprint — serialized once, reused in every tx_data payload.
|
||||
DEVICE: str = json.dumps(
|
||||
{
|
||||
"platform": "iphone",
|
||||
"appName": "Tonkeeper",
|
||||
"appVersion": "5.5.2",
|
||||
"maxProtocolVersion": 2,
|
||||
"features": [
|
||||
"SendTransaction",
|
||||
{"name": "SendTransaction", "maxMessages": 255},
|
||||
{"name": "SignData", "types": ["text", "binary", "cell"]},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# Base HTTP headers — shared across all Fragment API requests.
|
||||
# Each method merges these with its own "referer" and "x-aj-referer".
|
||||
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": "https://fragment.com",
|
||||
"priority": "u=1, i",
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
"user-agent": (
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) "
|
||||
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1"
|
||||
),
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
class FragmentError(Exception):
|
||||
"""Base exception for all fragmentapi library errors."""
|
||||
|
||||
|
||||
class ClientError(FragmentError):
|
||||
"""Raised for client configuration and setup issues (bad params, invalid cookies)."""
|
||||
|
||||
|
||||
class ConfigError(ClientError):
|
||||
"""Raised when required client parameters are missing or invalid."""
|
||||
|
||||
MISSING_VARS = "Missing required parameter(s): {keys}."
|
||||
UNSUPPORTED_VERSION = "Unsupported wallet_version '{version}'. Must be one of: {supported}."
|
||||
INVALID_MONTHS = "Invalid duration. Choose 3, 6, or 12 months."
|
||||
INVALID_STARS_AMOUNT = "Amount must be an integer between 50 and 1 000 000 stars."
|
||||
INVALID_TON_AMOUNT = "Amount must be an integer between 1 and 1 000 000 000 TON."
|
||||
|
||||
|
||||
class CookiesError(ClientError):
|
||||
"""Raised when cookies are unreadable or missing required fields."""
|
||||
|
||||
READ_FAILED = "Failed to parse cookies: {exc}"
|
||||
MISSING_KEYS = (
|
||||
"Cookies are missing or have empty values for: {keys}. " "Open Fragment.com in your browser and copy fresh cookies."
|
||||
)
|
||||
|
||||
|
||||
class FragmentAPIError(FragmentError):
|
||||
"""Raised for errors returned by Fragment's API responses."""
|
||||
|
||||
NO_REQUEST_ID = (
|
||||
"Fragment did not return a request ID for '{context}'. " "The session may have expired — refresh your cookies."
|
||||
)
|
||||
|
||||
|
||||
class HashFetchError(FragmentAPIError):
|
||||
"""Raised when the Fragment API hash cannot be fetched from the page."""
|
||||
|
||||
BAD_STATUS = "Fragment returned HTTP {status} for {url}. " "Check that your cookies are valid and not expired."
|
||||
NOT_FOUND = (
|
||||
"Fragment hash not found in the page source of {url}. " "The page structure may have changed or you are not logged in."
|
||||
)
|
||||
|
||||
|
||||
class UserNotFoundError(FragmentAPIError):
|
||||
"""Raised when the target Telegram user is not found on Fragment."""
|
||||
|
||||
NOT_FOUND = (
|
||||
"Telegram user '{username}' was not found on Fragment. " "Make sure the username is correct and the account exists."
|
||||
)
|
||||
|
||||
|
||||
class TransactionError(FragmentAPIError):
|
||||
"""Raised when a TON transaction fails to build or broadcast."""
|
||||
|
||||
INVALID_PAYLOAD = (
|
||||
"Fragment returned an invalid transaction payload. " "The API response is missing expected 'transaction.messages' data."
|
||||
)
|
||||
BROADCAST_FAILED = "Transaction broadcast failed: {exc}"
|
||||
|
||||
|
||||
class RequestError(FragmentAPIError):
|
||||
"""Raised when a Fragment API response cannot be parsed."""
|
||||
|
||||
UNPARSEABLE = "Fragment API returned an unparseable response for '{context}': {exc}"
|
||||
|
||||
|
||||
class VerificationError(FragmentAPIError):
|
||||
"""Raised when Fragment requires KYC verification before proceeding."""
|
||||
|
||||
KYC_REQUIRED = "Fragment requires identity (KYC) verification. " "Complete it at https://fragment.com/my/profile and retry."
|
||||
|
||||
|
||||
class OperationError(FragmentError):
|
||||
"""Raised for runtime operation failures unrelated to Fragment's API."""
|
||||
|
||||
|
||||
class WalletError(OperationError):
|
||||
"""Raised for TON wallet issues (connection, balance, account info)."""
|
||||
|
||||
LOW_BALANCE = "TON wallet balance is too low: {balance:.2f} TON. Minimum required is 0.056 TON."
|
||||
BALANCE_CHECK_FAILED = "Wallet balance check failed: {exc}"
|
||||
ACCOUNT_INFO_FAILED = "Failed to retrieve wallet account info: {exc}"
|
||||
|
||||
|
||||
class UnexpectedError(OperationError):
|
||||
"""Raised when an unexpected error occurs during an API call."""
|
||||
|
||||
UNEXPECTED = "An unexpected error occurred: {exc}"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"FragmentError",
|
||||
"ClientError",
|
||||
"ConfigError",
|
||||
"CookiesError",
|
||||
"FragmentAPIError",
|
||||
"HashFetchError",
|
||||
"UserNotFoundError",
|
||||
"TransactionError",
|
||||
"RequestError",
|
||||
"VerificationError",
|
||||
"OperationError",
|
||||
"WalletError",
|
||||
"UnexpectedError",
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
__all__ = ["AdsTopupResult", "PremiumResult", "StarsResult"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PremiumResult:
|
||||
"""Result of a successful Telegram Premium gift."""
|
||||
|
||||
transaction_id: str
|
||||
username: str
|
||||
months: int
|
||||
timestamp: int = field(default_factory=lambda: int(time.time()))
|
||||
|
||||
|
||||
@dataclass
|
||||
class StarsResult:
|
||||
"""Result of a successful Telegram Stars purchase."""
|
||||
|
||||
transaction_id: str
|
||||
username: str
|
||||
stars: int
|
||||
timestamp: int = field(default_factory=lambda: int(time.time()))
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdsTopupResult:
|
||||
"""Result of a successful Telegram Ads balance top-up."""
|
||||
|
||||
transaction_id: str
|
||||
username: str
|
||||
amount: int
|
||||
timestamp: int = field(default_factory=lambda: int(time.time()))
|
||||
Reference in New Issue
Block a user