From 7f3c3ea1c3c248ca6d6cb21a0d9dec9d506b2033 Mon Sep 17 00:00:00 2001 From: bohd4nx Date: Thu, 5 Mar 2026 06:26:49 +0200 Subject: [PATCH] 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 --- .env.example | 2 +- README.md | 182 +++++++++++++++++++--------------- app/core/logging.py | 2 +- app/methods/premium.py | 10 ++ app/methods/stars.py | 10 ++ app/methods/ton.py | 10 ++ app/utils/transaction.py | 59 ----------- app/utils/wallet.py | 37 +++---- tests/003_test_integration.py | 13 +++ 9 files changed, 162 insertions(+), 163 deletions(-) delete mode 100644 app/utils/transaction.py create mode 100644 tests/003_test_integration.py diff --git a/.env.example b/.env.example index 89cccc0..806b019 100644 --- a/.env.example +++ b/.env.example @@ -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) diff --git a/README.md b/README.md index 7a9800d..f1aeb84 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,13 @@ Automate TON topups, Telegram Premium purchases, and Stars transactions via Fragment.com

-[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) @@ -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 (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 @@ -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": "", - "stel_dt": "", - "stel_token": "", - "stel_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": "", + "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 | 1–1,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 | 50–1,000,000 Stars | -### Username Formats - -All methods accept various username formats: - -- `@username` (with @) -- `username` (without @) +Usernames can be passed with or without `@`.
diff --git a/app/core/logging.py b/app/core/logging.py index 5d046d1..08c986f 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -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) diff --git a/app/methods/premium.py b/app/methods/premium.py index 33639b7..a6004f9 100644 --- a/app/methods/premium.py +++ b/app/methods/premium.py @@ -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, diff --git a/app/methods/stars.py b/app/methods/stars.py index b4f2ce9..2dbf05e 100644 --- a/app/methods/stars.py +++ b/app/methods/stars.py @@ -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, diff --git a/app/methods/ton.py b/app/methods/ton.py index 1344118..60b3932 100644 --- a/app/methods/ton.py +++ b/app/methods/ton.py @@ -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, diff --git a/app/utils/transaction.py b/app/utils/transaction.py deleted file mode 100644 index 6c0a79a..0000000 --- a/app/utils/transaction.py +++ /dev/null @@ -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 - diff --git a/app/utils/wallet.py b/app/utils/wallet.py index 518591c..01932cd 100644 --- a/app/utils/wallet.py +++ b/app/utils/wallet.py @@ -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 diff --git a/tests/003_test_integration.py b/tests/003_test_integration.py new file mode 100644 index 0000000..8736906 --- /dev/null +++ b/tests/003_test_integration.py @@ -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"]