mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
feat: Refactor cookie handling and hash retrieval; update environment configuration and improve logging levels
This commit is contained in:
+5
-6
@@ -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=<your_fragment_hash_here>
|
||||
# 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"
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<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/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)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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=<TOKEN>;stel_dt=<STEL_DT>;stel_ssid=<SSID>;stel_ton_token=<TON_TOKEN>"
|
||||
```json
|
||||
{
|
||||
"stel_ssid": "<SSID>",
|
||||
"stel_dt": "<STEL_DT>",
|
||||
"stel_token": "<TOKEN>",
|
||||
"stel_ton_token": "<TON_TOKEN>"
|
||||
}
|
||||
```
|
||||
|
||||
#### 🔐 TON Wallet Seed Phrase
|
||||
@@ -81,52 +83,35 @@ COOKIES = "stel_token=<TOKEN>;stel_dt=<STEL_DT>;stel_ssid=<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=<your_hash_here>`
|
||||
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 |
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
@@ -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__)
|
||||
|
||||
+40
-19
@@ -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
|
||||
|
||||
+38
-17
@@ -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
|
||||
|
||||
+40
-19
@@ -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
|
||||
|
||||
+11
-1
@@ -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'
|
||||
]
|
||||
|
||||
+18
-14
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
+5
-6
@@ -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"):
|
||||
|
||||
@@ -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"]
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
python-dotenv==1.2.1
|
||||
asyncio==4.0.0
|
||||
httpx==0.28.1
|
||||
tonutils==0.5.5
|
||||
tonutils==0.5.6
|
||||
|
||||
Reference in New Issue
Block a user