From b1eae1dcb7270ada380516e96bf6e54c9421211b Mon Sep 17 00:00:00 2001 From: bohd4nx Date: Mon, 19 Jan 2026 19:02:28 +0200 Subject: [PATCH] feat: Refactor cookie handling and hash retrieval; update environment configuration and improve logging levels --- .env.example | 11 +++--- README.md | 85 +++++++++++++++++----------------------- app/core/config.py | 2 +- app/core/logging.py | 2 + app/methods/premium.py | 59 +++++++++++++++++++--------- app/methods/stars.py | 55 ++++++++++++++++++-------- app/methods/ton.py | 59 +++++++++++++++++++--------- app/utils/__init__.py | 12 +++++- app/utils/client.py | 32 ++++++++------- app/utils/cookies.py | 35 +++++++++++++++++ app/utils/hash.py | 35 +++++++++++++++++ app/utils/transaction.py | 25 +++++++++++- app/utils/wallet.py | 11 +++--- main.py | 2 +- requirements.txt | 2 +- 15 files changed, 290 insertions(+), 137 deletions(-) create mode 100644 app/utils/cookies.py create mode 100644 app/utils/hash.py diff --git a/.env.example b/.env.example index d6691ef..9abefcb 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,10 @@ # Fragment.com cookies - copy from browser after login (Header String format) -# IMPORTANT: Use quotes to prevent semicolons from being treated as comments -COOKIES = "your_fragment_cookies_here" - -HASH = +# How to get fragment hash: open devtools -> Network -> api?hash= +# HASH = "your_fragment_hash_here" --- IGNORE --- +# Hash is now fetched dynamically, so this line is no longer needed. # TON wallet seed phrase - 12 or 24 words separated by spaces -SEED = +SEED = "your_ton_wallet_seed_phrase_here" # TON API key - get from https://tonconsole.com -API_KEY = +API_KEY = "your_ton_api_key_here" diff --git a/README.md b/README.md index 9236d39..7a9800d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ 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/UQBUAa7KCx1ifmoEy6lF7j-822Dm_cE1j9SR7UWteu3jzukV) +[Report Bug](https://github.com/bohd4nx/fragmentapi/issues) · [Request Feature](https://github.com/bohd4nx/fragmentapi/issues) · [ +**Donate TON**](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5) @@ -40,12 +41,8 @@ cp .env.example .env Edit `.env` file: ```env -COOKIES=your_fragment_cookies_here - SEED=word1 word2 word3 ... word24 -HASH=your_fragment_hash_here - API_KEY=your_ton_api_key_here ``` @@ -57,22 +54,27 @@ API_KEY=your_ton_api_key_here 1. **Install Cookie Editor Extension**: - - Download from [Chrome Web Store](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) - - Add extension to your browser + - 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 → paste to `COOKIES` in your `.env` file - - **Important**: Wrap the value in quotes to prevent semicolons from being treated as comments + - 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**: +**Expected format** (`cookies.json` in project root): -```env -COOKIES = "stel_token=;stel_dt=;stel_ssid=;stel_ton_token=" +```json +{ + "stel_ssid": "", + "stel_dt": "", + "stel_token": "", + "stel_ton_token": "" +} ``` #### 🔐 TON Wallet Seed Phrase @@ -81,52 +83,35 @@ COOKIES = "stel_token=;stel_dt=;stel_ssid=;stel_ton_token= 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) + - 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 + - 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 + - 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 -**Prerequisites**: Complete the cookies setup above first. - -1. **Connect Wallet to Fragment**: - - - Go to [Fragment.com](https://fragment.com) - - Ensure your TON wallet is connected - - Log in with your Telegram account - -2. **Extract Hash**: - - Open **Developer Tools** (F12) - - Click on **"Network"** tab - - Navigate to [Fragment Stars Purchase](https://fragment.com/stars/buy) - - Select recipient as **"Buy for myself"** - - Refresh the page (Ctrl+R / Cmd+R) - - Look for network requests containing `?hash=` - - Find the hash value and copy it → paste to `HASH` in your `.env` file - -**Expected format**: `?hash=` +Hash is fetched automatically from Fragment pages at runtime. You no longer need to add `HASH` to `.env`. #### 🔑 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 + - 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 **Alternative**: You can also use [TON API](https://tonapi.io) for getting API key. @@ -159,7 +144,7 @@ result = await stars_client.buy_stars("@username", 50) ### Supported Operations | Operation | Method | 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 | diff --git a/app/core/config.py b/app/core/config.py index 5fe99d7..331989a 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -18,7 +18,7 @@ class Config: load_dotenv(env_path) - required_keys = ["COOKIES", "SEED", "HASH", "API_KEY"] + required_keys = ["SEED", "API_KEY"] missing_keys: list[str] = [] for key in required_keys: diff --git a/app/core/logging.py b/app/core/logging.py index 2f49e44..2f38067 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -28,6 +28,8 @@ def setup_logging() -> None: logging.getLogger("aiogram.dispatcher").setLevel(logging.INFO) logging.getLogger("aiogram.event").setLevel(logging.ERROR) + logging.getLogger("httpx").setLevel(logging.INFO) + logging.getLogger("httpcore").setLevel(logging.WARNING) logger = logging.getLogger(__name__) diff --git a/app/methods/premium.py b/app/methods/premium.py index f3fad52..21a1772 100644 --- a/app/methods/premium.py +++ b/app/methods/premium.py @@ -7,7 +7,15 @@ from tonutils.client import TonapiClient from tonutils.wallet import WalletV5R1 from app.core import config -from app.utils import TransactionProcessor, WalletLinker, ApiClient, clean_decode, parse_json_response +from app.utils import ( + TransactionProcessor, + WalletLinker, + ApiClient, + clean_decode, + parse_json_response, + load_cookies, + get_fragment_hash, +) logger = logging.getLogger(__name__) @@ -15,20 +23,21 @@ logger = logging.getLogger(__name__) class FragmentPremium: def __init__(self): self.headers = { - 'accept': 'application/json, text/javascript, */*; q=0.01', - 'accept-encoding': 'gzip, deflate, br, zstd', - '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', - 'cookie': config.COOKIES, - 'origin': 'https://fragment.com', - 'referer': 'https://fragment.com/premium/buy', - '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' + "accept": "application/json, text/javascript, */*; q=0.01", + "accept-encoding": "gzip, deflate, br, zstd", + "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", + "referer": "https://fragment.com/premium/gift", + "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", } + self.cookies = load_cookies() + self.transaction_processor = TransactionProcessor(clean_decode) - self.wallet_linker = WalletLinker(self.headers, self.transaction_processor) - self.api_client = ApiClient(self.headers, self.wallet_linker) + self.wallet_linker = WalletLinker(self.headers, self.cookies, self.transaction_processor) + self.api_client = ApiClient(self.headers, self.cookies, self.wallet_linker) @staticmethod async def _get_account_info(): @@ -47,12 +56,20 @@ class FragmentPremium: if months not in [3, 6, 12]: return {"success": False, "error": "Invalid duration. Use 3, 6, or 12 months"} + fragment_hash = await get_fragment_hash( + self.cookies, + self.headers, + "https://fragment.com/premium/gift", + ) + if not fragment_hash: + raise RuntimeError("Failed to fetch Fragment hash") + account = await self._get_account_info() async with httpx.AsyncClient() as client: search_data = {"query": username, "months": months, "method": "searchPremiumGiftRecipient"} - search_resp = await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=search_data) + search_resp = await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=search_data) search_result, error = parse_json_response(search_resp, logger, "search") if search_result is None: @@ -63,12 +80,12 @@ class FragmentPremium: return {"success": False, "error": "User not found"} update_data = {"mode": "new", "lv": "false", "dh": str(int(time.time())), "method": "updatePremiumState"} - await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=update_data) + await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=update_data) init_data = {"recipient": recipient, "months": months, "method": "initGiftPremiumRequest"} - init_resp = await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=init_data) + init_resp = await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=init_data) init_result, error = parse_json_response(init_resp, logger, "init") if init_result is None: @@ -91,7 +108,11 @@ class FragmentPremium: 'method': 'getGiftPremiumLink' } - request_success, transaction_result = await self.api_client.execute_transaction_request(tx_data, account) + request_success, transaction_result = await self.api_client.execute_transaction_request( + tx_data, + account, + fragment_hash, + ) if not request_success: return transaction_result diff --git a/app/methods/stars.py b/app/methods/stars.py index 93b110a..8be0e82 100644 --- a/app/methods/stars.py +++ b/app/methods/stars.py @@ -7,7 +7,15 @@ from tonutils.client import TonapiClient from tonutils.wallet import WalletV5R1 from app.core import config -from app.utils import TransactionProcessor, WalletLinker, ApiClient, clean_decode, parse_json_response +from app.utils import ( + TransactionProcessor, + WalletLinker, + ApiClient, + clean_decode, + parse_json_response, + load_cookies, + get_fragment_hash, +) logger = logging.getLogger(__name__) @@ -15,20 +23,21 @@ logger = logging.getLogger(__name__) class FragmentStars: def __init__(self): self.headers = { - 'accept': 'application/json, text/javascript, */*; q=0.01', - 'accept-encoding': 'gzip, deflate, br, zstd', - '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', - 'cookie': config.COOKIES, - 'origin': 'https://fragment.com', - 'referer': 'https://fragment.com/stars/buy', - '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' + "accept": "application/json, text/javascript, */*; q=0.01", + "accept-encoding": "gzip, deflate, br, zstd", + "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", + "referer": "https://fragment.com/stars/buy", + "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", } + self.cookies = load_cookies() + self.transaction_processor = TransactionProcessor(clean_decode) - self.wallet_linker = WalletLinker(self.headers, self.transaction_processor) - self.api_client = ApiClient(self.headers, self.wallet_linker) + self.wallet_linker = WalletLinker(self.headers, self.cookies, self.transaction_processor) + self.api_client = ApiClient(self.headers, self.cookies, self.wallet_linker) @staticmethod async def _get_account_info(): @@ -47,12 +56,20 @@ class FragmentStars: if amount < 50 or not isinstance(amount, int): return {"success": False, "error": "Amount must be an integer >= 50 stars"} + fragment_hash = await get_fragment_hash( + self.cookies, + self.headers, + "https://fragment.com/stars/buy", + ) + if not fragment_hash: + raise RuntimeError("Failed to fetch Fragment hash") + account = await self._get_account_info() async with httpx.AsyncClient() as client: search_data = {"query": username, "quantity": "", "method": "searchStarsRecipient"} - search_resp = await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=search_data) + search_resp = await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=search_data) search_result, error = parse_json_response(search_resp, logger, "search") if search_result is None: @@ -63,8 +80,8 @@ class FragmentStars: return {"success": False, "error": "User not found"} init_data = {"recipient": recipient, "quantity": amount, "method": "initBuyStarsRequest"} - init_resp = await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=init_data) + init_resp = await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=init_data) init_result, error = parse_json_response(init_resp, logger, "init") if init_result is None: @@ -83,7 +100,11 @@ class FragmentStars: 'method': 'getBuyStarsLink' } - request_success, transaction_result = await self.api_client.execute_transaction_request(tx_data, account) + request_success, transaction_result = await self.api_client.execute_transaction_request( + tx_data, + account, + fragment_hash, + ) if not request_success: return transaction_result diff --git a/app/methods/ton.py b/app/methods/ton.py index 634ca8f..596678b 100644 --- a/app/methods/ton.py +++ b/app/methods/ton.py @@ -7,7 +7,15 @@ from tonutils.client import TonapiClient from tonutils.wallet import WalletV5R1 from app.core import config -from app.utils import TransactionProcessor, WalletLinker, ApiClient, clean_decode, parse_json_response +from app.utils import ( + TransactionProcessor, + WalletLinker, + ApiClient, + clean_decode, + parse_json_response, + load_cookies, + get_fragment_hash, +) logger = logging.getLogger(__name__) @@ -15,20 +23,21 @@ logger = logging.getLogger(__name__) class FragmentTon: def __init__(self): self.headers = { - 'accept': 'application/json, text/javascript, */*; q=0.01', - 'accept-encoding': 'gzip, deflate, br, zstd', - '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', - 'cookie': config.COOKIES, - 'origin': 'https://fragment.com', - 'referer': 'https://fragment.com/ads/topup', - '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' + "accept": "application/json, text/javascript, */*; q=0.01", + "accept-encoding": "gzip, deflate, br, zstd", + "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", + "referer": "https://fragment.com/ads/topup", + "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", } + self.cookies = load_cookies() + self.transaction_processor = TransactionProcessor(clean_decode) - self.wallet_linker = WalletLinker(self.headers, self.transaction_processor) - self.api_client = ApiClient(self.headers, self.wallet_linker) + self.wallet_linker = WalletLinker(self.headers, self.cookies, self.transaction_processor) + self.api_client = ApiClient(self.headers, self.cookies, self.wallet_linker) @staticmethod async def _get_account_info(): @@ -47,16 +56,24 @@ class FragmentTon: if amount < 1 or not isinstance(amount, int): return {"success": False, "error": "Amount must be an integer >= 1 TON"} + fragment_hash = await get_fragment_hash( + self.cookies, + self.headers, + "https://fragment.com/ads/topup", + ) + if not fragment_hash: + raise RuntimeError("Failed to fetch Fragment hash") + account = await self._get_account_info() async with httpx.AsyncClient() as client: update_data = {"mode": "new", "method": "updateAdsTopupState"} - await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=update_data) + await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=update_data) search_data = {"query": username, "method": "searchAdsTopupRecipient"} - search_resp = await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=search_data) + search_resp = await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=search_data) search_result, error = parse_json_response(search_resp, logger, "search") if search_result is None: @@ -67,8 +84,8 @@ class FragmentTon: return {"success": False, "error": "User not found"} init_data = {"recipient": recipient, "amount": amount, "method": "initAdsTopupRequest"} - init_resp = await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=init_data) + init_resp = await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=init_data) init_result, error = parse_json_response(init_resp, logger, "init") if init_result is None: @@ -90,7 +107,11 @@ class FragmentTon: 'method': 'getAdsTopupLink' } - request_success, transaction_result = await self.api_client.execute_transaction_request(tx_data, account) + request_success, transaction_result = await self.api_client.execute_transaction_request( + tx_data, + account, + fragment_hash, + ) if not request_success: return transaction_result diff --git a/app/utils/__init__.py b/app/utils/__init__.py index a980904..9937bf4 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -1,6 +1,16 @@ from app.utils.client import ApiClient, parse_json_response +from app.utils.cookies import load_cookies from app.utils.decoder import clean_decode +from app.utils.hash import get_fragment_hash from app.utils.transaction import TransactionProcessor from app.utils.wallet import WalletLinker -__all__ = ['TransactionProcessor', 'WalletLinker', 'ApiClient', 'clean_decode', 'parse_json_response'] +__all__ = [ + 'TransactionProcessor', + 'WalletLinker', + 'ApiClient', + 'clean_decode', + 'parse_json_response', + 'load_cookies', + 'get_fragment_hash' +] diff --git a/app/utils/client.py b/app/utils/client.py index 24345bc..808fd9a 100644 --- a/app/utils/client.py +++ b/app/utils/client.py @@ -1,15 +1,13 @@ -from typing import Any import logging +from typing import Any import httpx -from app.core import config - def parse_json_response( - response: httpx.Response, - logger: logging.Logger, - context: str, + response: httpx.Response, + logger: logging.Logger, + context: str, ) -> tuple[dict[str, Any] | None, str | None]: try: return response.json(), None @@ -20,26 +18,32 @@ def parse_json_response( class ApiClient: - def __init__(self, headers: dict, wallet_linker): + def __init__(self, headers: dict, cookies: dict, wallet_linker): self.headers = headers + self.cookies = cookies self.wallet_linker = wallet_linker async def execute_transaction_request( self, tx_data: dict[str, Any], account: dict[str, Any], + fragment_hash: str, ) -> tuple[bool, dict[str, Any]]: async with httpx.AsyncClient() as client: - tx_resp = await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=tx_data) - transaction = tx_resp.json() + tx_resp = await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=tx_data) + transaction, error = parse_json_response(tx_resp, logging.getLogger(__name__), "transaction") + if transaction is None: + return False, {"success": False, "error": f"Invalid response from Fragment API: {error}"} if transaction.get("need_verify"): - if not await self.wallet_linker.link_wallet(account): + if not await self.wallet_linker.link_wallet(account, fragment_hash): return False, {"success": False, "error": "Failed to link wallet"} - tx_resp = await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=tx_data) - transaction = tx_resp.json() + tx_resp = await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=tx_data) + transaction, error = parse_json_response(tx_resp, logging.getLogger(__name__), "transaction") + if transaction is None: + return False, {"success": False, "error": f"Invalid response from Fragment API: {error}"} return True, transaction diff --git a/app/utils/cookies.py b/app/utils/cookies.py new file mode 100644 index 0000000..ca6c25a --- /dev/null +++ b/app/utils/cookies.py @@ -0,0 +1,35 @@ +import json +import logging +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + + +def load_cookies() -> dict[str, Any]: + cookies_path = Path(__file__).resolve().parents[2] / "cookies.json" + + if not cookies_path.exists(): + logger.error("cookies.json file not found!") + return {} + + try: + with cookies_path.open("r", encoding="utf-8") as file: + cookies = json.load(file) + + required_keys = ["stel_ssid", "stel_dt", "stel_token", "stel_ton_token"] + missing_or_empty = [ + key for key in required_keys + if not str(cookies.get(key, "")).strip() + ] + + if missing_or_empty: + logger.warning( + "cookies.json has missing or empty values: %s", + ", ".join(missing_or_empty) + ) + + return cookies + except Exception as exc: + logger.error(f"Failed to load cookies.json: {exc}") + return {} diff --git a/app/utils/hash.py b/app/utils/hash.py new file mode 100644 index 0000000..5414ff1 --- /dev/null +++ b/app/utils/hash.py @@ -0,0 +1,35 @@ +import logging +import re +from typing import Any + +import httpx + +logger = logging.getLogger(__name__) + + +async def get_fragment_hash( + cookies: dict[str, Any], + headers: dict[str, str], + page_url: str, +) -> str | None: + request_headers = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "accept-language": headers.get("accept-language") or headers.get("Accept-Language", "en-US,en;q=0.9"), + "user-agent": headers.get("user-agent") or headers.get("User-Agent", ""), + "referer": "https://fragment.com/", + } + + async with httpx.AsyncClient(cookies=cookies) as client: + response = await client.get(page_url, headers=request_headers) + + if response.status_code != 200: + logger.error("Failed to fetch Fragment page for hash: %s", response.status_code) + return None + + text = response.text + match = re.search(r"(?:https://fragment\.com)?/api\?hash=([a-f0-9]+)", text) + if match: + return match.group(1) + + logger.error("Failed to extract Fragment hash from page") + return None diff --git a/app/utils/transaction.py b/app/utils/transaction.py index c4a6dab..63b4655 100644 --- a/app/utils/transaction.py +++ b/app/utils/transaction.py @@ -1,10 +1,9 @@ import logging -from tonutils.client import TonapiClient +from tonutils.client import TonapiClient, ToncenterV3Client from tonutils.wallet import WalletV5R1 from tonutils.wallet.messages import TransferMessage - from app.core import config logger = logging.getLogger(__name__) @@ -14,10 +13,32 @@ class TransactionProcessor: def __init__(self, clean_decode_func): self._clean_decode = clean_decode_func + @staticmethod + async def _check_wallet_balance() -> tuple[bool, str | None]: + client = ToncenterV3Client(is_testnet=False, rps=1, max_retries=1) + wallet, _, _, _ = WalletV5R1.from_mnemonic(client=client, mnemonic=config.SEED) + + try: + balance = await wallet.balance() + except Exception as exc: + return False, f"Wallet balance check failed: {exc}" + + try: + if float(balance) <= 0: + return False, "Wallet balance is zero" + except Exception: + pass + + return True, None + async def process_transaction(self, transaction_data: dict) -> tuple[bool, str | None, str | None]: if "transaction" not in transaction_data or "messages" not in transaction_data["transaction"]: return False, "Invalid transaction", None + ready, reason = await self._check_wallet_balance() + if not ready: + return False, reason or "Wallet is not ready", None + client = TonapiClient(api_key=config.API_KEY, is_testnet=False) wallet, _, _, _ = WalletV5R1.from_mnemonic(client=client, mnemonic=config.SEED) diff --git a/app/utils/wallet.py b/app/utils/wallet.py index bd262b0..7bf4b0f 100644 --- a/app/utils/wallet.py +++ b/app/utils/wallet.py @@ -2,15 +2,14 @@ from typing import Any import httpx -from app.core import config - class WalletLinker: - def __init__(self, headers: dict, transaction_processor): + def __init__(self, headers: dict, cookies: dict, transaction_processor): self.headers = headers + self.cookies = cookies self.transaction_processor = transaction_processor - async def link_wallet(self, account: dict[str, Any]) -> bool: + async def link_wallet(self, account: dict[str, Any], fragment_hash: str) -> bool: async with httpx.AsyncClient() as client: data = { 'account': account, @@ -18,8 +17,8 @@ class WalletLinker: 'method': 'linkWallet' } - response = await client.post(f"https://fragment.com/api?hash={config.HASH}", - headers=self.headers, data=data) + response = await client.post(f"https://fragment.com/api?hash={fragment_hash}", + headers=self.headers, cookies=self.cookies, data=data) result = response.json() if result.get("ok"): diff --git a/main.py b/main.py index 18ce897..e3ca22d 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ async def topup_ton_example(): ton_client = FragmentTon() # @bohd4nx - target username, 5 - TON amount (integer 1-1000000000 (one billion)) - result = await ton_client.topup_ton("@bohd4nx", 1000000000) + result = await ton_client.topup_ton("@bohd4nx", 100) if result["success"]: data = result["data"] diff --git a/requirements.txt b/requirements.txt index 277b27f..7700cd3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ python-dotenv==1.2.1 asyncio==4.0.0 httpx==0.28.1 -tonutils==0.5.5 \ No newline at end of file +tonutils==0.5.6