mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-28 15:49:32 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 116ed24db9 | |||
| 4e017cb7a1 | |||
| 6c869428aa | |||
| 0c02062291 | |||
| c3de8a8ca3 | |||
| 38a4619e42 | |||
| 8cf57b58b7 | |||
| 2cdc102a74 | |||
| da56afc75f | |||
| 7f3c3ea1c3 | |||
| efcdda6b74 | |||
| b9c02646be | |||
| 2d7860682d | |||
| 64f8058c60 | |||
| f57f8271c3 | |||
| 5dc3cddf1a | |||
| d2faa27c5c | |||
| 91d33a0972 | |||
| e3706f01bf | |||
| b1eae1dcb7 | |||
| d299a1f804 | |||
| 76473993e2 | |||
| 49cf8843bc | |||
| 8adfaf4ad2 | |||
| 760c853acd |
@@ -0,0 +1,11 @@
|
||||
# Fragment.com cookies - copy from browser after login (Header String format)
|
||||
# Hash is now fetched dynamically
|
||||
|
||||
# TON wallet seed phrase - 12 or 24 words separated by spaces
|
||||
SEED = "your_ton_wallet_seed_phrase_here"
|
||||
|
||||
# TON API key - get from https://tonconsole.com
|
||||
API_KEY = "your_ton_api_key_here"
|
||||
|
||||
# TON wallet contract version: V4R2 or V5R1 (default: V5R1)
|
||||
WALLET_VERSION = "V5R1"
|
||||
@@ -1,5 +0,0 @@
|
||||
root: ./docs
|
||||
|
||||
structure:
|
||||
readme: README.md
|
||||
summary: SUMMARY.md
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
open-pull-requests-limit: 5
|
||||
labels:
|
||||
- "dependencies"
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Docs Branch Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [docs]
|
||||
pull_request:
|
||||
branches: [docs]
|
||||
|
||||
jobs:
|
||||
docs-tree:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Ensure docs structure exists
|
||||
run: |
|
||||
test -f docs/README.md
|
||||
test -f docs/SUMMARY.md
|
||||
test -d docs/getting-started
|
||||
test -d docs/client
|
||||
test -d docs/reference
|
||||
test -d docs/advanced
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt pytest pytest-asyncio
|
||||
|
||||
- name: Write cookies.json
|
||||
if: ${{ env.COOKIES_JSON != '' }}
|
||||
run: echo "$COOKIES_JSON" > cookies.json
|
||||
env:
|
||||
COOKIES_JSON: ${{ secrets.COOKIES_JSON }}
|
||||
|
||||
- name: Run tests
|
||||
run: pytest
|
||||
+25
@@ -1 +1,26 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
cookies.json
|
||||
|
||||
@@ -1,6 +1,187 @@
|
||||
# pyfragment docs branch
|
||||
<div align="center">
|
||||
<img src="fragment.svg" alt="Fragment Logo" width="120" height="120" style="border-radius: 24px;">
|
||||
|
||||
This branch is dedicated to GitBook content.
|
||||
<h1 style="margin-top: 24px;">💎 Fragment API by @bohd4nx</h1>
|
||||
|
||||
- Main docs source: docs/
|
||||
- Navigation: docs/SUMMARY.md
|
||||
<p style="font-size: 18px; margin-bottom: 24px;">
|
||||
<b>Automate TON topups, Telegram Premium purchases, and Stars transactions via Fragment.com</b>
|
||||
</p>
|
||||
|
||||
[](https://python.org)
|
||||
[](https://github.com/nessshon/tonutils)
|
||||
[](https://github.com/bohd4nx/FragmentAPI/stargazers)
|
||||
[](https://github.com/bohd4nx/FragmentAPI/issues)
|
||||
[](https://github.com/bohd4nx/FragmentAPI/actions)
|
||||
|
||||
[Report Bug](https://github.com/bohd4nx/fragmentapi/issues) · [Request Feature](https://github.com/bohd4nx/fragmentapi/issues) · [**Donate TON**](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 💰 **TON Advertisement Topups** — Send TON directly to Fragment ad accounts (1–1,000,000,000 TON)
|
||||
- 👑 **Telegram Premium Gifts** — Purchase Premium subscriptions for any user (3, 6, or 12 months)
|
||||
- ⭐ **Telegram Stars Purchases** — Buy Stars and send them to any Telegram user (50–1,000,000 Stars)
|
||||
- 🔐 **Multi-wallet support** — Configurable wallet contract version (V4R2 / V5R1)
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/bohd4nx/FragmentAPI.git
|
||||
cd FragmentAPI
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Configuration
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
cp cookies.example.json cookies.json
|
||||
```
|
||||
|
||||
Edit `.env`:
|
||||
|
||||
```env
|
||||
# 24-word TON wallet seed phrase
|
||||
SEED = word1 word2 word3 ... word24
|
||||
|
||||
# API key from @tonapibot on Telegram
|
||||
API_KEY = your_tonapi_key_here
|
||||
|
||||
# Wallet contract version: V4R2 or V5R1 (default: V5R1)
|
||||
WALLET_VERSION = V5R1
|
||||
```
|
||||
|
||||
### 3. Getting Required Data
|
||||
|
||||
#### 🍪 Fragment.com Cookies
|
||||
|
||||
**Prerequisites**: Log in to Telegram on Fragment and connect the TON wallet you'll use for payments.
|
||||
|
||||
1. Install [Cookie Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) extension
|
||||
2. Open [fragment.com](https://fragment.com) and make sure you're logged in
|
||||
3. Click the Cookie Editor icon → **Export** → **Header String**
|
||||
4. Split the result into the four fields in `cookies.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"stel_ssid": "...",
|
||||
"stel_dt": "...",
|
||||
"stel_token": "...",
|
||||
"stel_ton_token": "..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 🔐 TON Wallet Seed Phrase
|
||||
|
||||
If you don't have a TON wallet, create one in [Tonkeeper](https://tonkeeper.com) (iOS / Android).
|
||||
Go to **Settings → Backup**, copy the 24 words and paste them into `SEED` in `.env`.
|
||||
|
||||
> ⚠️ Never share your seed phrase with anyone. Store it offline.
|
||||
|
||||
#### 🔑 TON API Key
|
||||
|
||||
1. Go to [tonconsole.com](https://tonconsole.com)
|
||||
2. Create an account and log in
|
||||
3. Generate a new API key
|
||||
4. Paste it into `API_KEY` in `.env`
|
||||
|
||||
#### 🔐 Wallet Version
|
||||
|
||||
| Version | Use when |
|
||||
| ------- | -------------------------------------------------------------- |
|
||||
| `V5R1` | Default — Tonkeeper / MyTonWallet (wallets created after 2024) |
|
||||
| `V4R2` | Older Tonkeeper wallets |
|
||||
|
||||
Not sure? Run this to check which address matches your wallet:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import asyncio
|
||||
from tonutils.clients import TonapiClient
|
||||
from tonutils.contracts.wallet import WalletV4R2, WalletV5R1
|
||||
from tonutils.types import NetworkGlobalID
|
||||
from app.core import config
|
||||
|
||||
client = TonapiClient(network=NetworkGlobalID.MAINNET, api_key=config.API_KEY)
|
||||
w4, _, _, _ = WalletV4R2.from_mnemonic(client=client, mnemonic=config.SEED)
|
||||
w5, _, _, _ = WalletV5R1.from_mnemonic(client=client, mnemonic=config.SEED)
|
||||
print('V4R2:', w4.address.to_str(True, True))
|
||||
print('V5R1:', w5.address.to_str(True, True))
|
||||
"
|
||||
```
|
||||
|
||||
### 4. Usage
|
||||
|
||||
#### Run Examples
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
#### Programmatic Usage
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from app.methods import topup_ton, buy_premium, buy_stars
|
||||
|
||||
async def main():
|
||||
# Send 10 TON to @username
|
||||
result = await topup_ton("@username", 10)
|
||||
print(result)
|
||||
|
||||
# Gift 6 months of Telegram Premium (anonymous — recipient won't see sender)
|
||||
result = await buy_premium("@username", 6, show_sender=False)
|
||||
print(result)
|
||||
|
||||
# Buy 500 Stars for @username
|
||||
result = await buy_stars("@username", 500)
|
||||
print(result)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
**Return format** (on success):
|
||||
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"data": {
|
||||
"transaction_id": "<TL-B ExternalMessage ...>",
|
||||
"username": "@username",
|
||||
"amount": 10, # or "months" for Premium
|
||||
"timestamp": 1741234567
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Return format** (on failure):
|
||||
|
||||
```python
|
||||
{
|
||||
"success": False,
|
||||
"error": "Telegram user '@unknown' was not found on Fragment."
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Operations
|
||||
|
||||
| Operation | Function | Parameters | Limits |
|
||||
| ------------------ | ----------------------------------------------------- | ----------------------------------- | ------------------- |
|
||||
| **TON Topup** | `topup_ton(username, amount, show_sender=True)` | Username, TON amount, show sender | 1–1,000,000,000 TON |
|
||||
| **Premium Gift** | `buy_premium(username, months, show_sender=True)` | Username, duration, show sender | 3, 6, or 12 months |
|
||||
| **Stars Purchase** | `buy_stars(username, amount, show_sender=True)` | Username, Stars amount, show sender | 50–1,000,000 Stars |
|
||||
|
||||
Usernames can be passed with or without `@`.
|
||||
|
||||
<div align="center">
|
||||
|
||||
### Made with ❤️ by [@bohd4nx](https://t.me/bohd4nx)
|
||||
|
||||
**Star ⭐ this repo if you found it useful!**
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from app.core.config import config
|
||||
from app.core.constants import (
|
||||
ADS_PAGE,
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
PREMIUM_PAGE,
|
||||
STARS_PAGE,
|
||||
WALLET_CLASSES,
|
||||
WalletVersion,
|
||||
)
|
||||
from app.core.cookies import load_cookies
|
||||
from app.core.exceptions import (
|
||||
ConfigError,
|
||||
CookiesError,
|
||||
FragmentError,
|
||||
HashFetchError,
|
||||
RequestError,
|
||||
TransactionError,
|
||||
UserNotFoundError,
|
||||
WalletError,
|
||||
)
|
||||
from app.core.logging import logger, setup_logging
|
||||
|
||||
__all__ = [
|
||||
"ADS_PAGE",
|
||||
"BASE_HEADERS",
|
||||
"DEVICE",
|
||||
"PREMIUM_PAGE",
|
||||
"STARS_PAGE",
|
||||
"WALLET_CLASSES",
|
||||
"WalletVersion",
|
||||
"ConfigError",
|
||||
"CookiesError",
|
||||
"FragmentError",
|
||||
"HashFetchError",
|
||||
"RequestError",
|
||||
"TransactionError",
|
||||
"UserNotFoundError",
|
||||
"WalletError",
|
||||
"config",
|
||||
"load_cookies",
|
||||
"logger",
|
||||
"setup_logging",
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from app.core.constants import SUPPORTED_WALLET_VERSIONS, WalletVersion
|
||||
from app.core.exceptions import ConfigError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Config:
|
||||
SEED: str
|
||||
API_KEY: str
|
||||
WALLET_VERSION: WalletVersion
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Load .env if present; env vars already in the process take precedence
|
||||
env_path = Path(__file__).resolve().parents[2] / ".env"
|
||||
if env_path.exists():
|
||||
load_dotenv(env_path)
|
||||
|
||||
missing = [k for k in ("SEED", "API_KEY") if not os.getenv(k, "").strip()]
|
||||
if missing:
|
||||
raise ConfigError(
|
||||
f"Missing required environment variables: {', '.join(missing)}. "
|
||||
"Copy .env.example to .env and fill in SEED and API_KEY."
|
||||
)
|
||||
|
||||
self.SEED = os.getenv("SEED", "").strip()
|
||||
self.API_KEY = os.getenv("API_KEY", "").strip()
|
||||
|
||||
version = os.getenv("WALLET_VERSION", "V5R1").strip().upper()
|
||||
if version not in SUPPORTED_WALLET_VERSIONS:
|
||||
raise ConfigError(
|
||||
f"Unsupported WALLET_VERSION '{version}'. " f"Must be one of: {', '.join(sorted(SUPPORTED_WALLET_VERSIONS))}."
|
||||
)
|
||||
self.WALLET_VERSION: WalletVersion = version # type: ignore[assignment]
|
||||
|
||||
|
||||
config: Config | None = None
|
||||
try:
|
||||
config = Config()
|
||||
except ConfigError as e:
|
||||
logger.warning("Configuration not loaded: %s", e)
|
||||
@@ -0,0 +1,49 @@
|
||||
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}
|
||||
|
||||
# Fragment page URLs
|
||||
STARS_PAGE: str = "https://fragment.com/stars/buy"
|
||||
PREMIUM_PAGE: str = "https://fragment.com/premium/gift"
|
||||
ADS_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,32 @@
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.core.exceptions import CookiesError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_REQUIRED_KEYS = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token")
|
||||
|
||||
|
||||
def load_cookies() -> dict[str, Any]:
|
||||
cookies_path = Path(__file__).resolve().parents[2] / "cookies.json"
|
||||
|
||||
if not cookies_path.exists():
|
||||
raise CookiesError("cookies.json not found. Create it in the project root and paste your Fragment cookies.")
|
||||
|
||||
try:
|
||||
with cookies_path.open("r", encoding="utf-8") as f:
|
||||
cookies = json.load(f)
|
||||
except Exception as exc:
|
||||
raise CookiesError(f"Failed to read cookies.json: {exc}") from exc
|
||||
|
||||
missing = [k for k in _REQUIRED_KEYS if not str(cookies.get(k, "")).strip()]
|
||||
if missing:
|
||||
raise CookiesError(
|
||||
f"cookies.json is missing or has empty values for: {', '.join(missing)}. "
|
||||
"Open Fragment.com in your browser, copy fresh cookies, and update the file."
|
||||
)
|
||||
|
||||
return cookies
|
||||
@@ -0,0 +1,42 @@
|
||||
__all__ = [
|
||||
"ConfigError",
|
||||
"CookiesError",
|
||||
"FragmentError",
|
||||
"HashFetchError",
|
||||
"RequestError",
|
||||
"TransactionError",
|
||||
"UserNotFoundError",
|
||||
"WalletError",
|
||||
]
|
||||
|
||||
|
||||
class FragmentError(Exception):
|
||||
"""Base exception for all Fragment API errors."""
|
||||
|
||||
|
||||
class ConfigError(FragmentError):
|
||||
"""Raised when .env is missing or required keys are absent."""
|
||||
|
||||
|
||||
class CookiesError(FragmentError):
|
||||
"""Raised when cookies.json is missing, unreadable, or has empty required fields."""
|
||||
|
||||
|
||||
class HashFetchError(FragmentError):
|
||||
"""Raised when the Fragment API hash cannot be fetched from the page."""
|
||||
|
||||
|
||||
class UserNotFoundError(FragmentError):
|
||||
"""Raised when the target Telegram user is not found on Fragment."""
|
||||
|
||||
|
||||
class WalletError(FragmentError):
|
||||
"""Raised for TON wallet issues (connection, balance, account info)."""
|
||||
|
||||
|
||||
class TransactionError(FragmentError):
|
||||
"""Raised when a TON transaction fails to build or broadcast."""
|
||||
|
||||
|
||||
class RequestError(FragmentError):
|
||||
"""Raised when a Fragment API response cannot be parsed."""
|
||||
@@ -0,0 +1,20 @@
|
||||
import logging
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
formatter = logging.Formatter(fmt="[%(asctime)s] - %(levelname)s: %(message)s", datefmt="%d.%m.%y %H:%M:%S")
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
file_handler = logging.FileHandler("FragmentAPI.log", mode="w", encoding="utf-8")
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(formatter)
|
||||
logging.basicConfig(level=logging.DEBUG, handlers=[console_handler, file_handler], force=True)
|
||||
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -0,0 +1,5 @@
|
||||
from app.methods.premium import buy_premium
|
||||
from app.methods.stars import buy_stars
|
||||
from app.methods.ton import topup_ton
|
||||
|
||||
__all__ = ["buy_premium", "buy_stars", "topup_ton"]
|
||||
@@ -0,0 +1,151 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core import (
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
PREMIUM_PAGE,
|
||||
FragmentError,
|
||||
UserNotFoundError,
|
||||
load_cookies,
|
||||
)
|
||||
from app.utils import (
|
||||
execute_transaction_request,
|
||||
get_account_info,
|
||||
get_fragment_hash,
|
||||
parse_json_response,
|
||||
process_transaction,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Page-specific headers
|
||||
HEADERS: dict[str, str] = {
|
||||
**BASE_HEADERS,
|
||||
"referer": PREMIUM_PAGE,
|
||||
"x-aj-referer": PREMIUM_PAGE,
|
||||
}
|
||||
|
||||
|
||||
async def search_premium_recipient(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
username: str,
|
||||
months: int,
|
||||
) -> str:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
data={
|
||||
"query": username,
|
||||
"months": months,
|
||||
"method": "searchPremiumGiftRecipient",
|
||||
},
|
||||
)
|
||||
result = parse_json_response(resp, "searchPremiumGiftRecipient")
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(
|
||||
f"Telegram user '{username}' was not found on Fragment. "
|
||||
"Make sure the username is correct and the account exists."
|
||||
)
|
||||
return recipient
|
||||
|
||||
|
||||
async def init_gift_premium(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
recipient: str,
|
||||
months: int,
|
||||
) -> str:
|
||||
await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
data={
|
||||
"mode": "new",
|
||||
"lv": "false",
|
||||
"dh": str(int(time.time())),
|
||||
"method": "updatePremiumState",
|
||||
},
|
||||
)
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
data={
|
||||
"recipient": recipient,
|
||||
"months": months,
|
||||
"method": "initGiftPremiumRequest",
|
||||
},
|
||||
)
|
||||
result = parse_json_response(resp, "initGiftPremiumRequest")
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentError(
|
||||
"Fragment did not return a request ID for this Premium purchase. "
|
||||
"The session may have expired — refresh your cookies."
|
||||
)
|
||||
return req_id
|
||||
|
||||
|
||||
async def buy_premium(username: str, months: int, show_sender: bool = True) -> dict:
|
||||
if months not in (3, 6, 12):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Invalid duration. Choose 3, 6, or 12 months.",
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info("Loading session cookies")
|
||||
cookies = load_cookies()
|
||||
|
||||
logger.info("Fetching Fragment session hash")
|
||||
fragment_hash = await get_fragment_hash(cookies, HEADERS, PREMIUM_PAGE)
|
||||
|
||||
# logger.info("Retrieving TON wallet info")
|
||||
account = await get_account_info()
|
||||
|
||||
async with httpx.AsyncClient(cookies=cookies) as client:
|
||||
logger.info("Searching recipient: %s", username)
|
||||
recipient = await search_premium_recipient(client, fragment_hash, username, months)
|
||||
|
||||
logger.info("Initializing Premium gift request: %s months to %s", months, username)
|
||||
req_id = await init_gift_premium(client, fragment_hash, recipient, months)
|
||||
|
||||
# logger.info("Requesting transaction payload (req_id=%s)", req_id)
|
||||
tx_data = {
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
"transaction": 1,
|
||||
"id": req_id,
|
||||
"show_sender": int(show_sender),
|
||||
"method": "getGiftPremiumLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(client, HEADERS, account, tx_data, fragment_hash)
|
||||
|
||||
logger.info("Broadcasting transaction to TON blockchain")
|
||||
tx_hash = await process_transaction(transaction)
|
||||
logger.info(
|
||||
"Premium purchase successful: %s months -> %s | tx: %s",
|
||||
months,
|
||||
username,
|
||||
tx_hash,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"transaction_id": tx_hash,
|
||||
"username": username,
|
||||
"months": months,
|
||||
"timestamp": int(time.time()),
|
||||
},
|
||||
}
|
||||
|
||||
except FragmentError as exc:
|
||||
logger.error("Premium purchase failed — %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
except Exception as exc:
|
||||
logger.exception("Unexpected error during Premium purchase")
|
||||
return {"success": False, "error": f"Unexpected error: {exc}"}
|
||||
@@ -0,0 +1,133 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core import (
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
STARS_PAGE,
|
||||
FragmentError,
|
||||
UserNotFoundError,
|
||||
load_cookies,
|
||||
)
|
||||
from app.utils import (
|
||||
execute_transaction_request,
|
||||
get_account_info,
|
||||
get_fragment_hash,
|
||||
parse_json_response,
|
||||
process_transaction,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Page-specific headers
|
||||
HEADERS: dict[str, str] = {
|
||||
**BASE_HEADERS,
|
||||
"referer": STARS_PAGE,
|
||||
"x-aj-referer": STARS_PAGE,
|
||||
}
|
||||
|
||||
|
||||
async def search_stars_recipient(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
username: str,
|
||||
) -> str:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
data={"query": username, "quantity": "", "method": "searchStarsRecipient"},
|
||||
)
|
||||
result = parse_json_response(resp, "searchStarsRecipient")
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(
|
||||
f"Telegram user '{username}' was not found on Fragment. "
|
||||
"Make sure the username is correct and the account exists."
|
||||
)
|
||||
return recipient
|
||||
|
||||
|
||||
async def init_buy_stars(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
recipient: str,
|
||||
amount: int,
|
||||
) -> str:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
data={
|
||||
"recipient": recipient,
|
||||
"quantity": amount,
|
||||
"method": "initBuyStarsRequest",
|
||||
},
|
||||
)
|
||||
result = parse_json_response(resp, "initBuyStarsRequest")
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentError(
|
||||
"Fragment did not return a request ID for this Stars purchase. "
|
||||
"The session may have expired — refresh your cookies."
|
||||
)
|
||||
return req_id
|
||||
|
||||
|
||||
async def buy_stars(username: str, amount: int, show_sender: bool = True) -> dict:
|
||||
if not isinstance(amount, int) or amount < 50:
|
||||
return {"success": False, "error": "Amount must be an integer >= 50 stars."}
|
||||
|
||||
try:
|
||||
logger.info("Loading session cookies")
|
||||
cookies = load_cookies()
|
||||
|
||||
logger.info("Fetching Fragment session hash")
|
||||
fragment_hash = await get_fragment_hash(cookies, HEADERS, STARS_PAGE)
|
||||
|
||||
# logger.info("Retrieving TON wallet info")
|
||||
account = await get_account_info()
|
||||
|
||||
async with httpx.AsyncClient(cookies=cookies) as client:
|
||||
logger.info("Searching recipient: %s", username)
|
||||
recipient = await search_stars_recipient(client, fragment_hash, username)
|
||||
|
||||
logger.info("Initializing Stars purchase request: %s stars to %s", amount, username)
|
||||
req_id = await init_buy_stars(client, fragment_hash, recipient, amount)
|
||||
|
||||
# logger.info("Requesting transaction payload (req_id=%s)", req_id)
|
||||
tx_data = {
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
"transaction": 1,
|
||||
"id": req_id,
|
||||
"show_sender": int(show_sender),
|
||||
"method": "getBuyStarsLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(client, HEADERS, account, tx_data, fragment_hash)
|
||||
|
||||
logger.info("Broadcasting transaction to TON blockchain")
|
||||
tx_hash = await process_transaction(transaction)
|
||||
logger.info(
|
||||
"Stars purchase successful: %s stars -> %s | tx: %s",
|
||||
amount,
|
||||
username,
|
||||
tx_hash,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"transaction_id": tx_hash,
|
||||
"username": username,
|
||||
"amount": amount,
|
||||
"timestamp": int(time.time()),
|
||||
},
|
||||
}
|
||||
|
||||
except FragmentError as exc:
|
||||
logger.error("Stars purchase failed — %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
except Exception as exc:
|
||||
logger.exception("Unexpected error during Stars purchase")
|
||||
return {"success": False, "error": f"Unexpected error: {exc}"}
|
||||
@@ -0,0 +1,132 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core import (
|
||||
ADS_PAGE,
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
FragmentError,
|
||||
UserNotFoundError,
|
||||
load_cookies,
|
||||
)
|
||||
from app.utils import (
|
||||
execute_transaction_request,
|
||||
get_account_info,
|
||||
get_fragment_hash,
|
||||
parse_json_response,
|
||||
process_transaction,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Page-specific headers
|
||||
HEADERS: dict[str, str] = {
|
||||
**BASE_HEADERS,
|
||||
"referer": ADS_PAGE,
|
||||
"x-aj-referer": ADS_PAGE,
|
||||
}
|
||||
|
||||
|
||||
async def search_ads_recipient(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
username: str,
|
||||
) -> str:
|
||||
await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
data={"mode": "new", "method": "updateAdsTopupState"},
|
||||
)
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
data={"query": username, "method": "searchAdsTopupRecipient"},
|
||||
)
|
||||
result = parse_json_response(resp, "searchAdsTopupRecipient")
|
||||
recipient = result.get("found", {}).get("recipient")
|
||||
if not recipient:
|
||||
raise UserNotFoundError(
|
||||
f"Telegram user '{username}' was not found on Fragment. "
|
||||
"Make sure the username is correct and the account exists."
|
||||
)
|
||||
return recipient
|
||||
|
||||
|
||||
async def init_ads_topup(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
recipient: str,
|
||||
amount: int,
|
||||
) -> str:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
data={
|
||||
"recipient": recipient,
|
||||
"amount": amount,
|
||||
"method": "initAdsTopupRequest",
|
||||
},
|
||||
)
|
||||
result = parse_json_response(resp, "initAdsTopupRequest")
|
||||
req_id = result.get("req_id")
|
||||
if not req_id:
|
||||
raise FragmentError(
|
||||
"Fragment did not return a request ID for this TON topup. " "The session may have expired — refresh your cookies."
|
||||
)
|
||||
return req_id
|
||||
|
||||
|
||||
async def topup_ton(username: str, amount: int, show_sender: bool = True) -> dict:
|
||||
if not isinstance(amount, int) or amount < 1:
|
||||
return {"success": False, "error": "Amount must be an integer >= 1 TON."}
|
||||
|
||||
try:
|
||||
logger.info("Loading session cookies")
|
||||
cookies = load_cookies()
|
||||
|
||||
logger.info("Fetching Fragment session hash")
|
||||
fragment_hash = await get_fragment_hash(cookies, HEADERS, ADS_PAGE)
|
||||
|
||||
# logger.info("Retrieving TON wallet info")
|
||||
account = await get_account_info()
|
||||
|
||||
async with httpx.AsyncClient(cookies=cookies) as client:
|
||||
logger.info("Searching recipient: %s", username)
|
||||
recipient = await search_ads_recipient(client, fragment_hash, username)
|
||||
|
||||
logger.info("Initializing topup request: %s TON to %s", amount, username)
|
||||
req_id = await init_ads_topup(client, fragment_hash, recipient, amount)
|
||||
|
||||
# logger.info("Requesting transaction payload (req_id=%s)", req_id)
|
||||
tx_data = {
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
"transaction": 1,
|
||||
"id": req_id,
|
||||
"show_sender": int(show_sender),
|
||||
"method": "getAdsTopupLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(client, HEADERS, account, tx_data, fragment_hash)
|
||||
|
||||
logger.info("Broadcasting transaction to TON blockchain")
|
||||
tx_hash = await process_transaction(transaction)
|
||||
logger.info("TON topup successful: %s TON -> %s | tx: %s", amount, username, tx_hash)
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"transaction_id": tx_hash,
|
||||
"username": username,
|
||||
"amount": amount,
|
||||
"timestamp": int(time.time()),
|
||||
},
|
||||
}
|
||||
|
||||
except FragmentError as exc:
|
||||
logger.error("TON topup failed — %s", exc)
|
||||
return {"success": False, "error": str(exc)}
|
||||
except Exception as exc:
|
||||
logger.exception("Unexpected error during TON topup")
|
||||
return {"success": False, "error": f"Unexpected error: {exc}"}
|
||||
@@ -0,0 +1,14 @@
|
||||
from app.utils.client import execute_transaction_request, parse_json_response
|
||||
from app.utils.decoder import clean_decode
|
||||
from app.utils.hash import get_fragment_hash
|
||||
from app.utils.wallet import get_account_info, link_wallet, process_transaction
|
||||
|
||||
__all__ = [
|
||||
"clean_decode",
|
||||
"execute_transaction_request",
|
||||
"get_account_info",
|
||||
"get_fragment_hash",
|
||||
"link_wallet",
|
||||
"parse_json_response",
|
||||
"process_transaction",
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core import RequestError, WalletError
|
||||
from app.utils.wallet import link_wallet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_json_response(response: httpx.Response, context: str) -> dict[str, Any]:
|
||||
try:
|
||||
return response.json()
|
||||
except Exception as exc:
|
||||
raise RequestError(f"Fragment API returned an unparseable response for '{context}': {exc}") from exc
|
||||
|
||||
|
||||
async def execute_transaction_request(
|
||||
client: httpx.AsyncClient,
|
||||
headers: dict,
|
||||
account: dict[str, Any],
|
||||
tx_data: dict[str, Any],
|
||||
fragment_hash: str,
|
||||
) -> dict[str, Any]:
|
||||
url = f"https://fragment.com/api?hash={fragment_hash}"
|
||||
|
||||
resp = await client.post(url, headers=headers, data=tx_data)
|
||||
transaction = parse_json_response(resp, tx_data.get("method", "transaction"))
|
||||
|
||||
if transaction.get("need_verify"):
|
||||
if not await link_wallet(client, headers, account, fragment_hash):
|
||||
raise WalletError(
|
||||
"Failed to link your TON wallet to Fragment. " "Make sure the wallet matching your cookies is used."
|
||||
)
|
||||
resp = await client.post(url, headers=headers, data=tx_data)
|
||||
transaction = parse_json_response(resp, tx_data.get("method", "transaction"))
|
||||
|
||||
return transaction
|
||||
@@ -0,0 +1,36 @@
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from pytoniq_core import Cell
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# OLD decoder (manual base64 + regex, kept for reference):
|
||||
#
|
||||
# import re, string
|
||||
# def clean_decode(payload: str) -> str:
|
||||
# s = re.sub(r'[^A-Za-z0-9+/=]', '', payload.strip())
|
||||
# s += '=' * (-len(s) % 4)
|
||||
# text = base64.b64decode(s).decode('utf-8', errors='ignore')
|
||||
# text = ''.join(c for c in text if c in string.printable or c.isspace())
|
||||
# match = re.search(r'([0-9]*\s*Telegram .*?Ref#[A-Za-z0-9]+)', text, re.S)
|
||||
# return match.group(1).strip() if match else text.strip()
|
||||
|
||||
|
||||
def clean_decode(payload: str) -> str:
|
||||
# Pad and decode base64 → BOC bytes
|
||||
s = payload.strip()
|
||||
if not s:
|
||||
return ""
|
||||
s += "=" * (-len(s) % 4)
|
||||
boc = base64.b64decode(s)
|
||||
|
||||
# Parse BOC cell and read snake-encoded text (skipping 32-bit op prefix)
|
||||
cell = Cell.one_from_boc(boc)
|
||||
sl = cell.begin_parse()
|
||||
sl.load_uint(32) # op code — always 0 for text comment
|
||||
result = sl.load_snake_string().strip()
|
||||
|
||||
logger.debug("Payload: %s -> %s", payload, result.replace("\n", " "))
|
||||
return result
|
||||
@@ -0,0 +1,57 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core import HashFetchError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_fragment_hash(
|
||||
cookies: dict[str, Any],
|
||||
headers: dict[str, str],
|
||||
page_url: str,
|
||||
) -> str:
|
||||
# Must look like a real browser navigation — not an XHR — otherwise Fragment
|
||||
# returns JSON (no hash in it) instead of full HTML.
|
||||
page_headers = {
|
||||
k: v
|
||||
for k, v in headers.items()
|
||||
if k
|
||||
not in (
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"content-type",
|
||||
"x-requested-with",
|
||||
"x-aj-referer",
|
||||
)
|
||||
}
|
||||
page_headers.update(
|
||||
{
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"referer": "https://fragment.com/",
|
||||
"sec-fetch-dest": "document",
|
||||
"sec-fetch-mode": "navigate",
|
||||
"upgrade-insecure-requests": "1",
|
||||
}
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(cookies=cookies) as client:
|
||||
response = await client.get(page_url, headers=page_headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise HashFetchError(
|
||||
f"Fragment returned HTTP {response.status_code} for {page_url}. "
|
||||
"Check that your cookies are valid and not expired."
|
||||
)
|
||||
|
||||
match = re.search(r"(?:https://fragment\.com)?/api\?hash=([a-f0-9]+)", response.text)
|
||||
if not match:
|
||||
raise HashFetchError(
|
||||
f"Fragment hash not found in the page source of {page_url}. "
|
||||
"The page structure may have changed or you are not logged in."
|
||||
)
|
||||
|
||||
return match.group(1)
|
||||
@@ -0,0 +1,107 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from tonutils.clients import TonapiClient
|
||||
from tonutils.types import NetworkGlobalID
|
||||
|
||||
from app.core import DEVICE, WALLET_CLASSES, TransactionError, WalletError, config
|
||||
from app.utils.decoder import clean_decode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def initialize_ton_client() -> TonapiClient:
|
||||
return TonapiClient(network=NetworkGlobalID.MAINNET, api_key=config.API_KEY)
|
||||
|
||||
|
||||
async def process_transaction(transaction_data: dict) -> str:
|
||||
logger.debug("transaction_data: %s", transaction_data)
|
||||
|
||||
if "transaction" not in transaction_data or "messages" not in transaction_data["transaction"]:
|
||||
raise TransactionError(
|
||||
"Fragment returned an invalid transaction payload. "
|
||||
"The API response is missing expected 'transaction.messages' data."
|
||||
)
|
||||
|
||||
# TODO: Investigate 406 'inbound external message rejected before smart-contract execution'.
|
||||
# This happens when the previous transaction's seqno hasn't been confirmed on-chain yet,
|
||||
# causing the wallet contract to reject the new message.
|
||||
async with initialize_ton_client() as client:
|
||||
wallet_cls = WALLET_CLASSES[config.WALLET_VERSION]
|
||||
wallet, _, _, _ = wallet_cls.from_mnemonic(client=client, mnemonic=config.SEED)
|
||||
|
||||
# Check balance before broadcasting
|
||||
try:
|
||||
await wallet.refresh()
|
||||
balance_ton = wallet.balance / 1_000_000_000
|
||||
if balance_ton < 0.056:
|
||||
raise WalletError(f"TON wallet balance is too low: {balance_ton:.2f} TON. " "Minimum required is 0.056 TON.")
|
||||
except WalletError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise WalletError(f"Wallet balance check failed: {exc}") from exc
|
||||
|
||||
try:
|
||||
message = transaction_data["transaction"]["messages"][0]
|
||||
payload = clean_decode(message["payload"])
|
||||
|
||||
result = await wallet.transfer(
|
||||
destination=message["address"],
|
||||
amount=int(message["amount"]), # nanotons, not TON
|
||||
body=payload,
|
||||
)
|
||||
tx_hash = result.normalized_hash
|
||||
return tx_hash
|
||||
except (WalletError, TransactionError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise TransactionError(f"Transaction broadcast failed: {exc}") from exc
|
||||
|
||||
|
||||
async def get_account_info() -> dict[str, Any]:
|
||||
async with initialize_ton_client() as client:
|
||||
try:
|
||||
wallet_cls = WALLET_CLASSES[config.WALLET_VERSION]
|
||||
wallet, pub_key, _, _ = wallet_cls.from_mnemonic(client=client, mnemonic=config.SEED)
|
||||
boc = wallet.state_init.serialize().to_boc()
|
||||
return {
|
||||
"address": wallet.address.to_str(False, False),
|
||||
"publicKey": pub_key.as_hex,
|
||||
"chain": "-239",
|
||||
"walletStateInit": base64.b64encode(boc).decode(),
|
||||
}
|
||||
except Exception as exc:
|
||||
raise WalletError(f"Failed to retrieve wallet account info: {exc}") from exc
|
||||
|
||||
|
||||
async def link_wallet(
|
||||
client: httpx.AsyncClient,
|
||||
headers: dict,
|
||||
account: dict[str, Any],
|
||||
fragment_hash: str,
|
||||
) -> bool:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=headers,
|
||||
data={
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
"method": "linkWallet",
|
||||
},
|
||||
)
|
||||
result = resp.json()
|
||||
|
||||
if result.get("ok"):
|
||||
return True
|
||||
|
||||
if "transaction" in result:
|
||||
try:
|
||||
await process_transaction(result)
|
||||
return True
|
||||
except (TransactionError, WalletError):
|
||||
return False
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"stel_ssid": "",
|
||||
"stel_dt": "",
|
||||
"stel_token": "",
|
||||
"stel_ton_token": ""
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
# Overview
|
||||
|
||||
`pyfragment` is an async Python client for [Fragment](https://fragment.com).
|
||||
|
||||
If you are integrating Fragment into a bot or backend, this docs set is meant to be practical, not theoretical.
|
||||
|
||||
**Recommended reading order:**
|
||||
|
||||
1. Install the package
|
||||
2. Configure `FragmentClient`
|
||||
3. Set up credentials and cookies
|
||||
4. Run the quick start
|
||||
5. Move to feature-specific flows
|
||||
|
||||
## Who this is for
|
||||
|
||||
- Python developers integrating Fragment into bots, services, and automation.
|
||||
- Teams that need predictable typed results and explicit error behavior.
|
||||
|
||||
**Important:** this library is not affiliated with Fragment or Telegram.
|
||||
|
||||
## Where to begin
|
||||
|
||||
1. [Installation](getting-started/installation.md)
|
||||
2. [Library and Configuration](getting-started/configuration.md)
|
||||
3. [Credentials and Cookies](getting-started/credentials-and-cookies.md)
|
||||
4. [Quick Start](getting-started/quickstart.md)
|
||||
|
||||
## Feature entry points
|
||||
|
||||
- Stars: [Purchase](client/stars/purchase.md), [Giveaway](client/stars/giveaway.md)
|
||||
- Premium: [Purchase](client/premium/purchase.md), [Giveaway](client/premium/giveaway.md)
|
||||
- Marketplace: [Overview](client/marketplace/overview.md), Ads: [Overview](client/ads/overview.md)
|
||||
- Numbers: [Anonymous Numbers](client/anonymous-numbers/overview.md)
|
||||
- Utility operations: [Raw API Calls](client/raw-call.md)
|
||||
|
||||
## Additional references
|
||||
|
||||
- [Error Handling](reference/errors.md)
|
||||
- [Result Models](reference/models.md)
|
||||
- [Literal Types](reference/literals.md)
|
||||
- [Troubleshooting](advanced/troubleshooting.md)
|
||||
|
||||
## Live examples
|
||||
|
||||
**Up-to-date runnable examples live in the main repository:**
|
||||
|
||||
- https://github.com/bohd4nx/pyfragment/tree/master/examples
|
||||
@@ -1,36 +0,0 @@
|
||||
- [Overview](README.md)
|
||||
- Setup Guide
|
||||
- [Installation](getting-started/installation.md)
|
||||
- [Library and Configuration](getting-started/configuration.md)
|
||||
- [Credentials and Cookies](getting-started/credentials-and-cookies.md)
|
||||
- [Quick Start](getting-started/quickstart.md)
|
||||
- [Error Handling](reference/errors.md)
|
||||
- API Guides
|
||||
- [Overview](client/overview.md)
|
||||
- Stars
|
||||
- [Purchase](client/stars/purchase.md)
|
||||
- [Giveaway](client/stars/giveaway.md)
|
||||
- Premium
|
||||
- [Purchase](client/premium/purchase.md)
|
||||
- [Giveaway](client/premium/giveaway.md)
|
||||
- Marketplace
|
||||
- [Overview](client/marketplace/overview.md)
|
||||
- [Search Usernames](client/marketplace/search-usernames.md)
|
||||
- [Search Numbers](client/marketplace/search-numbers.md)
|
||||
- [Search Gifts](client/marketplace/search-gifts.md)
|
||||
- Ads
|
||||
- [Overview](client/ads/overview.md)
|
||||
- [Top Up GRAM](client/ads/topup-gram.md)
|
||||
- [Recharge Ads](client/ads/recharge-ads.md)
|
||||
- Anonymous Numbers
|
||||
- [Overview](client/anonymous-numbers/overview.md)
|
||||
- [Get Login Code](client/anonymous-numbers/get-login-code.md)
|
||||
- [Toggle Login Codes](client/anonymous-numbers/toggle-login-codes.md)
|
||||
- [Terminate Sessions](client/anonymous-numbers/terminate-sessions.md)
|
||||
- [Raw API Calls](client/raw-call.md)
|
||||
- Reference
|
||||
- [Result Models](reference/models.md)
|
||||
- [Literal Types](reference/literals.md)
|
||||
- Advanced
|
||||
- [Cookie Extraction Details](advanced/cookies.md)
|
||||
- [Troubleshooting](advanced/troubleshooting.md)
|
||||
@@ -1,24 +0,0 @@
|
||||
# Cookie Extraction Details
|
||||
|
||||
`get_cookies_from_browser(browser)` reads Fragment cookies from local browser storage (via `rookiepy`).
|
||||
|
||||
This is the fastest way to start when you do not want manual cookie export.
|
||||
|
||||
Supported browsers are defined in constants and include:
|
||||
|
||||
- chrome, firefox, edge, brave,
|
||||
- arc, opera, opera_gx,
|
||||
- safari, vivaldi,
|
||||
- chromium variants.
|
||||
|
||||
Validation includes:
|
||||
|
||||
- required key presence,
|
||||
- non-empty values,
|
||||
- optional expiration check for `stel_ssid`.
|
||||
|
||||
**If any required cookie is empty or missing, extraction is treated as failed.**
|
||||
|
||||
If extraction fails, `CookieError` is raised with actionable details.
|
||||
|
||||
Use [Credentials and Cookies](../getting-started/credentials-and-cookies.md) for setup-first instructions.
|
||||
@@ -1,59 +0,0 @@
|
||||
# Troubleshooting
|
||||
|
||||
When something breaks, start here. Most issues are caused by cookies, session state, or wallet balance.
|
||||
|
||||
## Auth/session errors
|
||||
|
||||
Symptoms:
|
||||
|
||||
- Fragment page hash cannot be extracted,
|
||||
- bad status loading Fragment pages,
|
||||
- missing request IDs.
|
||||
|
||||
Actions:
|
||||
|
||||
- re-login on fragment.com,
|
||||
- refresh cookies,
|
||||
- ensure all `stel_*` keys are present.
|
||||
- verify constructor payload in [Library and Configuration](../getting-started/configuration.md).
|
||||
|
||||
**Re-login + fresh cookies solves the majority of auth errors.**
|
||||
|
||||
## Cookie extraction errors
|
||||
|
||||
Symptoms:
|
||||
|
||||
- browser not supported,
|
||||
- cannot read browser profile,
|
||||
- required cookies not found.
|
||||
|
||||
Actions:
|
||||
|
||||
- install `pyfragment[browser]`,
|
||||
- close locked browser profiles,
|
||||
- use manual cookies if needed.
|
||||
|
||||
## Balance/transaction failures
|
||||
|
||||
Symptoms:
|
||||
|
||||
- low TON/USDT balance errors,
|
||||
- broadcast failures,
|
||||
- duplicate seqno retries.
|
||||
|
||||
Actions:
|
||||
|
||||
- keep GRAM (ex TON) reserve for fees,
|
||||
- ensure USDT is on the **Fragment-linked wallet**,
|
||||
- retry after short delay when seqno collisions happen.
|
||||
- check operation constraints in Stars/Premium/Ads method pages.
|
||||
|
||||
## SSL-related broadcast failures
|
||||
|
||||
If you get SSL-related errors during **TON transaction broadcast** (not Fragment page loading — those use curl_cffi with bundled SSL):
|
||||
|
||||
```bash
|
||||
pip install --upgrade certifi
|
||||
```
|
||||
|
||||
On macOS, also run Python's `Install Certificates.command` if needed.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Ads Overview
|
||||
|
||||
Ads flow is split into two methods:
|
||||
|
||||
- [Top Up GRAM](topup-gram.md)
|
||||
- [Recharge Ads](recharge-ads.md)
|
||||
|
||||
Use the first method to send GRAM (ex TON) to a Telegram user.
|
||||
Use the second method to fund your own Telegram Ads account.
|
||||
|
||||
## Common errors
|
||||
|
||||
- `ConfigurationError`
|
||||
- `UserNotFoundError` (for recipient/account issues)
|
||||
- `WalletError`
|
||||
- `VerificationError`
|
||||
@@ -1,30 +0,0 @@
|
||||
# Recharge Ads
|
||||
|
||||
Use this method to add funds to your Telegram Ads account.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.recharge_ads(
|
||||
account: str,
|
||||
amount: int,
|
||||
) -> AdsRechargeResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `account`: channel or bot username linked to your ads account
|
||||
- `amount`: integer from `1` to `1_000_000_000`
|
||||
|
||||
**Important:** `amount` must be an integer in the allowed range.
|
||||
|
||||
## Return
|
||||
|
||||
- `AdsRechargeResult(transaction_id, amount)`
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: AdsRechargeResult = await client.recharge_ads("@mychannel", amount=50)
|
||||
print(result.transaction_id)
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
# Top Up GRAM
|
||||
|
||||
Use this method to send GRAM (ex TON) to a user's Telegram balance.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.topup_gram(
|
||||
username: str,
|
||||
amount: int,
|
||||
show_sender: bool = True,
|
||||
) -> AdsTopupResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `username`: recipient Telegram username — `@username`, `username`, or `https://t.me/username`
|
||||
- `amount`: integer from `1` to `1_000_000_000`
|
||||
- `show_sender`: controls sender visibility
|
||||
|
||||
**`amount` must be an integer in the allowed range.**
|
||||
|
||||
## Return
|
||||
|
||||
- `AdsTopupResult(transaction_id, username, amount)`
|
||||
|
||||
## Typical errors
|
||||
|
||||
- `ConfigurationError`: invalid amount
|
||||
- `UserNotFoundError`: recipient not found on Fragment
|
||||
- `WalletError`: insufficient GRAM (ex TON) balance
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: AdsTopupResult = await client.topup_gram("@username", amount=10, show_sender=True)
|
||||
print(result.transaction_id)
|
||||
```
|
||||
@@ -1,26 +0,0 @@
|
||||
# Get Login Code
|
||||
|
||||
Use this method to fetch a pending login code for an anonymous number.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.get_login_code(number: str) -> LoginCodeResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `number`: anonymous number (with or without leading `+`)
|
||||
|
||||
## Return
|
||||
|
||||
- `number`
|
||||
- `code` (`None` if no pending code)
|
||||
- `active_sessions`
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: LoginCodeResult = await client.get_login_code("+1234567890")
|
||||
print(result.code)
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
# Anonymous Numbers Overview
|
||||
|
||||
These methods help you manage login behavior and active sessions for anonymous numbers owned by your account.
|
||||
|
||||
Available methods:
|
||||
|
||||
- [Get Login Code](get-login-code.md)
|
||||
- [Toggle Login Codes](toggle-login-codes.md)
|
||||
- [Terminate Sessions](terminate-sessions.md)
|
||||
|
||||
## Common errors
|
||||
|
||||
- `AnonymousNumberError.NOT_OWNED`
|
||||
- `AnonymousNumberError.TERMINATE_FAILED`
|
||||
|
||||
**If a number is not owned by your account, requests will fail.**
|
||||
@@ -1,25 +0,0 @@
|
||||
# Terminate Sessions
|
||||
|
||||
Use this method to terminate active sessions for an anonymous number.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.terminate_sessions(number: str) -> TerminateSessionsResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `number`: anonymous number (with or without leading `+`)
|
||||
|
||||
## Return
|
||||
|
||||
- `number`
|
||||
- `message`
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: TerminateSessionsResult = await client.terminate_sessions("+1234567890")
|
||||
print(result.message)
|
||||
```
|
||||
@@ -1,24 +0,0 @@
|
||||
# Toggle Login Codes
|
||||
|
||||
Use this method to allow or block login code delivery.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.toggle_login_codes(number: str, can_receive: bool) -> None
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `number`: anonymous number (with or without leading `+`)
|
||||
- `can_receive`: `True` to allow codes, `False` to block codes
|
||||
|
||||
## Return
|
||||
|
||||
- `None`
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
await client.toggle_login_codes("+1234567890", can_receive=False)
|
||||
```
|
||||
@@ -1,31 +0,0 @@
|
||||
# Marketplace Overview
|
||||
|
||||
Marketplace methods are exposed directly on `FragmentClient` and via `client.marketplace` service.
|
||||
|
||||
If you only need one thing: pick the method by asset type (username, number, gift), then paginate until `next_offset_id` or `next_offset` becomes `None`.
|
||||
|
||||
Available methods:
|
||||
|
||||
- [Search Usernames](search-usernames.md)
|
||||
- [Search Numbers](search-numbers.md)
|
||||
- [Search Gifts](search-gifts.md)
|
||||
|
||||
## Shared behavior
|
||||
|
||||
- All methods are async.
|
||||
- All methods call Fragment `searchAuctions` under the hood.
|
||||
- `sort` and `filter` are optional passthrough strings.
|
||||
|
||||
**These values are passed to Fragment as-is.** If Fragment changes accepted values, behavior can change too.
|
||||
|
||||
Common values used by Fragment pages:
|
||||
|
||||
- `sort`: `price_desc`, `price_asc`, `listed`, `ending`
|
||||
- `filter`: empty string, `auction`, `sale`, `sold`
|
||||
|
||||
## Pagination model
|
||||
|
||||
- Usernames and Numbers return `next_offset_id` (string)
|
||||
- Gifts return `next_offset` (integer)
|
||||
|
||||
Use these fields to request next pages.
|
||||
@@ -1,87 +0,0 @@
|
||||
# Search Gifts
|
||||
|
||||
This endpoint is the most flexible marketplace search and supports collection, traits, and pagination.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.search_gifts(
|
||||
query: str = "",
|
||||
collection: str | None = None,
|
||||
sort: str | None = None,
|
||||
filter: str | None = None,
|
||||
view: str | None = None,
|
||||
attr: dict[str, list[str]] | None = None,
|
||||
offset: int | None = None,
|
||||
) -> GiftsResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `query`: search text (empty string for broad listing)
|
||||
- `collection`: collection slug (for example `plushpepe`, `swisswatch`)
|
||||
- `sort`: optional sort key passed to Fragment
|
||||
- `filter`: optional listing filter passed to Fragment
|
||||
- `view`: optional UI/view mode passed to Fragment
|
||||
- `attr`: optional trait filters where key is trait name and value is list of allowed values
|
||||
- `offset`: page offset for next page
|
||||
|
||||
**`attr` is ideal for narrowing results by visual or rarity traits.**
|
||||
|
||||
## Sorting values
|
||||
|
||||
Common values accepted by Fragment:
|
||||
|
||||
- `price_desc`
|
||||
- `price_asc`
|
||||
- `listed`
|
||||
- `ending`
|
||||
|
||||
## Filter values
|
||||
|
||||
Common values accepted by Fragment:
|
||||
|
||||
- empty string
|
||||
- `auction`
|
||||
- `sale`
|
||||
- `sold`
|
||||
|
||||
## Attribute filter format
|
||||
|
||||
`attr` is encoded into request fields in this form:
|
||||
|
||||
- `attr[trait_name] = ["value1", "value2"]`
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
attr={
|
||||
"model": ["gold", "silver"],
|
||||
"rarity": ["rare"],
|
||||
}
|
||||
```
|
||||
|
||||
In requests, each trait is sent as `attr[trait]` with a list of values.
|
||||
|
||||
## Return type
|
||||
|
||||
`GiftsResult` contains:
|
||||
|
||||
- `items: list[dict[str, Any]]`
|
||||
- `next_offset: int | None`
|
||||
|
||||
## Pagination
|
||||
|
||||
If `next_offset` is not `None`, pass it back as `offset` to load the next page.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: GiftsResult = await client.search_gifts(
|
||||
query="",
|
||||
collection="plushpepe",
|
||||
sort="price_desc",
|
||||
filter="auction",
|
||||
)
|
||||
print(len(result.items), result.next_offset)
|
||||
```
|
||||
@@ -1,61 +0,0 @@
|
||||
# Search Numbers
|
||||
|
||||
Use this endpoint to search anonymous Telegram number listings.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.search_numbers(
|
||||
query: str = "",
|
||||
sort: str | None = None,
|
||||
filter: str | None = None,
|
||||
offset_id: str | None = None,
|
||||
) -> NumbersResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `query`: digits or text to match number listings
|
||||
- `sort`: optional sort key passed to Fragment
|
||||
- `filter`: optional listing filter passed to Fragment
|
||||
- `offset_id`: page cursor for next page
|
||||
|
||||
`query` can be partial digits (for example `"888"`) when you need pattern-based discovery.
|
||||
|
||||
## Sorting values
|
||||
|
||||
Common values accepted by Fragment:
|
||||
|
||||
- `price_desc`
|
||||
- `price_asc`
|
||||
- `listed`
|
||||
- `ending`
|
||||
|
||||
## Filter values
|
||||
|
||||
Common values accepted by Fragment:
|
||||
|
||||
- empty string
|
||||
- `auction`
|
||||
- `sale`
|
||||
- `sold`
|
||||
|
||||
## Return type
|
||||
|
||||
`NumbersResult` contains:
|
||||
|
||||
- `items: list[dict[str, Any]]`
|
||||
- `next_offset_id: str | None`
|
||||
|
||||
## Pagination
|
||||
|
||||
If `next_offset_id` is not `None`, pass it back as `offset_id` to load the next page.
|
||||
|
||||
Keep requesting pages until `next_offset_id` becomes `None`.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: NumbersResult = await client.search_numbers("888", sort="price_asc", filter="sale")
|
||||
print(len(result.items), result.next_offset_id)
|
||||
```
|
||||
@@ -1,61 +0,0 @@
|
||||
# Search Usernames
|
||||
|
||||
Use this endpoint to discover Telegram usernames listed on Fragment.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.search_usernames(
|
||||
query: str = "",
|
||||
sort: str | None = None,
|
||||
filter: str | None = None,
|
||||
offset_id: str | None = None,
|
||||
) -> UsernamesResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `query`: search text (empty string means broad listing)
|
||||
- `sort`: optional sort key passed to Fragment
|
||||
- `filter`: optional listing filter passed to Fragment
|
||||
- `offset_id`: page cursor for next page
|
||||
|
||||
For broad browsing, use empty `query` and set sorting only.
|
||||
|
||||
## Sorting values
|
||||
|
||||
Common values accepted by Fragment:
|
||||
|
||||
- `price_desc`
|
||||
- `price_asc`
|
||||
- `listed`
|
||||
- `ending`
|
||||
|
||||
## Filter values
|
||||
|
||||
Common values accepted by Fragment:
|
||||
|
||||
- empty string
|
||||
- `auction`
|
||||
- `sale`
|
||||
- `sold`
|
||||
|
||||
## Return type
|
||||
|
||||
`UsernamesResult` contains:
|
||||
|
||||
- `items: list[dict[str, Any]]`
|
||||
- `next_offset_id: str | None`
|
||||
|
||||
## Pagination
|
||||
|
||||
If `next_offset_id` is not `None`, pass it back as `offset_id` to load the next page.
|
||||
|
||||
This is cursor pagination, so do not try to calculate offsets manually.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: UsernamesResult = await client.search_usernames("durov", sort="price_desc", filter="auction")
|
||||
print(len(result.items), result.next_offset_id)
|
||||
```
|
||||
@@ -1,43 +0,0 @@
|
||||
# Client Overview
|
||||
|
||||
`FragmentClient` is the main API surface.
|
||||
|
||||
You can call methods directly on the client or use grouped services.
|
||||
|
||||
Grouped service wrappers:
|
||||
|
||||
- `client.purchases`
|
||||
- `client.giveaways`
|
||||
- `client.ads`
|
||||
- `client.anonymous_numbers`
|
||||
- `client.marketplace`
|
||||
- `client.tonapi`
|
||||
|
||||
Main async methods on `FragmentClient`:
|
||||
|
||||
- `purchase_stars(...)`
|
||||
- `purchase_premium(...)`
|
||||
- `giveaway_stars(...)`
|
||||
- `giveaway_premium(...)`
|
||||
- `topup_gram(...)`
|
||||
- `recharge_ads(...)`
|
||||
- `get_wallet()`
|
||||
- `get_login_code(...)`
|
||||
- `toggle_login_codes(...)`
|
||||
- `terminate_sessions(...)`
|
||||
- `search_usernames(...)`
|
||||
- `search_numbers(...)`
|
||||
- `search_gifts(...)`
|
||||
- `call(...)`
|
||||
|
||||
All methods are async and should be used inside `async with FragmentClient(...) as client:`.
|
||||
|
||||
## Flow map
|
||||
|
||||
- Stars: [Purchase](stars/purchase.md), [Giveaway](stars/giveaway.md)
|
||||
- Premium: [Purchase](premium/purchase.md), [Giveaway](premium/giveaway.md)
|
||||
- Marketplace: [Overview](marketplace/overview.md), Ads: [Overview](ads/overview.md)
|
||||
- Numbers: [Anonymous Numbers](anonymous-numbers/overview.md)
|
||||
- Utility operations: [Raw API Calls](raw-call.md)
|
||||
|
||||
**If you are new to the library, start with Stars Purchase or Wallet read (`get_wallet`) first.**
|
||||
@@ -1,41 +0,0 @@
|
||||
# Premium Giveaway
|
||||
|
||||
Use this method to run a Telegram Premium giveaway for your channel.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.giveaway_premium(
|
||||
channel: str,
|
||||
winners: int,
|
||||
months: int = 3,
|
||||
payment_method: PaymentMethod = PaymentMethod.GRAM,
|
||||
) -> PremiumGiveawayResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `channel`: accepts `@channel`, `channel`, or `https://t.me/channel`
|
||||
- `winners`: integer from `1` to `24_000`
|
||||
- `months`: one of `3`, `6`, `12`
|
||||
- `payment_method`: `PaymentMethod.GRAM` (default), `PaymentMethod.USDT_GRAM`, or any other `PaymentMethod` value
|
||||
|
||||
**`winners` must be a positive integer, and large values can increase total cost significantly.**
|
||||
|
||||
## Return
|
||||
|
||||
- `PremiumGiveawayResult(transaction_id, channel, winners, amount)`
|
||||
|
||||
## Typical errors
|
||||
|
||||
- `ConfigurationError`
|
||||
- `UserNotFoundError`
|
||||
- `WalletError`
|
||||
- `VerificationError`
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: PremiumGiveawayResult = await client.giveaway_premium("@channel", winners=100, months=3)
|
||||
print(result.amount)
|
||||
```
|
||||
@@ -1,41 +0,0 @@
|
||||
# Premium Purchase
|
||||
|
||||
Use this method to gift Telegram Premium to a specific user.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.purchase_premium(
|
||||
username: str,
|
||||
months: int,
|
||||
show_sender: bool = True,
|
||||
payment_method: PaymentMethod = PaymentMethod.GRAM,
|
||||
) -> PremiumResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `username`: accepts `@username`, `username`, or `https://t.me/username`
|
||||
- `months`: one of `3`, `6`, `12`
|
||||
- `show_sender`: controls sender visibility on recipient side
|
||||
- `payment_method`: `PaymentMethod.GRAM` (default), `PaymentMethod.USDT_GRAM`, or any other `PaymentMethod` value
|
||||
|
||||
**`months` only supports `3`, `6`, or `12`.**
|
||||
|
||||
## Return
|
||||
|
||||
- `PremiumResult(transaction_id, username, amount)`
|
||||
|
||||
## Typical errors
|
||||
|
||||
- `ConfigurationError`
|
||||
- `UserNotFoundError`
|
||||
- `WalletError`
|
||||
- `VerificationError`
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: PremiumResult = await client.purchase_premium("@username", months=6, payment_method=PaymentMethod.GRAM)
|
||||
print(result.transaction_id)
|
||||
```
|
||||
@@ -1,42 +0,0 @@
|
||||
# Raw API Calls
|
||||
|
||||
Use `client.call()` when you need a Fragment API method that does not yet have a dedicated wrapper.
|
||||
|
||||
```python
|
||||
result = await client.call(
|
||||
"searchPremiumGiftRecipient",
|
||||
{"query": "@username", "months": 3},
|
||||
page_url="https://fragment.com/premium/gift",
|
||||
)
|
||||
```
|
||||
|
||||
Signature:
|
||||
|
||||
```python
|
||||
await client.call(
|
||||
method: str,
|
||||
data: dict[str, Any] | None = None,
|
||||
*,
|
||||
page_url: str = "https://fragment.com",
|
||||
) -> dict[str, Any]
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `method`: Fragment API method name
|
||||
- `data`: optional request payload as dictionary
|
||||
- `page_url`: page URL used for referer/hash context (defaults to `https://fragment.com`)
|
||||
|
||||
## Return
|
||||
|
||||
- `dict[str, Any]`: raw Fragment API response
|
||||
|
||||
Use this carefully:
|
||||
|
||||
- request/response shape is Fragment-defined,
|
||||
- undocumented methods can change without notice,
|
||||
- you are responsible for validating returned fields.
|
||||
|
||||
## Recommended approach
|
||||
|
||||
Use dedicated wrappers first, and fallback to `call()` only for missing API surface.
|
||||
@@ -1,41 +0,0 @@
|
||||
# Stars Giveaway
|
||||
|
||||
Use this method to run a Stars giveaway for a channel audience.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.giveaway_stars(
|
||||
channel: str,
|
||||
winners: int,
|
||||
amount: int,
|
||||
payment_method: PaymentMethod = PaymentMethod.GRAM,
|
||||
) -> StarsGiveawayResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `channel`: accepts `@channel`, `channel`, or `https://t.me/channel`
|
||||
- `winners`: integer from `1` to `15`
|
||||
- `amount`: integer from `500` to `1_000_000` (per winner)
|
||||
- `payment_method`: `PaymentMethod.GRAM` (default), `PaymentMethod.USDT_GRAM`, or any other `PaymentMethod` value
|
||||
|
||||
**Each winner receives the full `amount` value.**
|
||||
|
||||
## Return
|
||||
|
||||
- `StarsGiveawayResult(transaction_id, channel, winners, amount)`
|
||||
|
||||
## Typical errors
|
||||
|
||||
- `ConfigurationError`
|
||||
- `UserNotFoundError`
|
||||
- `WalletError`
|
||||
- `VerificationError`
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: StarsGiveawayResult = await client.giveaway_stars("@channel", winners=3, amount=1000)
|
||||
print(result.transaction_id)
|
||||
```
|
||||
@@ -1,41 +0,0 @@
|
||||
# Stars Purchase
|
||||
|
||||
Use this method to send Telegram Stars directly to a user.
|
||||
|
||||
## Method
|
||||
|
||||
```python
|
||||
await client.purchase_stars(
|
||||
username: str,
|
||||
amount: int,
|
||||
show_sender: bool = True,
|
||||
payment_method: PaymentMethod = PaymentMethod.GRAM,
|
||||
) -> StarsResult
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `username`: accepts `@username`, `username`, or `https://t.me/username`
|
||||
- `amount`: integer from `50` to `10_000_000`
|
||||
- `show_sender`: controls sender visibility on recipient side
|
||||
- `payment_method`: `PaymentMethod.GRAM` (default), `PaymentMethod.USDT_GRAM`, or any other `PaymentMethod` value
|
||||
|
||||
**Amount must be between `50` and `10_000_000`.**
|
||||
|
||||
## Return
|
||||
|
||||
- `StarsResult(transaction_id, username, amount)`
|
||||
|
||||
## Typical errors
|
||||
|
||||
- `ConfigurationError`: invalid amount or payment method
|
||||
- `UserNotFoundError`: target user not found
|
||||
- `WalletError`: insufficient balance or wallet-side issue
|
||||
- `VerificationError`: verification/KYC required for operation
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
result: StarsResult = await client.purchase_stars("@username", amount=500, payment_method=PaymentMethod.GRAM)
|
||||
print(result.amount)
|
||||
```
|
||||
@@ -1,78 +0,0 @@
|
||||
# Library and Configuration
|
||||
|
||||
Main entry point of the library is `FragmentClient`.
|
||||
|
||||
```python
|
||||
FragmentClient(
|
||||
seed: str,
|
||||
api_key: str,
|
||||
cookies: dict[str, Any] | str,
|
||||
wallet_version: str = "V5R1",
|
||||
api_provider: str = "tonapi",
|
||||
timeout: float = 30.0,
|
||||
)
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `seed`: wallet mnemonic (**12 or 24 words**)
|
||||
- `api_key`: API key — from [tonconsole.com](https://tonconsole.com) (tonapi) or [@toncenter](https://t.me/toncenter)
|
||||
- `cookies`: Fragment cookies as a dictionary or JSON string
|
||||
- `wallet_version`: `"V4R2"`, `"V5R1"`, `"HighloadV2"`, or `"HighloadV3R1"`
|
||||
- `api_provider`: blockchain API provider — `"tonapi"` (default) or `"toncenter"`
|
||||
- `timeout`: request timeout in seconds
|
||||
|
||||
**If `api_key` or cookies are missing, initialization fails immediately.**
|
||||
|
||||
## Required cookies
|
||||
|
||||
- `stel_ssid`
|
||||
- `stel_dt`
|
||||
- `stel_token`
|
||||
- `stel_ton_token`
|
||||
|
||||
## Minimal initialization pattern
|
||||
|
||||
```python
|
||||
from pyfragment import FragmentClient
|
||||
|
||||
async with FragmentClient(
|
||||
seed="word1 word2 ... word24",
|
||||
api_key="YOUR_API_KEY",
|
||||
cookies={
|
||||
"stel_ssid": "...",
|
||||
"stel_dt": "...",
|
||||
"stel_token": "...",
|
||||
"stel_ton_token": "...",
|
||||
},
|
||||
) as client:
|
||||
wallet = await client.get_wallet()
|
||||
```
|
||||
|
||||
## Switching API provider
|
||||
|
||||
By default, the library uses [tonconsole.com](https://tonconsole.com) (tonapi). To use [toncenter](https://t.me/toncenter) instead, pass `api_provider="toncenter"`:
|
||||
|
||||
```python
|
||||
async with FragmentClient(
|
||||
seed="...",
|
||||
api_key="YOUR_TONCENTER_API_KEY",
|
||||
cookies={...},
|
||||
api_provider="toncenter",
|
||||
) as client:
|
||||
...
|
||||
```
|
||||
|
||||
Both providers work identically — the correct `tonutils` client is selected automatically based on `api_provider`.
|
||||
|
||||
## Validation behavior
|
||||
|
||||
At initialization, library validates:
|
||||
|
||||
- seed format,
|
||||
- cookie shape and required keys,
|
||||
- supported wallet version,
|
||||
- supported API provider,
|
||||
- parseability of cookie JSON strings.
|
||||
|
||||
Constructor-level issues are raised as `ConfigurationError` or `CookieError`.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Credentials and Cookies
|
||||
|
||||
This page covers the three things you need before making real requests: Tonapi key, wallet seed, and Fragment cookies.
|
||||
|
||||
## Tonapi key
|
||||
|
||||
Generate an API key at https://tonconsole.com.
|
||||
|
||||
## Seed phrase
|
||||
|
||||
Use your GRAM (ex TON) wallet mnemonic.
|
||||
|
||||
- **Keep it private.**
|
||||
- **Never log it or commit it to git.**
|
||||
|
||||
## Fragment cookies
|
||||
|
||||
You must be logged in to Fragment.
|
||||
|
||||
### Option 1: automatic extraction
|
||||
|
||||
```python
|
||||
from pyfragment import get_cookies_from_browser
|
||||
|
||||
cookie_result = get_cookies_from_browser("chrome")
|
||||
cookies = cookie_result.cookies
|
||||
```
|
||||
|
||||
`cookie_result` is `CookieResult`:
|
||||
|
||||
- `cookies`: `dict[str, str]`
|
||||
- `expires`: ISO string or `None`
|
||||
|
||||
### Option 2: manual export
|
||||
|
||||
Export the four required Fragment cookies and pass them directly as dict or JSON string.
|
||||
|
||||
Required keys:
|
||||
|
||||
- `stel_ssid`
|
||||
- `stel_dt`
|
||||
- `stel_token`
|
||||
- `stel_ton_token`
|
||||
|
||||
## Common auth failures
|
||||
|
||||
- expired session cookies,
|
||||
- not logged in on fragment.com,
|
||||
- missing `stel_*` keys,
|
||||
- stale cookies from another browser/profile.
|
||||
|
||||
When this happens, re-login on fragment.com and refresh cookies first. It solves most auth issues.
|
||||
|
||||
## Next step
|
||||
|
||||
Proceed to [Quick Start](quickstart.md).
|
||||
@@ -1,35 +0,0 @@
|
||||
# Installation
|
||||
|
||||
You can be up and running in under a minute.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.11 – 3.14
|
||||
|
||||
## Install from PyPI
|
||||
|
||||
```bash
|
||||
pip install pyfragment
|
||||
```
|
||||
|
||||
## Install latest dev branch
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/bohd4nx/pyfragment.git@dev
|
||||
```
|
||||
|
||||
## Optional browser cookie extraction support
|
||||
|
||||
If you want automatic cookie extraction from local browser profiles:
|
||||
|
||||
```bash
|
||||
pip install "pyfragment[browser]"
|
||||
```
|
||||
|
||||
This installs `rookiepy`, used by `get_cookies_from_browser()`.
|
||||
|
||||
**Use this extra if you do not want to copy cookies manually.**
|
||||
|
||||
## Next step
|
||||
|
||||
After installation, continue with [Library and Configuration](configuration.md).
|
||||
@@ -1,48 +0,0 @@
|
||||
# Quick Start
|
||||
|
||||
Use this minimal example to verify that your credentials, cookies, and wallet setup are correct.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from pyfragment import FragmentClient
|
||||
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"
|
||||
) as client:
|
||||
wallet = await client.get_wallet()
|
||||
print("GRAM: %s | USDT: %s" % (wallet.gram_balance, wallet.usdt_balance))
|
||||
|
||||
recipient = "https://t.me/username" # also: @username, username
|
||||
|
||||
stars = await client.purchase_stars(recipient, amount=500, payment_method=PaymentMethod.USDT_GRAM)
|
||||
print("Sent %s Stars to %s | tx: %s" % (stars.amount, stars.username, stars.transaction_id))
|
||||
|
||||
premium = await client.purchase_premium(recipient, months=6, payment_method=PaymentMethod.GRAM)
|
||||
print("Sent Premium %sm to %s | tx: %s" % (premium.amount, premium.username, premium.transaction_id))
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
If this script returns wallet data, your setup is healthy.
|
||||
|
||||
Then move to feature pages:
|
||||
|
||||
- Stars: [Purchase](client/stars/purchase.md), [Giveaway](client/stars/giveaway.md)
|
||||
- Premium: [Purchase](client/premium/purchase.md), [Giveaway](client/premium/giveaway.md)
|
||||
- Marketplace: [Overview](client/marketplace/overview.md), Ads: [Overview](client/ads/overview.md)
|
||||
- Numbers: [Anonymous Numbers](client/anonymous-numbers/overview.md)
|
||||
- Utility operations: [Raw API Calls](client/raw-call.md)
|
||||
@@ -1,56 +0,0 @@
|
||||
# Error Handling
|
||||
|
||||
Good error handling is the difference between a stable integration and random production failures.
|
||||
|
||||
## Exception hierarchy
|
||||
|
||||
- `FragmentError`
|
||||
- `ClientError`
|
||||
- `ConfigurationError`
|
||||
- `CookieError`
|
||||
- `FragmentAPIError`
|
||||
- `FragmentPageError`
|
||||
- `UserNotFoundError`
|
||||
- `AlreadySubscribedError`
|
||||
- `AnonymousNumberError`
|
||||
- `TransactionError`
|
||||
- `ParseError`
|
||||
- `VerificationError`
|
||||
- `OperationError`
|
||||
- `WalletError`
|
||||
- `UnexpectedError`
|
||||
|
||||
## Recommended handling pattern
|
||||
|
||||
```python
|
||||
from pyfragment import ConfigurationError, FragmentError, UserNotFoundError, WalletError
|
||||
|
||||
try:
|
||||
result = await client.purchase_stars("@username", amount=500)
|
||||
except UserNotFoundError:
|
||||
# recipient does not exist on Fragment
|
||||
...
|
||||
except WalletError:
|
||||
# insufficient balance or wallet-side issue
|
||||
...
|
||||
except ConfigurationError:
|
||||
# invalid local input
|
||||
...
|
||||
except FragmentError:
|
||||
# any other library-level failure
|
||||
...
|
||||
```
|
||||
|
||||
**Catch specific errors first, then fallback to `FragmentError`.**
|
||||
|
||||
## Method-to-error mapping
|
||||
|
||||
- Stars purchase: `ConfigurationError`, `UserNotFoundError`, `WalletError`, `VerificationError`
|
||||
- Premium purchase: `ConfigurationError`, `UserNotFoundError`, `AlreadySubscribedError`, `WalletError`, `VerificationError`
|
||||
- Stars/Premium giveaway: `ConfigurationError`, `UserNotFoundError`, `WalletError`, `VerificationError`
|
||||
- Ads operations: `ConfigurationError`, `UserNotFoundError`, `WalletError`, `VerificationError`
|
||||
- Cookies/auth setup: `CookieError`, `ConfigurationError`, `FragmentPageError`
|
||||
|
||||
## Canonical messages
|
||||
|
||||
See `pyfragment/exceptions.py` for source-of-truth message templates.
|
||||
@@ -1,54 +0,0 @@
|
||||
# Literal Types
|
||||
|
||||
These literals describe accepted string values for key method parameters.
|
||||
|
||||
## ApiProvider
|
||||
|
||||
```python
|
||||
from pyfragment.enums import ApiProvider
|
||||
|
||||
ApiProvider.TONAPI # tonconsole.com — default
|
||||
ApiProvider.TONCENTER # t.me/toncenter
|
||||
```
|
||||
|
||||
Pass as string to `FragmentClient(api_provider=...)`:
|
||||
|
||||
```python
|
||||
FragmentClient(..., api_provider="tonapi") # default
|
||||
FragmentClient(..., api_provider="toncenter")
|
||||
```
|
||||
|
||||
## PaymentMethod
|
||||
|
||||
```python
|
||||
from pyfragment.enums import PaymentMethod
|
||||
|
||||
PaymentMethod.GRAM # GRAM (ex TON) — default
|
||||
PaymentMethod.USDT_GRAM # USDT on GRAM (ex TON)
|
||||
PaymentMethod.USDT_ETH # USDT on Ethereum
|
||||
PaymentMethod.USDT_POL # USDT on Polygon
|
||||
PaymentMethod.USDC_ETH # USDC on Ethereum
|
||||
PaymentMethod.USDC_BASE # USDC on Base
|
||||
PaymentMethod.USDC_POL # USDC on Polygon
|
||||
```
|
||||
|
||||
## WalletVersion
|
||||
|
||||
```python
|
||||
from pyfragment.enums import WalletVersion
|
||||
|
||||
WalletVersion.V5R1 # default
|
||||
WalletVersion.V4R2
|
||||
WalletVersion.HighloadV2
|
||||
WalletVersion.HighloadV3R1
|
||||
```
|
||||
|
||||
All enums are exported from both `pyfragment` (top-level) and `pyfragment.enums`.
|
||||
|
||||
## Usage notes
|
||||
|
||||
- Use `ApiProvider` when configuring the blockchain API provider in `FragmentClient`.
|
||||
- Use `PaymentMethod` for purchase and giveaway operations.
|
||||
- Use `WalletVersion` when configuring `FragmentClient`.
|
||||
|
||||
**Passing unsupported values raises `ConfigurationError`.**
|
||||
@@ -1,47 +0,0 @@
|
||||
# Result Models
|
||||
|
||||
Every high-level method returns a typed model, so you can rely on predictable fields instead of raw payload parsing.
|
||||
|
||||
Exported result models:
|
||||
|
||||
- `CookieResult(cookies, expires)`
|
||||
- `StarsResult(transaction_id, username, amount)`
|
||||
- `PremiumResult(transaction_id, username, amount)`
|
||||
- `AdsTopupResult(transaction_id, username, amount)`
|
||||
- `AdsRechargeResult(transaction_id, amount)`
|
||||
- `StarsGiveawayResult(transaction_id, channel, winners, amount)`
|
||||
- `PremiumGiveawayResult(transaction_id, channel, winners, amount)`
|
||||
- `WalletInfo(address, state, gram_balance, usdt_balance)`
|
||||
- `LoginCodeResult(number, code, active_sessions)`
|
||||
- `TerminateSessionsResult(number, message)`
|
||||
- `UsernamesResult(items, next_offset_id)`
|
||||
- `NumbersResult(items, next_offset_id)`
|
||||
- `GiftsResult(items, next_offset)`
|
||||
|
||||
Most high-level methods return one of these dataclasses.
|
||||
|
||||
## Where they are used
|
||||
|
||||
- `purchase_stars()`: `StarsResult`
|
||||
- `purchase_premium()`: `PremiumResult`
|
||||
- `giveaway_stars()`: `StarsGiveawayResult`
|
||||
- `giveaway_premium()`: `PremiumGiveawayResult`
|
||||
- `topup_gram()`: `AdsTopupResult`
|
||||
- `recharge_ads()`: `AdsRechargeResult`
|
||||
- `get_wallet()`: `WalletInfo`
|
||||
- `get_login_code()`: `LoginCodeResult`
|
||||
- `terminate_sessions()`: `TerminateSessionsResult`
|
||||
- `search_usernames()`: `UsernamesResult`
|
||||
- `search_numbers()`: `NumbersResult`
|
||||
- `search_gifts()`: `GiftsResult`
|
||||
|
||||
## Methods without dataclass return
|
||||
|
||||
- `toggle_login_codes()`: returns `None`
|
||||
- `call()`: returns `dict[str, Any]` (raw Fragment API response)
|
||||
|
||||
## Cookie helper
|
||||
|
||||
`CookieResult` is returned by `get_cookies_from_browser()`, not by `FragmentClient` methods.
|
||||
|
||||
**Use these models directly in your app layer and avoid passing raw dictionaries around.**
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" width="512" height="512" style="width: 100%; height: 100%; transform: translate3d(0,0,0); content-visibility: visible;" preserveAspectRatio="xMidYMid meet"><defs><clipPath id="__lottie_element_2"><rect width="512" height="512" x="0" y="0"></rect></clipPath><clipPath id="__lottie_element_4"><path d="M0,0 L100,0 L100,100 L0,100z"></path></clipPath></defs><g clip-path="url(#__lottie_element_2)"><g clip-path="url(#__lottie_element_4)" style="display: block;" transform="matrix(5.119999885559082,0,0,5.119999885559082,0,0)" opacity="1"><g style="display: block;" transform="matrix(1.333299994468689,0,0,1.333299994468689,0,0)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill="rgb(30,40,51)" fill-opacity="1" d=" M47.31999969482422,5.619999885559082 C47.31999969482422,5.619999885559082 27.68000030517578,5.619999885559082 27.68000030517578,5.619999885559082 C15.5,5.619999885559082 5.619999885559082,15.5 5.619999885559082,27.68000030517578 C5.619999885559082,27.68000030517578 5.619999885559082,47.31999969482422 5.619999885559082,47.31999969482422 C5.619999885559082,59.5 15.5,69.37999725341797 27.68000030517578,69.37999725341797 C27.68000030517578,69.37999725341797 47.31999969482422,69.37999725341797 47.31999969482422,69.37999725341797 C59.5,69.37999725341797 69.37999725341797,59.5 69.37999725341797,47.31999969482422 C69.37999725341797,47.31999969482422 69.37999725341797,27.68000030517578 69.37999725341797,27.68000030517578 C69.37999725341797,15.5 59.5,5.619999885559082 47.31999969482422,5.619999885559082 C47.31999969482422,5.619999885559082 47.31999969482422,5.619999885559082 47.31999969482422,5.619999885559082z"></path></g><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill="rgb(255,255,255)" fill-opacity="1" d=" M36.349998474121094,32.79999923706055 C36.349998474121094,32.79999923706055 21.1299991607666,25.940000534057617 21.1299991607666,25.940000534057617 C20.020000457763672,25.450000762939453 20.389999389648438,23.790000915527344 21.600000381469727,23.790000915527344 C21.600000381469727,23.790000915527344 53.41999816894531,23.790000915527344 53.41999816894531,23.790000915527344 C54.630001068115234,23.790000915527344 54.9900016784668,25.440000534057617 53.880001068115234,25.940000534057617 C53.880001068115234,25.940000534057617 38.66999816894531,32.79999923706055 38.66999816894531,32.79999923706055 C37.939998626708984,33.130001068115234 37.09000015258789,33.130001068115234 36.36000061035156,32.79999923706055 C36.36000061035156,32.79999923706055 36.349998474121094,32.79999923706055 36.349998474121094,32.79999923706055z M56.81999969482422,30.06999969482422 C57.43000030517578,29.1200008392334 56.43000030517578,27.979999542236328 55.400001525878906,28.440000534057617 C55.400001525878906,28.440000534057617 40.72999954223633,35.13999938964844 40.72999954223633,35.13999938964844 C39.72999954223633,35.599998474121094 39.09000015258789,36.61000061035156 39.09000015258789,37.70000076293945 C39.09000015258789,37.70000076293945 39.09000015258789,53.810001373291016 39.09000015258789,53.810001373291016 C39.09000015258789,54.93000030517578 40.54999923706055,55.36000061035156 41.15999984741211,54.41999816894531 C41.15999984741211,54.41999816894531 56.810001373291016,30.06999969482422 56.810001373291016,30.06999969482422 C56.810001373291016,30.06999969482422 56.81999969482422,30.06999969482422 56.81999969482422,30.06999969482422z M19.600000381469727,28.440000534057617 C18.579999923706055,27.979999542236328 17.56999969482422,29.1200008392334 18.18000030517578,30.06999969482422 C18.18000030517578,30.06999969482422 33.84000015258789,54.43000030517578 33.84000015258789,54.43000030517578 C34.45000076293945,55.380001068115234 35.90999984741211,54.939998626708984 35.90999984741211,53.81999969482422 C35.90999984741211,53.81999969482422 35.90999984741211,37.70000076293945 35.90999984741211,37.70000076293945 C35.90999984741211,36.599998474121094 35.27000045776367,35.599998474121094 34.27000045776367,35.13999938964844 C34.27000045776367,35.13999938964844 19.59000015258789,28.450000762939453 19.59000015258789,28.450000762939453 C19.59000015258789,28.450000762939453 19.600000381469727,28.440000534057617 19.600000381469727,28.440000534057617z"></path></g></g></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,66 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from app.core import setup_logging
|
||||
from app.methods import buy_premium, buy_stars, topup_ton
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def topup_ton_example():
|
||||
logger.info("Starting TON topup example")
|
||||
|
||||
# @bohd4nx - target username, 100 - TON amount (integer 1-1000000000 (one billion))
|
||||
# show_sender=True — recipient sees who sent the topup
|
||||
result = await topup_ton("@bohd4nx", 100, show_sender=True)
|
||||
|
||||
if result["success"]:
|
||||
pass # Transaction successful, details are logged in the method
|
||||
else:
|
||||
logger.error(f"TON topup failed: {result['error']}")
|
||||
|
||||
|
||||
async def buy_premium_example():
|
||||
logger.info("Starting Premium purchase example")
|
||||
|
||||
# @bohd4nx - target username, 12 - months duration (3, 6, or 12 only)
|
||||
# show_sender=True — recipient sees who gifted the Premium
|
||||
result = await buy_premium("@bohd4nx", 12, show_sender=True)
|
||||
|
||||
if result["success"]:
|
||||
pass # Transaction successful, details are logged in the method
|
||||
else:
|
||||
logger.error(f"Premium purchase failed: {result['error']}")
|
||||
|
||||
|
||||
async def buy_stars_example():
|
||||
logger.info("Starting Stars purchase example")
|
||||
|
||||
# @bohd4nx - target username, 1000000 - stars amount (integer 50-1000000 (one million))
|
||||
# show_sender=True — recipient sees who sent the Stars
|
||||
result = await buy_stars("@bohd4nx", 1000000, show_sender=True)
|
||||
|
||||
if result["success"]:
|
||||
pass # Transaction successful, details are logged in the method
|
||||
else:
|
||||
logger.error(f"Stars purchase failed: {result['error']}")
|
||||
|
||||
|
||||
async def main():
|
||||
setup_logging()
|
||||
logger.info("Starting Fragment API by @bohd4nx - examples")
|
||||
|
||||
await topup_ton_example()
|
||||
await buy_premium_example()
|
||||
await buy_stars_example()
|
||||
|
||||
logger.info("All examples completed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("Fragment API by @bohd4nx - Usage Examples")
|
||||
logger.info("Supported username formats: @username, username")
|
||||
logger.info("Limits: TON minimum 1, Premium 3/6/12 months, Stars minimum 50")
|
||||
logger.info("Setup: Copy .env.example to .env and fill all fields")
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,19 @@
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["[0-9][0-9][0-9]_test_*.py"]
|
||||
asyncio_mode = "auto"
|
||||
addopts = "-v --tb=short"
|
||||
|
||||
[tool.black]
|
||||
line-length = 128
|
||||
target-version = ["py312"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py312"
|
||||
line-length = 128
|
||||
|
||||
[tool.ruff.lint]
|
||||
# E — pycodestyle errors, F — pyflakes, W — warnings, I — isort
|
||||
select = ["E", "F", "W", "I"]
|
||||
# E501 — line too long (covered by line-length above)
|
||||
ignore = ["E501"]
|
||||
@@ -0,0 +1,4 @@
|
||||
python-dotenv==1.2.2
|
||||
asyncio==4.0.0
|
||||
httpx==0.28.1
|
||||
tonutils[pytoniq]==2.0.0
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Tests for clean_decode() — BOC-encoded Fragment payloads decode to
|
||||
human-readable UTF-8 with the Telegram label and Ref# intact."""
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from app.utils.decoder import clean_decode
|
||||
|
||||
PAYLOADS = [
|
||||
pytest.param(
|
||||
"te6ccgEBAgEALwABTgAAAAAxMDAwMDAwIFRlbGVncmFtIFN0YXJzIAoKUmVmI1RQb01wegEABkM3ZQ",
|
||||
id="stars",
|
||||
),
|
||||
pytest.param(
|
||||
"te6ccgEBAgEANAABTgAAAABUZWxlZ3JhbSBQcmVtaXVtIGZvciAxIHllYXIgCgpSZWYjcgEAEE9OQnM2cmNt",
|
||||
id="premium",
|
||||
),
|
||||
pytest.param(
|
||||
"te6ccgEBAgEAMAABTgAAAABUZWxlZ3JhbSBhY2NvdW50IHRvcCB1cCAKClJlZiNrMXpDRQEACFkxd3g",
|
||||
id="topup",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("payload", PAYLOADS)
|
||||
def test_payload(payload: str) -> None:
|
||||
result = clean_decode(payload)
|
||||
assert "Telegram" in result
|
||||
assert re.search(r"Ref#[A-Za-z0-9]+", result), f"no Ref# in {result!r}"
|
||||
assert all(ord(c) <= 127 for c in result), f"non-ASCII chars in {result!r}"
|
||||
|
||||
|
||||
def test_empty_input_returns_string() -> None:
|
||||
assert isinstance(clean_decode(""), str)
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Tests for get_fragment_hash() — fetches a valid lowercase hex hash
|
||||
from the fragment.com/stars/buy page source."""
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.constants import BASE_HEADERS, STARS_PAGE
|
||||
from app.utils.hash import get_fragment_hash
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hash_is_valid_hex(cookies: dict) -> None:
|
||||
result = await get_fragment_hash(cookies, BASE_HEADERS, STARS_PAGE)
|
||||
assert isinstance(result, str)
|
||||
assert len(result) >= 10, f"hash too short: {result!r}"
|
||||
assert re.fullmatch(r"[a-f0-9]+", result), f"not a hex string: {result!r}"
|
||||
@@ -0,0 +1,13 @@
|
||||
import pytest
|
||||
|
||||
from app.core.cookies import load_cookies
|
||||
from app.core.exceptions import CookiesError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cookies():
|
||||
"""Load Fragment cookies; skip the test if they are unavailable."""
|
||||
try:
|
||||
return load_cookies()
|
||||
except CookiesError as exc:
|
||||
pytest.skip(f"Cookies unavailable — {exc}")
|
||||
Reference in New Issue
Block a user