feat: Add Fragment API for TON topups, Telegram Premium purchases, and Stars transactions

- Implemented Fragment API with methods for TON topups, Premium gifts, and Stars purchases.
- Created configuration management using environment variables.
- Added detailed README documentation for installation, configuration, and usage.
- Developed utility classes for API client, wallet linking, and transaction processing.
- Included example usage in main.py for easy demonstration of API functionalities.
- Updated requirements.txt with necessary dependencies.
This commit is contained in:
bohd4nx
2025-11-03 05:58:34 +08:00
commit 760c853acd
17 changed files with 963 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
__title__ = "Fragment API by @bohd4nx"
__version__ = "2025.1.1"
__author__ = "Bohdan (bohd4nx)"
__timestamp__ = "2025-11-03T12:00:00Z"
+3
View File
@@ -0,0 +1,3 @@
from app.core.config import Config
__all__ = ['Config']
+37
View File
@@ -0,0 +1,37 @@
import logging
import os
import sys
from typing import Dict
from dotenv import load_dotenv
load_dotenv(encoding='utf-8')
logger = logging.getLogger(__name__)
class Config:
def __init__(self):
required_keys = ['COOKIES', 'SEED', 'HASH', 'API_KEY']
missing_keys = []
self._config = {}
for key in required_keys:
value = os.getenv(key, '').strip()
if not value:
missing_keys.append(key)
self._config[key.lower()] = value
if missing_keys:
logger.error(f"Missing required environment variables: {', '.join(missing_keys)}")
logger.error("Create .env file based on .env.example and fill all fields")
sys.exit(1)
logger.info("Configuration loaded successfully")
def get_config(self) -> Dict[str, str]:
return self._config.copy()
def get(self, key: str, default: str = '') -> str:
return self._config.get(key, default)
+5
View File
@@ -0,0 +1,5 @@
from app.methods.premium import FragmentPremium
from app.methods.stars import FragmentStars
from app.methods.ton import FragmentTon
__all__ = ['FragmentTon', 'FragmentPremium', 'FragmentStars']
+120
View File
@@ -0,0 +1,120 @@
import base64
import logging
import re
import string
import time
import httpx
from tonutils.client import TonapiClient
from tonutils.wallet import WalletV5R1
from app.core.config import Config
from app.utils import TransactionProcessor, WalletLinker, ApiClient
logger = logging.getLogger(__name__)
class FragmentPremium:
def __init__(self):
config_reader = Config()
self.config = config_reader.get_config()
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': self.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'
}
self.transaction_processor = TransactionProcessor(self.config, self._clean_decode)
self.wallet_linker = WalletLinker(self.config, self.headers, self.transaction_processor)
self.api_client = ApiClient(self.config, self.headers, self.wallet_linker)
@staticmethod
def _clean_decode(s):
b = base64.b64decode(re.sub(r'[^A-Za-z0-9+/=]', '', s.strip()) + "=" * (-len(s) % 4))
t = next(
(b[i:].decode('utf-8', 'ignore') for i in range(20) if
b[i:].decode('utf-8', 'ignore').startswith("Telegram Premium")),
b.decode('utf-8', 'ignore')
)
return ''.join(c for c in t if c in string.printable or c in '\n\r\t ').strip()
async def _get_account_info(self):
client = TonapiClient(api_key=self.config['api_key'], is_testnet=False)
wallet, pub_key, _, _ = WalletV5R1.from_mnemonic(client=client, mnemonic=self.config['seed'])
boc = wallet.state_init.serialize().to_boc()
return {
"address": wallet.address.to_str(False, False),
"publicKey": pub_key.hex(),
"chain": "-239",
"walletStateInit": base64.b64encode(boc).decode()
}
async def buy_premium(self, username, months):
if months not in [3, 6, 12]:
return {"success": False, "error": "Invalid duration. Use 3, 6, or 12 months"}
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={self.config['hash']}",
headers=self.headers, data=search_data)
search_result = search_resp.json()
recipient = search_result.get("found", {}).get("recipient")
if not recipient:
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={self.config['hash']}",
headers=self.headers, data=update_data)
init_data = {"recipient": recipient, "months": months, "method": "initGiftPremiumRequest"}
init_resp = await client.post(f"https://fragment.com/api?hash={self.config['hash']}",
headers=self.headers, data=init_data)
init_result = init_resp.json()
req_id = init_result.get("req_id")
if not req_id:
return {"success": False, "error": "Failed to initialize purchase"}
tx_data = {
'account': account,
'device': {"appVersion": "5.4.3", "platform": "iphone",
"features": ["SendTransaction", {"maxMessages": 255, "name": "SendTransaction"},
{"types": ["text", "binary", "cell"], "name": "SignData"}],
"appName": "Tonkeeper", "maxProtocolVersion": 2},
'transaction': 1,
'id': req_id,
'show_sender': 1,
'ref': "OprzztcdJ",
'method': 'getGiftPremiumLink'
}
request_success, transaction_result = await self.api_client.execute_transaction_request(tx_data, account)
if not request_success:
return transaction_result
success, error, tx_hash = await self.transaction_processor.process_transaction(transaction_result)
if success:
return {
"success": True,
"data": {
"transaction_id": tx_hash,
"username": username,
"months": months,
"timestamp": int(time.time())
}
}
return {"success": False, "error": error}
+109
View File
@@ -0,0 +1,109 @@
import base64
import logging
import re
import time
import httpx
from tonutils.client import TonapiClient
from tonutils.wallet import WalletV5R1
from app.core.config import Config
from app.utils import TransactionProcessor, WalletLinker, ApiClient
logger = logging.getLogger(__name__)
class FragmentStars:
def __init__(self):
config_reader = Config()
self.config = config_reader.get_config()
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': self.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'
}
self.transaction_processor = TransactionProcessor(self.config, self._clean_decode)
self.wallet_linker = WalletLinker(self.config, self.headers, self.transaction_processor)
self.api_client = ApiClient(self.config, self.headers, self.wallet_linker)
@staticmethod
def _clean_decode(s):
s = re.sub(r'[^A-Za-z0-9+/=]', '', s.strip())
b = base64.b64decode(s + "=" * (-len(s) % 4))
t = b.decode('utf-8', 'ignore')
m = re.search(r'(\d+\s+Telegram\s+Stars.*?Ref#[A-Za-z0-9]+)', t, re.S)
return m.group(1).strip() if m else t.strip()
async def _get_account_info(self):
client = TonapiClient(api_key=self.config['api_key'], is_testnet=False)
wallet, pub_key, _, _ = WalletV5R1.from_mnemonic(client=client, mnemonic=self.config['seed'])
boc = wallet.state_init.serialize().to_boc()
return {
"address": wallet.address.to_str(False, False),
"publicKey": pub_key.hex(),
"chain": "-239",
"walletStateInit": base64.b64encode(boc).decode()
}
async def buy_stars(self, username, amount):
if amount < 50:
return {"success": False, "error": "Minimum amount is 50 stars"}
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={self.config['hash']}",
headers=self.headers, data=search_data)
search_result = search_resp.json()
recipient = search_result.get("found", {}).get("recipient")
if not recipient:
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={self.config['hash']}",
headers=self.headers, data=init_data)
init_result = init_resp.json()
req_id = init_result.get("req_id")
if not req_id:
return {"success": False, "error": "Failed to initialize purchase"}
tx_data = {
'account': account,
'device': "iPhone15,2",
'transaction': 1,
'id': req_id,
'show_sender': 0,
'method': 'getBuyStarsLink'
}
request_success, transaction_result = await self.api_client.execute_transaction_request(tx_data, account)
if not request_success:
return transaction_result
success, error, tx_hash = await self.transaction_processor.process_transaction(transaction_result)
if success:
return {
"success": True,
"data": {
"transaction_id": tx_hash,
"username": username,
"amount": amount,
"timestamp": int(time.time())
}
}
return {"success": False, "error": error}
+115
View File
@@ -0,0 +1,115 @@
import base64
import logging
import re
import time
import httpx
from tonutils.client import TonapiClient
from tonutils.wallet import WalletV5R1
from app.core.config import Config
from app.utils import TransactionProcessor, WalletLinker, ApiClient
logger = logging.getLogger(__name__)
class FragmentTon:
def __init__(self):
config_reader = Config()
self.config = config_reader.get_config()
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': self.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'
}
self.transaction_processor = TransactionProcessor(self.config, self._clean_decode)
self.wallet_linker = WalletLinker(self.config, self.headers, self.transaction_processor)
self.api_client = ApiClient(self.config, self.headers, self.wallet_linker)
@staticmethod
def _clean_decode(s):
t = base64.b64decode(re.sub(r'[^A-Za-z0-9+/=]', '', s.strip()) + "=" * (-len(s) % 4)).decode('utf-8',
errors='ignore')
t = ''.join(c for c in t if c.isprintable() or c.isspace())
return t.lstrip("r0N").strip()
async def _get_account_info(self):
client = TonapiClient(api_key=self.config['api_key'], is_testnet=False)
wallet, pub_key, _, _ = WalletV5R1.from_mnemonic(client=client, mnemonic=self.config['seed'])
boc = wallet.state_init.serialize().to_boc()
return {
"address": wallet.address.to_str(False, False),
"publicKey": pub_key.hex(),
"chain": "-239",
"walletStateInit": base64.b64encode(boc).decode()
}
async def topup_ton(self, username, amount):
if amount < 1 or not isinstance(amount, int):
return {"success": False, "error": "Amount must be an integer >= 1 TON"}
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={self.config['hash']}",
headers=self.headers, data=update_data)
search_data = {"query": username, "method": "searchAdsTopupRecipient"}
search_resp = await client.post(f"https://fragment.com/api?hash={self.config['hash']}",
headers=self.headers, data=search_data)
search_result = search_resp.json()
recipient = search_result.get("found", {}).get("recipient")
if not recipient:
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={self.config['hash']}",
headers=self.headers, data=init_data)
init_result = init_resp.json()
req_id = init_result.get("req_id")
if not req_id:
return {"success": False, "error": "Failed to initialize topup"}
tx_data = {
'account': account,
'device': {"appVersion": "5.4.3", "platform": "iphone",
"features": ["SendTransaction", {"maxMessages": 255, "name": "SendTransaction"},
{"types": ["text", "binary", "cell"], "name": "SignData"}],
"appName": "Tonkeeper", "maxProtocolVersion": 2},
'transaction': 1,
'id': req_id,
'show_sender': 1,
'method': 'getAdsTopupLink'
}
request_success, transaction_result = await self.api_client.execute_transaction_request(tx_data, account)
if not request_success:
return transaction_result
success, error, tx_hash = await self.transaction_processor.process_transaction(transaction_result)
if success:
return {
"success": True,
"data": {
"transaction_id": tx_hash,
"username": username,
"amount": amount,
"timestamp": int(time.time())
}
}
return {"success": False, "error": error}
+5
View File
@@ -0,0 +1,5 @@
from .api_client import ApiClient
from .linkWallet import WalletLinker
from .transaction import TransactionProcessor
__all__ = ['TransactionProcessor', 'WalletLinker', 'ApiClient']
+27
View File
@@ -0,0 +1,27 @@
from typing import Dict, Any, Tuple
import httpx
class ApiClient:
def __init__(self, config: dict, headers: dict, wallet_linker):
self.config = config
self.headers = headers
self.wallet_linker = wallet_linker
async def execute_transaction_request(self, tx_data: Dict[str, Any], account: Dict[str, Any]) -> Tuple[
bool, Dict[str, Any]]:
async with httpx.AsyncClient() as client:
tx_resp = await client.post(f"https://fragment.com/api?hash={self.config['hash']}",
headers=self.headers, data=tx_data)
transaction = tx_resp.json()
if transaction.get("need_verify"):
if not await self.wallet_linker.link_wallet(account):
return False, {"success": False, "error": "Failed to link wallet"}
tx_resp = await client.post(f"https://fragment.com/api?hash={self.config['hash']}",
headers=self.headers, data=tx_data)
transaction = tx_resp.json()
return True, transaction
+31
View File
@@ -0,0 +1,31 @@
from typing import Dict, Any
import httpx
class WalletLinker:
def __init__(self, config: dict, headers: dict, transaction_processor):
self.config = config
self.headers = headers
self.transaction_processor = transaction_processor
async def link_wallet(self, account: Dict[str, Any]) -> bool:
async with httpx.AsyncClient() as client:
data = {
'account': account,
'device': "iPhone15,2",
'method': 'linkWallet'
}
response = await client.post(f"https://fragment.com/api?hash={self.config['hash']}",
headers=self.headers, data=data)
result = response.json()
if result.get("ok"):
return True
if "transaction" in result:
success, _, _ = await self.transaction_processor.process_transaction(result)
return success
return False
+36
View File
@@ -0,0 +1,36 @@
import logging
from typing import Tuple, Optional
from tonutils.client import TonapiClient
from tonutils.wallet import WalletV5R1
from tonutils.wallet.messages import TransferMessage
logger = logging.getLogger(__name__)
class TransactionProcessor:
def __init__(self, config: dict, clean_decode_func):
self.config = config
self._clean_decode = clean_decode_func
async def process_transaction(self, transaction_data: dict) -> Tuple[bool, Optional[str], Optional[str]]:
if "transaction" not in transaction_data or "messages" not in transaction_data["transaction"]:
return False, "Invalid transaction", None
client = TonapiClient(api_key=self.config['api_key'], is_testnet=False)
wallet, _, _, _ = WalletV5R1.from_mnemonic(client=client, mnemonic=self.config['seed'])
try:
message = transaction_data["transaction"]["messages"][0]
payload = self._clean_decode(message["payload"])
messages = [TransferMessage(
destination=message["address"],
amount=int(message["amount"]) / 1000000000,
body=payload
)]
tx_hash = await wallet.batch_transfer_messages(messages=messages)
return True, None, tx_hash
except Exception as e:
return False, str(e), None