mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
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:
@@ -0,0 +1,4 @@
|
||||
__title__ = "Fragment API by @bohd4nx"
|
||||
__version__ = "2025.1.1"
|
||||
__author__ = "Bohdan (bohd4nx)"
|
||||
__timestamp__ = "2025-11-03T12:00:00Z"
|
||||
@@ -0,0 +1,3 @@
|
||||
from app.core.config import Config
|
||||
|
||||
__all__ = ['Config']
|
||||
@@ -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)
|
||||
@@ -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']
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -0,0 +1,5 @@
|
||||
from .api_client import ApiClient
|
||||
from .linkWallet import WalletLinker
|
||||
from .transaction import TransactionProcessor
|
||||
|
||||
__all__ = ['TransactionProcessor', 'WalletLinker', 'ApiClient']
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user