refactor: update API key source in .env.example and enhance README with badges and feature descriptions

feat: improve logging levels for HTTP clients and add detailed logging in premium and stars purchase methods
chore: remove unused transaction utility and adjust wallet processing logic
test: add end-to-end integration test for buying Stars
This commit is contained in:
bohd4nx
2026-03-05 06:26:49 +02:00
parent efcdda6b74
commit 7f3c3ea1c3
9 changed files with 162 additions and 163 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
# TON wallet seed phrase - 12 or 24 words separated by spaces
SEED = "your_ton_wallet_seed_phrase_here"
# TON API key - get from https://t.me/tonapibot
# TON API key - get from https://tonconsole.com
API_KEY = "your_ton_api_key_here"
# TON wallet contract version: V4R2 or V5R1 (default: V5R1)
+102 -80
View File
@@ -7,8 +7,13 @@
<b>Automate TON topups, Telegram Premium purchases, and Stars transactions via Fragment.com</b>
</p>
[Report Bug](https://github.com/bohd4nx/fragmentapi/issues) · [Request Feature](https://github.com/bohd4nx/fragmentapi/issues) · [
**Donate TON**](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5)
[![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=flat&logo=python&logoColor=white)](https://python.org)
[![tonutils](https://img.shields.io/badge/tonutils-2.0.0-0098EA?style=flat&logo=ton&logoColor=white)](https://github.com/nessshon/tonutils)
[![Stars](https://img.shields.io/github/stars/bohd4nx/FragmentAPI?style=flat&color=yellow)](https://github.com/bohd4nx/FragmentAPI/stargazers)
[![Issues](https://img.shields.io/github/issues/bohd4nx/FragmentAPI?style=flat&color=red)](https://github.com/bohd4nx/FragmentAPI/issues)
[![CI](https://img.shields.io/github/actions/workflow/status/bohd4nx/FragmentAPI/tests.yml?style=flat&label=tests&logo=github)](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>
@@ -16,9 +21,10 @@
## ✨ Features
- 💰 **TON Advertisement Topups** - Send TON for advertising campaigns and purchasing gifts (1-1,000,000,000 TON)
- 👑 **Telegram Premium Gifts** - Purchase Premium subscriptions (3, 6, or 12 months)
-**Telegram Stars Purchases** - Buy Stars for users (50-1,000,000 Stars)
- 💰 **TON Advertisement Topups** Send TON directly to Fragment ad accounts (11,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 (501,000,000 Stars)
- 🔐 **Multi-wallet support** — Configurable wallet contract version (V4R2 / V5R1)
## 🚀 Quick Start
@@ -32,88 +38,82 @@ pip install -r requirements.txt
### 2. Configuration
Copy example configuration and edit:
```bash
cp .env.example .env
cp cookies.example.json cookies.json
```
Edit `.env` file:
Edit `.env`:
```env
SEED=word1 word2 word3 ... word24
# 24-word TON wallet seed phrase
SEED = word1 word2 word3 ... word24
API_KEY=your_ton_api_key_here
# 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**: Login to your Telegram account and connect the TON wallet you want to use for payments.
**Prerequisites**: Log in to Telegram on Fragment and connect the TON wallet you'll use for payments.
1. **Install Cookie Editor Extension**:
- Download
from [Chrome Web Store](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm)
- Add extension to your browser
2. **Extract Cookies**:
- Open [Fragment.com](https://fragment.com) and ensure you're logged in
- Refresh the page completely
- Click on the Cookie Editor extension icon
- Click **"Export"** button
- Select **"Header String"** format
- Copy the result and split it into JSON fields in `cookies.json`
**Expected format** (`cookies.json` in project root):
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": "<SSID>",
"stel_dt": "<STEL_DT>",
"stel_token": "<TOKEN>",
"stel_ton_token": "<TON_TOKEN>"
"stel_ssid": "...",
"stel_dt": "...",
"stel_token": "...",
"stel_ton_token": "..."
}
```
#### 🔐 TON Wallet Seed Phrase
**If you don't have a TON wallet yet**:
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`.
1. **Download Tonkeeper**:
- iOS: [App Store](https://apps.apple.com/app/tonkeeper/id1587742107)
- Android: [Google Play](https://play.google.com/store/apps/details?id=com.ton_keeper)
2. **Create New Wallet**:
- Open Tonkeeper app
- Tap **"Create New Wallet"**
- **IMPORTANT**: Write down your 24-word seed phrase on paper
- Store it securely - never share with anyone!
- Complete wallet setup
3. **Get Your Seed Phrase**:
- If you already have a wallet, go to Settings → Backup
- Enter your passcode
- Copy the 24 words → paste to `SEED` in your `.env` file
**Format**: `word1 word2 word3 ... word24`
#### 🔗 Fragment Hash
Hash is fetched automatically from Fragment pages at runtime. You no longer need to add `HASH` to `.env`.
> ⚠️ Never share your seed phrase with anyone. Store it offline.
#### 🔑 TON API Key
1. **Get API Key**:
- Visit [TON Console](https://tonconsole.com)
- Create account and login
- Generate new API key
- Copy the key → paste to `API_KEY` in your `.env` file
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`
**Alternative**: You can also use [TON API](https://tonapi.io) for getting API key.
#### 🔐 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
@@ -126,35 +126,57 @@ python main.py
#### Programmatic Usage
```python
from app.methods import FragmentTon, FragmentPremium, FragmentStars
import asyncio
from app.methods import topup_ton, buy_premium, buy_stars
# TON topup for ads
ton_client = FragmentTon()
result = await ton_client.topup_ton("@username", 5)
async def main():
# Send 10 TON to @username
result = await topup_ton("@username", 10)
print(result)
# Premium purchase
premium_client = FragmentPremium()
result = await premium_client.buy_premium("@username", 6)
# Gift 6 months of Telegram Premium
result = await buy_premium("@username", 6)
print(result)
# Stars purchase
stars_client = FragmentStars()
result = await stars_client.buy_stars("@username", 50)
# 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 | Method | Parameters | Limits |
|--------------------|---------------------------------|------------------------|---------------------|
| **TON Topup** | `topup_ton(username, amount)` | Username, TON amount | 1-1,000,000,000 TON |
| Operation | Function | Parameters | Limits |
| ------------------ | ------------------------------- | ---------------------- | ------------------- |
| **TON Topup** | `topup_ton(username, amount)` | Username, TON amount | 11,000,000,000 TON |
| **Premium Gift** | `buy_premium(username, months)` | Username, duration | 3, 6, or 12 months |
| **Stars Purchase** | `buy_stars(username, amount)` | Username, Stars amount | 50-1,000,000 Stars |
| **Stars Purchase** | `buy_stars(username, amount)` | Username, Stars amount | 501,000,000 Stars |
### Username Formats
All methods accept various username formats:
- `@username` (with @)
- `username` (without @)
Usernames can be passed with or without `@`.
<div align="center">
+1 -1
View File
@@ -15,7 +15,7 @@ def setup_logging() -> None:
file_handler.setFormatter(formatter)
logging.basicConfig(level=logging.DEBUG, handlers=[console_handler, file_handler], force=True)
logging.getLogger("httpx").setLevel(logging.INFO)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
+10
View File
@@ -87,16 +87,25 @@ async def buy_premium(username: str, months: int) -> dict:
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() as client:
logger.info("Searching recipient: %s", username)
recipient = await search_premium_recipient(
client, fragment_hash, cookies, username, months
)
logger.info("Initializing Premium gift request: %s months to %s", months, username)
req_id = await init_gift_premium(client, fragment_hash, cookies, recipient, months)
# logger.info("Requesting transaction payload (req_id=%s)", req_id)
tx_data = {
"account": json.dumps(account),
"device": DEVICE,
@@ -109,6 +118,7 @@ async def buy_premium(username: str, months: int) -> dict:
client, HEADERS, cookies, account, tx_data, fragment_hash
)
logger.info("Broadcasting transaction to TON blockchain")
tx_hash = await process_transaction(transaction)
return {
"success": True,
+10
View File
@@ -75,14 +75,23 @@ async def buy_stars(username: str, amount: int) -> dict:
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() as client:
logger.info("Searching recipient: %s", username)
recipient = await search_stars_recipient(client, fragment_hash, cookies, username)
logger.info("Initializing Stars purchase request: %s stars to %s", amount, username)
req_id = await init_buy_stars(client, fragment_hash, cookies, recipient, amount)
# logger.info("Requesting transaction payload (req_id=%s)", req_id)
tx_data = {
"account": json.dumps(account),
"device": DEVICE,
@@ -95,6 +104,7 @@ async def buy_stars(username: str, amount: int) -> dict:
client, HEADERS, cookies, account, tx_data, fragment_hash
)
logger.info("Broadcasting transaction to TON blockchain")
tx_hash = await process_transaction(transaction)
return {
"success": True,
+10
View File
@@ -81,14 +81,23 @@ async def topup_ton(username: str, amount: int) -> dict:
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() as client:
logger.info("Searching recipient: %s", username)
recipient = await search_ads_recipient(client, fragment_hash, cookies, username)
logger.info("Initializing topup request: %s TON to %s", amount, username)
req_id = await init_ads_topup(client, fragment_hash, cookies, recipient, amount)
# logger.info("Requesting transaction payload (req_id=%s)", req_id)
tx_data = {
"account": json.dumps(account),
"device": DEVICE,
@@ -101,6 +110,7 @@ async def topup_ton(username: str, amount: int) -> dict:
client, HEADERS, cookies, account, tx_data, fragment_hash
)
logger.info("Broadcasting transaction to TON blockchain")
tx_hash = await process_transaction(transaction)
return {
"success": True,
-59
View File
@@ -1,59 +0,0 @@
import asyncio
import logging
from tonutils.clients import TonapiClient
from tonutils.types import NetworkGlobalID
from app.core import config
from app.core.constants import WALLET_CLASSES
from app.core.exceptions import TransactionError, WalletError
from app.utils.decoder import clean_decode
logger = logging.getLogger(__name__)
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."
)
client = TonapiClient(network=NetworkGlobalID.MAINNET, api_key=config.API_KEY)
async with 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"])
await wallet.refresh()
result = await wallet.transfer(
destination=message["address"],
amount=int(message["amount"]), # nanotons, not TON
body=payload,
)
return result
except (WalletError, TransactionError):
raise
except Exception as exc:
raise TransactionError(f"Transaction broadcast failed: {exc}") from exc
+15 -22
View File
@@ -1,4 +1,3 @@
import asyncio
import base64
import json
import logging
@@ -29,43 +28,37 @@ async def process_transaction(transaction_data: dict) -> str:
"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:
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"])
seqno_before = wallet.seqno
result = await wallet.transfer(
destination=message["address"],
amount=int(message["amount"]), # nanotons, not TON
body=payload,
)
# Wait for on-chain confirmation so the next call sees updated seqno
for _ in range(30):
await asyncio.sleep(3)
await wallet.refresh()
if wallet.seqno != seqno_before:
break
return result
except (WalletError, TransactionError):
raise
+13
View File
@@ -0,0 +1,13 @@
"""End-to-end integration test: buy 50 Stars for @bohd4nx.
Requires cookies.json and a valid .env (API_KEY + SEED).
Auto-skipped when cookies are unavailable (e.g. local runs without secrets).
"""
from app.methods.stars import buy_stars
async def test_buy_stars_e2e(cookies):
result = await buy_stars("@bohd4nx", 50)
assert result["success"] is True, result.get("error")
assert result["data"]["transaction_id"]