From ef831d12a06e5158028c66e19ab0f27e7d6803e0 Mon Sep 17 00:00:00 2001 From: bohd4nx Date: Sat, 27 Jun 2026 02:25:09 +0300 Subject: [PATCH 1/2] chore: update Python version to 3.11 in CI and project files; remove outdated CONTRIBUTING and SECURITY documents --- .github/workflows/ci.yml | 4 +- CONTRIBUTING.md | 58 ------------- README.md | 114 ++----------------------- SECURITY.md | 19 ----- pyfragment/core/transport.py | 16 +--- pyfragment/enums.py | 10 +-- pyfragment/exceptions.py | 4 - pyfragment/services/cookies/service.py | 8 +- pyproject.toml | 32 ++----- tests/shared.py | 4 - 10 files changed, 20 insertions(+), 249 deletions(-) delete mode 100644 CONTRIBUTING.md delete mode 100644 SECURITY.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d23d6d9..4d79616 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: "3.11" cache: pip - run: pip install ".[dev]" @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 1db8218..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,58 +0,0 @@ -# Contributing to pyfragment - -## Development setup - -```bash -git clone https://github.com/bohd4nx/pyfragment.git -cd pyfragment -pip install -e ".[dev]" -``` - -## Running checks - -```bash -# Lint and format -ruff check . --fix && ruff format . - -# Type check -mypy . --explicit-package-bases - -# Tests -pytest -``` - -All three must pass before opening a PR. - -## Project structure - -``` -pyfragment/ - client.py — FragmentClient (public entry point) - enums.py — ApiProvider, PaymentMethod, WalletVersion - exceptions.py — exception hierarchy - core/ — constants, validation helpers - domains/ — one package per feature domain - ads/ — recharge_ads, topup_gram - anonymous_numbers/— get_login_code, toggle_login_codes, terminate_sessions - giveaways/ — giveaway_stars, giveaway_premium - marketplace/ — search_usernames, search_numbers, search_gifts - purchases/ — purchase_stars, purchase_premium - services/ — shared infrastructure services - cookies/ — browser cookie extraction (models + service) - tonapi/ — wallet info, transaction signing (tonapi/toncenter) -tests/ — unit tests (pytest) -examples/ — runnable usage examples (excluded from CI) -``` - -## Conventions - -- All public async methods live on `FragmentClient` and delegate to a domain service. -- Domain functions receive a `FragmentClient` instance, never raw httpx clients. -- Patch targets in tests use the module where the name is **defined**, e.g. `pyfragment.services.tonapi.transaction._make_ton_client`. -- Versioning follows [CalVer](https://calver.org/): `YYYY.MINOR.MICRO`. Bump in `pyproject.toml`; tag as `vYYYY.MINOR.MICRO`. - -## Pull requests - -- Keep PRs focused — one feature or fix per PR. -- Update `CHANGELOG.md` under `[Unreleased]`. -- Add or update tests for any changed behaviour. diff --git a/README.md b/README.md index d4dbf8a..d20c2fe 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,14 @@ [![Tests](https://img.shields.io/github/actions/workflow/status/bohd4nx/pyfragment/ci.yml?style=flat&label=Tests&logo=github)](https://github.com/bohd4nx/pyfragment/actions) [![License](https://img.shields.io/github/license/bohd4nx/pyfragment?style=flat&color=blue&label=License)](LICENSE) -Async Python client for the **[Fragment](https://fragment.com)** marketplace API. -Buy Stars & Premium, run giveaways, top up GRAM (ex TON) and Ads balances, -manage anonymous numbers, and search Fragment listings. +Async Python client for the **[Fragment.com](https://fragment.com)** marketplace API. -**[Documentation](https://bohd4nx.gitbook.io/pyfragment/)** · **[Examples](https://github.com/bohd4nx/pyfragment/tree/master/examples)** · **[Changelog](CHANGELOG.md)** · **[Donate GRAM](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5)** +**[Documentation](https://bohd4nx.gitbook.io/pyfragment/)** · **[Examples](https://github.com/bohd4nx/pyfragment/tree/master/examples)** > **Disclaimer:** This project is not affiliated with [Fragment](https://fragment.com) or [Telegram](https://telegram.org). ---- - ## Installation ```bash @@ -46,120 +42,24 @@ from pyfragment.enums import PaymentMethod async def main() -> None: async with FragmentClient( seed="word1 word2 ... word24", - api_key="YOUR_API_KEY", # tonconsole.com (tonapi, default) or t.me/toncenter - cookies={ - "stel_ssid": "...", - "stel_dt": "...", - "stel_token": "...", - "stel_ton_token": "...", - }, - wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1" - api_provider="tonapi", # or "toncenter" + api_key="YOUR_API_KEY", + cookies={"stel_ssid": "...", "stel_dt": "...", "stel_token": "...", "stel_ton_token": "..."}, ) as client: wallet = await client.get_wallet() print(f"GRAM: {wallet.gram_balance} | USDT: {wallet.usdt_balance}") - recipient = "https://t.me/username" # also: @username, username - - stars = await client.purchase_stars(recipient, amount=500, payment_method=PaymentMethod.USDT_GRAM) + stars = await client.purchase_stars("@username", amount=500, payment_method=PaymentMethod.USDT_GRAM) print(f"Sent {stars.amount} Stars to {stars.username} | tx: {stars.transaction_id}") - premium = await client.purchase_premium(recipient, months=6, payment_method=PaymentMethod.GRAM) + premium = await client.purchase_premium("@username", months=6, payment_method=PaymentMethod.GRAM) print(f"Sent Premium {premium.amount}m to {premium.username} | tx: {premium.transaction_id}") asyncio.run(main()) ``` ---- - -## Configuration - -| Parameter | Type | Default | Description | -| ---------------- | ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------- | -| `seed` | `str` | — | 12- or 24-word GRAM (ex TON) wallet mnemonic | -| `api_key` | `str` | — | API key for the chosen provider (see `api_provider`) | -| `cookies` | `dict \| str` | — | Fragment session cookies | -| `wallet_version` | `str` | `"V5R1"` | `"V4R2"` or `"V5R1"` — also accepts `WalletVersion` literal | -| `api_provider` | `str` | `"tonapi"` | `"tonapi"` ([tonconsole.com](https://tonconsole.com)) or `"toncenter"` ([t.me/toncenter](https://t.me/toncenter)) | -| `timeout` | `float` | `30.0` | HTTP request timeout in seconds | - ---- - -## Credentials - -### Fragment cookies - -Log in to [fragment.com](https://fragment.com) and connect your GRAM (ex TON) wallet. - -**Automatically** (recommended) — reads directly from your browser, no extension needed: - -```bash -pip install "pyfragment[browser]" -``` - -```python -from pyfragment import get_cookies_from_browser - -result = get_cookies_from_browser("chrome") # firefox, edge, brave, ... -# result.cookies — dict[str, str] ready to pass to FragmentClient -# result.expires — ISO 8601 expiry of stel_ssid, or None for session cookies -``` - -**Manually** — use [Cookie Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) and export: `stel_ssid`, `stel_dt`, `stel_token`, `stel_ton_token`. Pass as a `dict` or JSON string. - -Refresh cookies when you get authentication errors. - -### Tonapi key - -Generate at [tonconsole.com](https://tonconsole.com). - -### Seed phrase - -12- or 24-word mnemonic from your GRAM (ex TON) wallet (**Tonkeeper → Settings → Backup**). Never share it. - ---- - -## Logging - -`pyfragment` uses the standard `logging` module under the `pyfragment` namespace and is silent by default: - -```python -import logging - -logging.basicConfig(level=logging.INFO) -logging.getLogger("pyfragment").setLevel(logging.DEBUG) # for detailed request logs -``` - ---- - -## Error Handling - -All exceptions inherit from `FragmentError`: - -```python -from pyfragment import ( - ConfigurationError, # invalid arguments (amount, months, payment_method…) - UserNotFoundError, # recipient not found on Fragment - WalletError, # insufficient GRAM (ex TON) or USDT balance - TransactionError, # broadcast failed, duplicate seqno, invalid payload - FragmentAPIError, # Fragment API returned an error response - FragmentPageError, # page fetch or hash extraction failed - AnonymousNumberError, # number not owned, wrong state, login code issues - CookieError, # missing or malformed session cookies - ParseError, # failed to decode Fragment payload - VerificationError, # on-chain verification step failed - OperationError, # generic operation-level failure - UnexpectedError, # unexpected API response structure -) -``` - ---- -
-Made with ❤️ by [@bohd4nx](https://t.me/bohd4nx) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) - -**Star ⭐ if you found it useful** +Made with ❤️ by [@bohd4nx](https://t.me/bohd4nx) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)
diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 1800853..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,19 +0,0 @@ -# Security Policy - -## Reporting a vulnerability - -Please **do not** open a public GitHub issue for security vulnerabilities. - -Report them privately via GitHub's [Security Advisory](https://github.com/bohd4nx/pyfragment/security/advisories/new) feature, or contact the maintainer directly at [@bohd4nx](https://t.me/bohd4nx) on Telegram. - -Include: - -- A description of the vulnerability and its potential impact. -- Steps to reproduce or a proof-of-concept. -- Affected versions. - -You will receive a response within 72 hours. Once the fix is released, the advisory will be published. - -## Scope - -This library handles sensitive credentials (GRAM (ex TON) seed phrases, Fragment session cookies, Tonapi keys). Please treat any finding that could expose or misuse these credentials as high severity. diff --git a/pyfragment/core/transport.py b/pyfragment/core/transport.py index 9ebeb32..58cfac7 100644 --- a/pyfragment/core/transport.py +++ b/pyfragment/core/transport.py @@ -8,7 +8,7 @@ from typing import Any, cast import httpx from pyfragment.core.constants import DEFAULT_TIMEOUT, FRAGMENT_BASE_URL -from pyfragment.exceptions import FragmentPageError, ParseError, VerificationError +from pyfragment.exceptions import FragmentPageError, ParseError async def get_fragment_hash( @@ -73,17 +73,3 @@ async def fragment_request( ) return parse_json_response(resp, data.get("method", "request")) raise FragmentPageError(FragmentPageError.BAD_STATUS.format(status=429, url=f"{FRAGMENT_BASE_URL}/api")) - - -async def execute_transaction_request( - session: httpx.AsyncClient, - headers: dict[str, str], - tx_data: dict[str, Any], - fragment_hash: str, -) -> dict[str, Any]: - transaction = await fragment_request(session, fragment_hash, headers, tx_data) - - if transaction.get("need_verify"): - raise VerificationError(VerificationError.KYC_REQUIRED) - - return transaction diff --git a/pyfragment/enums.py b/pyfragment/enums.py index 098f17b..e8ea1c3 100644 --- a/pyfragment/enums.py +++ b/pyfragment/enums.py @@ -1,18 +1,10 @@ from __future__ import annotations -import sys -from enum import Enum +from enum import StrEnum from typing import Any from tonutils.contracts.wallet import WalletHighloadV2, WalletHighloadV3R1, WalletV4R2, WalletV5R1 -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - - class StrEnum(str, Enum): # noqa: F811 - pass - class PaymentMethod(StrEnum): GRAM = "ton" diff --git a/pyfragment/exceptions.py b/pyfragment/exceptions.py index 9153d24..0dd296e 100644 --- a/pyfragment/exceptions.py +++ b/pyfragment/exceptions.py @@ -36,10 +36,6 @@ class ConfigurationError(ClientError): f"Invalid Stars amount: must be an integer between {STARS_PURCHASE_MIN:,} and {STARS_PURCHASE_MAX:,}." ) INVALID_GRAM_AMOUNT = f"Invalid GRAM (ex TON) amount: must be an integer between {GRAM_TOPUP_MIN:,} and {GRAM_TOPUP_MAX:,}." - 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 = ( f"Invalid winners count: must be an integer between {STARS_WINNERS_MIN:,} and {STARS_WINNERS_MAX:,}." ) diff --git a/pyfragment/services/cookies/service.py b/pyfragment/services/cookies/service.py index c88ed40..9ed7a8a 100644 --- a/pyfragment/services/cookies/service.py +++ b/pyfragment/services/cookies/service.py @@ -1,7 +1,7 @@ from __future__ import annotations import importlib -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from pyfragment.core.constants import FRAGMENT_BASE_URL, FRAGMENT_DOMAIN, REQUIRED_COOKIE_KEYS @@ -42,11 +42,11 @@ def get_cookies_from_browser(browser: str = "chrome") -> CookieResult: if cookie.get("name") == "stel_ssid": raw = cookie.get("expires") if isinstance(raw, (int, float)): - expires_iso = datetime.fromtimestamp(raw, tz=timezone.utc).isoformat() + expires_iso = datetime.fromtimestamp(raw, tz=UTC).isoformat() elif isinstance(raw, str) and raw: for fmt in ("%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"): try: - expires_iso = datetime.strptime(raw, fmt).replace(tzinfo=timezone.utc).isoformat() + expires_iso = datetime.strptime(raw, fmt).replace(tzinfo=UTC).isoformat() break except ValueError: continue @@ -54,7 +54,7 @@ def get_cookies_from_browser(browser: str = "chrome") -> CookieResult: if expires_iso: expires_dt = datetime.fromisoformat(expires_iso) - if expires_dt < datetime.now(timezone.utc): + if expires_dt < datetime.now(UTC): raise CookieError(CookieError.EXPIRED.format(expires=expires_iso)) return CookieResult(cookies={k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS}, expires=expires_iso) diff --git a/pyproject.toml b/pyproject.toml index dbc5a69..4af9914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,51 +5,33 @@ build-backend = "hatchling.build" [project] name = "pyfragment" version = "2026.3.2" -description = "Async Python client for the Fragment API. Buy Stars and Premium, top up GRAM (ex TON) and Ads balances, run giveaways, manage anonymous numbers, and search Fragment listings." +description = "Async Python client for the Fragment.com marketplace API." readme = "README.md" license = { text = "MIT" } -requires-python = ">=3.10" +requires-python = ">=3.11" authors = [{ name = "bohd4nx" }] keywords = [ "fragment", - "fragment-api", "telegram", - "telegram-api", "telegram-stars", "telegram-premium", - "telegram-giveaway", - "telegram-ads", "ton", - "ton-blockchain", "tonapi", "anonymous-numbers", - "username-auctions", - "gift-marketplace", - "crypto-payments", - "nft-marketplace", - "web3", - "python-client", - "typed", "asyncio", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", - "Natural Language :: English", "Operating System :: OS Independent", - "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Framework :: AsyncIO", - "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Internet", - "Topic :: Internet :: WWW/HTTP", "Typing :: Typed", ] dependencies = ["httpx>=0.25", "tonutils>=2.0.1"] @@ -59,7 +41,6 @@ dev = ["pytest", "pytest-asyncio", "pytest-mock", "mypy", "ruff"] browser = ["rookiepy>=0.5.6"] [project.urls] -Homepage = "https://github.com/bohd4nx/pyfragment" Repository = "https://github.com/bohd4nx/pyfragment" Documentation = "https://bohd4nx.gitbook.io/pyfragment/" Issues = "https://github.com/bohd4nx/pyfragment/issues" @@ -76,20 +57,17 @@ addopts = "-v --tb=short" [tool.ruff] line-length = 128 -target-version = "py310" +target-version = "py311" [tool.ruff.lint] -# E — pycodestyle errors, F — pyflakes, W — warnings, I — isort, UP — pyupgrade select = ["E", "F", "W", "I", "UP"] -# E501 — line too long (covered by line-length above) -# UP017 — use datetime.UTC (only available in Python 3.11+, we support 3.10) -ignore = ["E501", "UP017"] +ignore = ["E501"] [tool.ruff.lint.per-file-ignores] "tests/*" = ["E402"] [tool.mypy] -python_version = "3.10" +python_version = "3.11" strict = true exclude = ["^examples/", "^logs/", "^tests/"] diff --git a/tests/shared.py b/tests/shared.py index 161cf9e..edb1fd8 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -36,10 +36,6 @@ FAKE_BALANCE_NANOGRAM: int = 1_500_000_000 # 1.5 GRAM (ex TON) # recharge_ads FAKE_ADS_ACCOUNT: str = "@mychannel" -# Revenue withdrawals -FAKE_WITHDRAWAL_WALLET: str = "EQDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -FAKE_REVENUE_TX: str = "revenue_tx_abc123" - # Anonymous number FAKE_HTML_WITH_CODE: str = """ From d78be012a3a8f1896b80c1b13ff04c93e3e2560c Mon Sep 17 00:00:00 2001 From: bohd4nx Date: Sat, 27 Jun 2026 02:46:41 +0300 Subject: [PATCH 2/2] chore: remove author credit from README footer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d20c2fe..35de889 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,6 @@ asyncio.run(main())
-Made with ❤️ by [@bohd4nx](https://t.me/bohd4nx) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) +[Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)