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