mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
Merge pull request #3 from bohd4nx/refactor/tonutils-v2
Refactor/tonutils v2
This commit is contained in:
@@ -21,7 +21,18 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt pytest pytest-asyncio
|
||||
|
||||
- name: Write cookies.json
|
||||
if: ${{ env.COOKIES_JSON != '' }}
|
||||
run: echo "$COOKIES_JSON" > cookies.json
|
||||
env:
|
||||
COOKIES_JSON: ${{ secrets.COOKIES_JSON }}
|
||||
|
||||
- name: Run tests
|
||||
if: github.ref != 'refs/heads/master'
|
||||
run: pytest --ignore=tests/003_test_integration.py
|
||||
|
||||
- name: Run tests (master — full suite)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: pytest
|
||||
env:
|
||||
SEED: ${{ secrets.SEED }}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
from app.core.config import config
|
||||
from app.core.constants import ADS_PAGE, BASE_HEADERS, DEVICE, PREMIUM_PAGE, STARS_PAGE
|
||||
from app.core.constants import (
|
||||
ADS_PAGE,
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
PREMIUM_PAGE,
|
||||
STARS_PAGE,
|
||||
WALLET_CLASSES,
|
||||
)
|
||||
from app.core.cookies import load_cookies
|
||||
from app.core.exceptions import (
|
||||
ConfigError,
|
||||
@@ -19,6 +26,7 @@ __all__ = [
|
||||
"DEVICE",
|
||||
"PREMIUM_PAGE",
|
||||
"STARS_PAGE",
|
||||
"WALLET_CLASSES",
|
||||
"ConfigError",
|
||||
"CookiesError",
|
||||
"FragmentError",
|
||||
|
||||
+15
-14
@@ -4,9 +4,14 @@ import time
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core import load_cookies
|
||||
from app.core.constants import BASE_HEADERS, DEVICE, PREMIUM_PAGE
|
||||
from app.core.exceptions import FragmentError, UserNotFoundError
|
||||
from app.core import (
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
PREMIUM_PAGE,
|
||||
FragmentError,
|
||||
UserNotFoundError,
|
||||
load_cookies,
|
||||
)
|
||||
from app.utils import (
|
||||
execute_transaction_request,
|
||||
get_account_info,
|
||||
@@ -28,14 +33,12 @@ HEADERS: dict[str, str] = {
|
||||
async def search_premium_recipient(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
cookies: dict,
|
||||
username: str,
|
||||
months: int,
|
||||
) -> str:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
cookies=cookies,
|
||||
data={"query": username, "months": months, "method": "searchPremiumGiftRecipient"},
|
||||
)
|
||||
result = parse_json_response(resp, "searchPremiumGiftRecipient")
|
||||
@@ -51,14 +54,12 @@ async def search_premium_recipient(
|
||||
async def init_gift_premium(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
cookies: dict,
|
||||
recipient: str,
|
||||
months: int,
|
||||
) -> str:
|
||||
await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
cookies=cookies,
|
||||
data={
|
||||
"mode": "new",
|
||||
"lv": "false",
|
||||
@@ -69,7 +70,6 @@ async def init_gift_premium(
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
cookies=cookies,
|
||||
data={"recipient": recipient, "months": months, "method": "initGiftPremiumRequest"},
|
||||
)
|
||||
result = parse_json_response(resp, "initGiftPremiumRequest")
|
||||
@@ -96,14 +96,12 @@ async def buy_premium(username: str, months: int) -> dict:
|
||||
# logger.info("Retrieving TON wallet info")
|
||||
account = await get_account_info()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(cookies=cookies) as client:
|
||||
logger.info("Searching recipient: %s", username)
|
||||
recipient = await search_premium_recipient(
|
||||
client, fragment_hash, cookies, username, months
|
||||
)
|
||||
recipient = await search_premium_recipient(client, fragment_hash, username, months)
|
||||
|
||||
logger.info("Initializing Premium gift request: %s months to %s", months, username)
|
||||
req_id = await init_gift_premium(client, fragment_hash, cookies, recipient, months)
|
||||
req_id = await init_gift_premium(client, fragment_hash, recipient, months)
|
||||
|
||||
# logger.info("Requesting transaction payload (req_id=%s)", req_id)
|
||||
tx_data = {
|
||||
@@ -115,11 +113,14 @@ async def buy_premium(username: str, months: int) -> dict:
|
||||
"method": "getGiftPremiumLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(
|
||||
client, HEADERS, cookies, account, tx_data, fragment_hash
|
||||
client, HEADERS, account, tx_data, fragment_hash
|
||||
)
|
||||
|
||||
logger.info("Broadcasting transaction to TON blockchain")
|
||||
tx_hash = await process_transaction(transaction)
|
||||
logger.info(
|
||||
"Premium purchase successful: %s months -> %s | tx: %s", months, username, tx_hash
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
|
||||
+13
-11
@@ -4,9 +4,14 @@ import time
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core import load_cookies
|
||||
from app.core.constants import BASE_HEADERS, DEVICE, STARS_PAGE
|
||||
from app.core.exceptions import FragmentError, UserNotFoundError
|
||||
from app.core import (
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
STARS_PAGE,
|
||||
FragmentError,
|
||||
UserNotFoundError,
|
||||
load_cookies,
|
||||
)
|
||||
from app.utils import (
|
||||
execute_transaction_request,
|
||||
get_account_info,
|
||||
@@ -28,13 +33,11 @@ HEADERS: dict[str, str] = {
|
||||
async def search_stars_recipient(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
cookies: dict,
|
||||
username: str,
|
||||
) -> str:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
cookies=cookies,
|
||||
data={"query": username, "quantity": "", "method": "searchStarsRecipient"},
|
||||
)
|
||||
result = parse_json_response(resp, "searchStarsRecipient")
|
||||
@@ -50,14 +53,12 @@ async def search_stars_recipient(
|
||||
async def init_buy_stars(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
cookies: dict,
|
||||
recipient: str,
|
||||
amount: int,
|
||||
) -> str:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
cookies=cookies,
|
||||
data={"recipient": recipient, "quantity": amount, "method": "initBuyStarsRequest"},
|
||||
)
|
||||
result = parse_json_response(resp, "initBuyStarsRequest")
|
||||
@@ -84,12 +85,12 @@ async def buy_stars(username: str, amount: int) -> dict:
|
||||
# logger.info("Retrieving TON wallet info")
|
||||
account = await get_account_info()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(cookies=cookies) as client:
|
||||
logger.info("Searching recipient: %s", username)
|
||||
recipient = await search_stars_recipient(client, fragment_hash, cookies, username)
|
||||
recipient = await search_stars_recipient(client, fragment_hash, username)
|
||||
|
||||
logger.info("Initializing Stars purchase request: %s stars to %s", amount, username)
|
||||
req_id = await init_buy_stars(client, fragment_hash, cookies, recipient, amount)
|
||||
req_id = await init_buy_stars(client, fragment_hash, recipient, amount)
|
||||
|
||||
# logger.info("Requesting transaction payload (req_id=%s)", req_id)
|
||||
tx_data = {
|
||||
@@ -101,11 +102,12 @@ async def buy_stars(username: str, amount: int) -> dict:
|
||||
"method": "getBuyStarsLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(
|
||||
client, HEADERS, cookies, account, tx_data, fragment_hash
|
||||
client, HEADERS, account, tx_data, fragment_hash
|
||||
)
|
||||
|
||||
logger.info("Broadcasting transaction to TON blockchain")
|
||||
tx_hash = await process_transaction(transaction)
|
||||
logger.info("Stars purchase successful: %s stars -> %s | tx: %s", amount, username, tx_hash)
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
|
||||
+6
-12
@@ -4,9 +4,7 @@ import time
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core import load_cookies
|
||||
from app.core.constants import ADS_PAGE, BASE_HEADERS, DEVICE
|
||||
from app.core.exceptions import FragmentError, UserNotFoundError
|
||||
from app.core import ADS_PAGE, BASE_HEADERS, DEVICE, FragmentError, UserNotFoundError, load_cookies
|
||||
from app.utils import (
|
||||
execute_transaction_request,
|
||||
get_account_info,
|
||||
@@ -28,19 +26,16 @@ HEADERS: dict[str, str] = {
|
||||
async def search_ads_recipient(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
cookies: dict,
|
||||
username: str,
|
||||
) -> str:
|
||||
await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
cookies=cookies,
|
||||
data={"mode": "new", "method": "updateAdsTopupState"},
|
||||
)
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
cookies=cookies,
|
||||
data={"query": username, "method": "searchAdsTopupRecipient"},
|
||||
)
|
||||
result = parse_json_response(resp, "searchAdsTopupRecipient")
|
||||
@@ -56,14 +51,12 @@ async def search_ads_recipient(
|
||||
async def init_ads_topup(
|
||||
client: httpx.AsyncClient,
|
||||
fragment_hash: str,
|
||||
cookies: dict,
|
||||
recipient: str,
|
||||
amount: int,
|
||||
) -> str:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=HEADERS,
|
||||
cookies=cookies,
|
||||
data={"recipient": recipient, "amount": amount, "method": "initAdsTopupRequest"},
|
||||
)
|
||||
result = parse_json_response(resp, "initAdsTopupRequest")
|
||||
@@ -90,12 +83,12 @@ async def topup_ton(username: str, amount: int) -> dict:
|
||||
# logger.info("Retrieving TON wallet info")
|
||||
account = await get_account_info()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(cookies=cookies) as client:
|
||||
logger.info("Searching recipient: %s", username)
|
||||
recipient = await search_ads_recipient(client, fragment_hash, cookies, username)
|
||||
recipient = await search_ads_recipient(client, fragment_hash, username)
|
||||
|
||||
logger.info("Initializing topup request: %s TON to %s", amount, username)
|
||||
req_id = await init_ads_topup(client, fragment_hash, cookies, recipient, amount)
|
||||
req_id = await init_ads_topup(client, fragment_hash, recipient, amount)
|
||||
|
||||
# logger.info("Requesting transaction payload (req_id=%s)", req_id)
|
||||
tx_data = {
|
||||
@@ -107,11 +100,12 @@ async def topup_ton(username: str, amount: int) -> dict:
|
||||
"method": "getAdsTopupLink",
|
||||
}
|
||||
transaction = await execute_transaction_request(
|
||||
client, HEADERS, cookies, account, tx_data, fragment_hash
|
||||
client, HEADERS, account, tx_data, fragment_hash
|
||||
)
|
||||
|
||||
logger.info("Broadcasting transaction to TON blockchain")
|
||||
tx_hash = await process_transaction(transaction)
|
||||
logger.info("TON topup successful: %s TON -> %s | tx: %s", amount, username, tx_hash)
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
|
||||
+4
-5
@@ -3,7 +3,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.exceptions import RequestError, WalletError
|
||||
from app.core import RequestError, WalletError
|
||||
from app.utils.wallet import link_wallet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,23 +21,22 @@ def parse_json_response(response: httpx.Response, context: str) -> dict[str, Any
|
||||
async def execute_transaction_request(
|
||||
client: httpx.AsyncClient,
|
||||
headers: dict,
|
||||
cookies: dict,
|
||||
account: dict[str, Any],
|
||||
tx_data: dict[str, Any],
|
||||
fragment_hash: str,
|
||||
) -> dict[str, Any]:
|
||||
url = f"https://fragment.com/api?hash={fragment_hash}"
|
||||
|
||||
resp = await client.post(url, headers=headers, cookies=cookies, data=tx_data)
|
||||
resp = await client.post(url, headers=headers, data=tx_data)
|
||||
transaction = parse_json_response(resp, tx_data.get("method", "transaction"))
|
||||
|
||||
if transaction.get("need_verify"):
|
||||
if not await link_wallet(client, headers, cookies, account, fragment_hash):
|
||||
if not await link_wallet(client, headers, account, fragment_hash):
|
||||
raise WalletError(
|
||||
"Failed to link your TON wallet to Fragment. "
|
||||
"Make sure the wallet matching your cookies is used."
|
||||
)
|
||||
resp = await client.post(url, headers=headers, cookies=cookies, data=tx_data)
|
||||
resp = await client.post(url, headers=headers, data=tx_data)
|
||||
transaction = parse_json_response(resp, tx_data.get("method", "transaction"))
|
||||
|
||||
return transaction
|
||||
|
||||
@@ -19,8 +19,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def clean_decode(payload: str) -> str:
|
||||
logger.debug("Original payload: %s", payload)
|
||||
|
||||
# Pad and decode base64 → BOC bytes
|
||||
s = payload.strip()
|
||||
if not s:
|
||||
@@ -34,5 +32,5 @@ def clean_decode(payload: str) -> str:
|
||||
sl.load_uint(32) # op code — always 0 for text comment
|
||||
result = sl.load_snake_string().strip()
|
||||
|
||||
logger.debug("Decoded payload: %s", result.replace("\n", " "))
|
||||
logger.debug("Payload: %s -> %s", payload, result.replace("\n", " "))
|
||||
return result
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.exceptions import HashFetchError
|
||||
from app.core import HashFetchError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
+3
-7
@@ -7,9 +7,7 @@ import httpx
|
||||
from tonutils.clients import TonapiClient
|
||||
from tonutils.types import NetworkGlobalID
|
||||
|
||||
from app.core import config
|
||||
from app.core.constants import DEVICE, WALLET_CLASSES
|
||||
from app.core.exceptions import TransactionError, WalletError
|
||||
from app.core import DEVICE, WALLET_CLASSES, TransactionError, WalletError, config
|
||||
from app.utils.decoder import clean_decode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -58,8 +56,8 @@ async def process_transaction(transaction_data: dict) -> str:
|
||||
amount=int(message["amount"]), # nanotons, not TON
|
||||
body=payload,
|
||||
)
|
||||
|
||||
return result
|
||||
tx_hash = result.normalized_hash
|
||||
return tx_hash
|
||||
except (WalletError, TransactionError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
@@ -85,14 +83,12 @@ async def get_account_info() -> dict[str, Any]:
|
||||
async def link_wallet(
|
||||
client: httpx.AsyncClient,
|
||||
headers: dict,
|
||||
cookies: dict,
|
||||
account: dict[str, Any],
|
||||
fragment_hash: str,
|
||||
) -> bool:
|
||||
resp = await client.post(
|
||||
f"https://fragment.com/api?hash={fragment_hash}",
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
data={
|
||||
"account": json.dumps(account),
|
||||
"device": DEVICE,
|
||||
|
||||
@@ -14,9 +14,7 @@ async def topup_ton_example():
|
||||
result = await topup_ton("@bohd4nx", 100)
|
||||
|
||||
if result["success"]:
|
||||
data = result["data"]
|
||||
logger.info(f"TON topup successful: {data['amount']} TON sent to {data['username']}")
|
||||
logger.info(f"Transaction ID: {data['transaction_id']}")
|
||||
pass # Transaction successful, details are logged in the method
|
||||
else:
|
||||
logger.error(f"TON topup failed: {result['error']}")
|
||||
|
||||
@@ -28,11 +26,7 @@ async def buy_premium_example():
|
||||
result = await buy_premium("@bohd4nx", 12)
|
||||
|
||||
if result["success"]:
|
||||
data = result["data"]
|
||||
logger.info(
|
||||
f"Premium purchase successful: {data['months']} months sent to {data['username']}"
|
||||
)
|
||||
logger.info(f"Transaction ID: {data['transaction_id']}")
|
||||
pass # Transaction successful, details are logged in the method
|
||||
else:
|
||||
logger.error(f"Premium purchase failed: {result['error']}")
|
||||
|
||||
@@ -44,9 +38,7 @@ async def buy_stars_example():
|
||||
result = await buy_stars("@bohd4nx", 1000000)
|
||||
|
||||
if result["success"]:
|
||||
data = result["data"]
|
||||
logger.info(f"Stars purchase successful: {data['amount']} stars sent to {data['username']}")
|
||||
logger.info(f"Transaction ID: {data['transaction_id']}")
|
||||
pass # Transaction successful, details are logged in the method
|
||||
else:
|
||||
logger.error(f"Stars purchase failed: {result['error']}")
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""End-to-end integration test: buy 50 Stars for @bohd4nx.
|
||||
|
||||
Requires cookies.json and a valid .env (API_KEY + SEED).
|
||||
Auto-skipped when cookies are unavailable (e.g. local runs without secrets).
|
||||
Auto-skipped when cookies or config are unavailable (e.g. CI without secrets).
|
||||
"""
|
||||
|
||||
from app.methods.stars import buy_stars
|
||||
|
||||
|
||||
async def test_buy_stars_e2e(cookies):
|
||||
async def test_buy_stars_e2e(cookies, tests_config):
|
||||
result = await buy_stars("@bohd4nx", 50)
|
||||
assert result["success"] is True, result.get("error")
|
||||
assert result["data"]["transaction_id"]
|
||||
|
||||
+10
-1
@@ -1,13 +1,22 @@
|
||||
import pytest
|
||||
|
||||
from app.core.config import config
|
||||
from app.core.cookies import load_cookies
|
||||
from app.core.exceptions import CookiesError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cookies():
|
||||
"""Load Fragment cookies, skip the test if they are unavailable."""
|
||||
"""Load Fragment cookies; skip the test if they are unavailable."""
|
||||
try:
|
||||
return load_cookies()
|
||||
except CookiesError as exc:
|
||||
pytest.skip(f"Cookies unavailable — {exc}")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tests_config():
|
||||
"""Require a fully configured environment (SEED + API_KEY); skip otherwise."""
|
||||
if config is None:
|
||||
pytest.skip("Config unavailable — set SEED and API_KEY in .env or environment")
|
||||
return config
|
||||
|
||||
Reference in New Issue
Block a user