diff --git a/.gitignore b/.gitignore index ca55ba5..cda7104 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ Thumbs.db .ruff_cache/ .coverage htmlcov/ +demo.run.py # Build & distribution dist/ diff --git a/pyfragment/client.py b/pyfragment/client.py index 677f90a..3618632 100644 --- a/pyfragment/client.py +++ b/pyfragment/client.py @@ -58,6 +58,9 @@ class FragmentClient: if word_count not in (12, 18, 24): raise ConfigurationError(ConfigurationError.INVALID_MNEMONIC.format(count=word_count)) + if len(api_key.strip()) < 68: + raise ConfigurationError(ConfigurationError.INVALID_API_KEY.format(length=len(api_key.strip()))) + if isinstance(cookies, str): try: cookies = json.loads(cookies) diff --git a/pyfragment/types/exceptions.py b/pyfragment/types/exceptions.py index b25fdbe..435a50c 100644 --- a/pyfragment/types/exceptions.py +++ b/pyfragment/types/exceptions.py @@ -12,6 +12,7 @@ class ConfigurationError(ClientError): MISSING_VARS = "Missing required parameter(s): {keys}." UNSUPPORTED_VERSION = "Unsupported wallet_version '{version}'. Must be one of: {supported}." INVALID_MNEMONIC = "Invalid mnemonic: got {count} words, expected 12, 18, or 24." + INVALID_API_KEY = "Invalid Tonapi key: got {length} characters, expected at least 68. Get one at https://tonconsole.com." INVALID_MONTHS = "Invalid duration. Choose 3, 6, or 12 months." INVALID_STARS_AMOUNT = "Amount must be an integer between 50 and 1 000 000 stars." INVALID_TON_AMOUNT = "Amount must be an integer between 1 and 1 000 000 000 TON." diff --git a/pyfragment/utils/wallet.py b/pyfragment/utils/wallet.py index 3a8fded..55680f2 100644 --- a/pyfragment/utils/wallet.py +++ b/pyfragment/utils/wallet.py @@ -1,7 +1,9 @@ +import asyncio import base64 from typing import TYPE_CHECKING, Any from tonutils.clients import TonapiClient +from tonutils.exceptions import ProviderResponseError from tonutils.types import NetworkGlobalID from pyfragment.types import MIN_TON_BALANCE, WALLET_CLASSES, TransactionError, WalletError @@ -57,12 +59,19 @@ async def process_transaction(client: "FragmentClient", transaction_data: dict) try: payload = clean_decode(message["payload"]) - result = await wallet.transfer( - destination=message["address"], - amount=int(message["amount"]), # nanotons, not TON - body=payload, - ) - return result.normalized_hash + for attempt in range(2): + try: + result = await wallet.transfer( + destination=message["address"], + amount=int(message["amount"]), # nanotons, not TON + body=payload, + ) + return result.normalized_hash + except ProviderResponseError as exc: + if exc.code == 429 and attempt == 0: + await asyncio.sleep(1) + continue + raise except (WalletError, TransactionError): raise except Exception as exc: diff --git a/pyproject.toml b/pyproject.toml index b96a1f0..1f6ab66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,13 +27,13 @@ classifiers = [ ] dependencies = [ "httpx==0.28.1", - "tonutils[pytoniq]==2.0.0", + "tonutils[pytoniq]==2.0.4", ] [project.optional-dependencies] dev = [ - "pytest>=9.0", - "pytest-asyncio>=1.0", + "pytest==9.0.2", + "pytest-asyncio==1.3.0", "ruff", "black", ] diff --git a/tests/002_test_client.py b/tests/002_test_client.py index 79b7406..152ae6d 100644 --- a/tests/002_test_client.py +++ b/tests/002_test_client.py @@ -8,7 +8,7 @@ from pyfragment import FragmentClient from pyfragment.types import ConfigurationError, CookieError VALID_SEED = "abandon " * 23 + "about" -VALID_API_KEY = "test_api_key" +VALID_API_KEY = "A" * 68 VALID_COOKIES = { "stel_ssid": "x", "stel_dt": "x", @@ -92,3 +92,8 @@ def test_valid_mnemonic_lengths() -> None: seed = " ".join(["abandon"] * (length - 1) + ["about"]) client = FragmentClient(seed=seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES) assert len(client.seed.split()) == length + + +def test_short_api_key_raises() -> None: + with pytest.raises(ConfigurationError): + FragmentClient(seed=VALID_SEED, api_key="A" * 42, cookies=VALID_COOKIES)