Merge pull request #3 from bohd4nx/refactor/tonutils-v2

Refactor/tonutils v2
This commit is contained in:
Bohdan 👾
2026-03-08 04:37:31 +02:00
committed by GitHub
12 changed files with 78 additions and 68 deletions
+11
View File
@@ -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 }}
+9 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+3 -11
View File
@@ -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']}")
+2 -2
View File
@@ -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
View File
@@ -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