mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
feat: add wallet management features and enhance error handling; update tests and examples
This commit is contained in:
@@ -2,9 +2,9 @@ name: Lint
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: ["**"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: ["**"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ name: Publish to PyPI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["**"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ logs/
|
|||||||
# System files
|
# System files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
cookies.json
|
|
||||||
|
|
||||||
# Testing & tooling artifacts
|
# Testing & tooling artifacts
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ See the [`examples/`](examples/) folder for ready-to-run scripts.
|
|||||||
| `gift_premium(username, months, show_sender=True)` | `PremiumResult` | Gift Telegram Premium subscription | `months`: 3, 6, or 12 |
|
| `gift_premium(username, months, show_sender=True)` | `PremiumResult` | Gift Telegram Premium subscription | `months`: 3, 6, or 12 |
|
||||||
| `gift_stars(username, amount, show_sender=True)` | `StarsResult` | Gift Telegram Stars | `amount`: 50–1,000,000 |
|
| `gift_stars(username, amount, show_sender=True)` | `StarsResult` | Gift Telegram Stars | `amount`: 50–1,000,000 |
|
||||||
| `topup_ton(username, amount, show_sender=True)` | `AdsTopupResult` | Top up Telegram Ads balance | `amount`: 1–1,000,000,000 |
|
| `topup_ton(username, amount, show_sender=True)` | `AdsTopupResult` | Top up Telegram Ads balance | `amount`: 1–1,000,000,000 |
|
||||||
|
| `get_wallet()` | `WalletInfo` | Get wallet address, state, balance | — |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ async def main() -> None:
|
|||||||
wallet_version="V5R1", # or "V4R2"
|
wallet_version="V5R1", # or "V4R2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
wallet = await client.get_wallet()
|
||||||
|
|
||||||
print("FragmentClient initialized")
|
print("FragmentClient initialized")
|
||||||
print(" %-16s %s" % ("Wallet version:", client.wallet_version))
|
print(" %-16s %s" % ("Wallet version:", client.wallet_version))
|
||||||
print(" %-16s %s..." % ("API key:", client.api_key[:8]))
|
print(" %-16s %s" % ("Address:", wallet.address))
|
||||||
|
print(" %-16s %s" % ("State:", wallet.state))
|
||||||
|
print(" %-16s %s TON" % ("Balance:", wallet.balance))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
# This source code is licensed under the MIT License found in the
|
# This source code is licensed under the MIT License found in the
|
||||||
# LICENSE file in the root directory of this source tree.
|
# LICENSE file in the root directory of this source tree.
|
||||||
|
|
||||||
|
from importlib.metadata import version
|
||||||
|
|
||||||
from pyfragment.client import FragmentClient
|
from pyfragment.client import FragmentClient
|
||||||
from pyfragment.types import (
|
from pyfragment.types import (
|
||||||
AdsTopupResult,
|
AdsTopupResult,
|
||||||
@@ -21,13 +23,18 @@ from pyfragment.types import (
|
|||||||
UserNotFoundError,
|
UserNotFoundError,
|
||||||
VerificationError,
|
VerificationError,
|
||||||
WalletError,
|
WalletError,
|
||||||
|
WalletInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__version__: str = version("pyfragment")
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"__version__",
|
||||||
"FragmentClient",
|
"FragmentClient",
|
||||||
"AdsTopupResult",
|
"AdsTopupResult",
|
||||||
"PremiumResult",
|
"PremiumResult",
|
||||||
"StarsResult",
|
"StarsResult",
|
||||||
|
"WalletInfo",
|
||||||
"ClientError",
|
"ClientError",
|
||||||
"ConfigurationError",
|
"ConfigurationError",
|
||||||
"CookieError",
|
"CookieError",
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ from pyfragment.types import (
|
|||||||
CookieError,
|
CookieError,
|
||||||
PremiumResult,
|
PremiumResult,
|
||||||
StarsResult,
|
StarsResult,
|
||||||
|
WalletInfo,
|
||||||
WalletVersion,
|
WalletVersion,
|
||||||
)
|
)
|
||||||
|
from pyfragment.utils.wallet import get_wallet_info
|
||||||
|
|
||||||
|
|
||||||
class FragmentClient:
|
class FragmentClient:
|
||||||
@@ -36,6 +38,7 @@ class FragmentClient:
|
|||||||
api_key="AAABBB...",
|
api_key="AAABBB...",
|
||||||
cookies={"stel_ssid": "...", "stel_dt": "...", ...},
|
cookies={"stel_ssid": "...", "stel_dt": "...", ...},
|
||||||
)
|
)
|
||||||
|
print(await client.get_wallet())
|
||||||
result = await client.gift_premium("@username", months=6)
|
result = await client.gift_premium("@username", months=6)
|
||||||
print(result.transaction_id)
|
print(result.transaction_id)
|
||||||
"""
|
"""
|
||||||
@@ -51,6 +54,10 @@ class FragmentClient:
|
|||||||
if missing:
|
if missing:
|
||||||
raise ConfigurationError(ConfigurationError.MISSING_VARS.format(keys=", ".join(missing)))
|
raise ConfigurationError(ConfigurationError.MISSING_VARS.format(keys=", ".join(missing)))
|
||||||
|
|
||||||
|
word_count = len(seed.split())
|
||||||
|
if word_count not in (12, 18, 24):
|
||||||
|
raise ConfigurationError(ConfigurationError.INVALID_MNEMONIC.format(count=word_count))
|
||||||
|
|
||||||
if isinstance(cookies, str):
|
if isinstance(cookies, str):
|
||||||
try:
|
try:
|
||||||
cookies = json.loads(cookies)
|
cookies = json.loads(cookies)
|
||||||
@@ -112,3 +119,12 @@ class FragmentClient:
|
|||||||
:class:`AdsTopupResult` with ``transaction_id``, ``username``, ``amount``, ``timestamp``.
|
:class:`AdsTopupResult` with ``transaction_id``, ``username``, ``amount``, ``timestamp``.
|
||||||
"""
|
"""
|
||||||
return await topup_ton(self, username, amount, show_sender)
|
return await topup_ton(self, username, amount, show_sender)
|
||||||
|
|
||||||
|
async def get_wallet(self) -> WalletInfo:
|
||||||
|
"""Return the address, state and balance of the TON wallet.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`WalletInfo` with ``address`` (``"UQ..."``), ``state``
|
||||||
|
(``"active"``, ``"uninit"``, or ``"frozen"``), and ``balance`` in TON.
|
||||||
|
"""
|
||||||
|
return await get_wallet_info(self)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from pyfragment.types.exceptions import (
|
|||||||
VerificationError,
|
VerificationError,
|
||||||
WalletError,
|
WalletError,
|
||||||
)
|
)
|
||||||
from pyfragment.types.results import AdsTopupResult, PremiumResult, StarsResult
|
from pyfragment.types.results import AdsTopupResult, PremiumResult, StarsResult, WalletInfo
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# constants
|
# constants
|
||||||
@@ -58,4 +58,5 @@ __all__ = [
|
|||||||
"AdsTopupResult",
|
"AdsTopupResult",
|
||||||
"PremiumResult",
|
"PremiumResult",
|
||||||
"StarsResult",
|
"StarsResult",
|
||||||
|
"WalletInfo",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class ConfigurationError(ClientError):
|
|||||||
|
|
||||||
MISSING_VARS = "Missing required parameter(s): {keys}."
|
MISSING_VARS = "Missing required parameter(s): {keys}."
|
||||||
UNSUPPORTED_VERSION = "Unsupported wallet_version '{version}'. Must be one of: {supported}."
|
UNSUPPORTED_VERSION = "Unsupported wallet_version '{version}'. Must be one of: {supported}."
|
||||||
|
INVALID_MNEMONIC = "Invalid mnemonic: got {count} words, expected 12, 18, or 24."
|
||||||
INVALID_MONTHS = "Invalid duration. Choose 3, 6, or 12 months."
|
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_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."
|
INVALID_TON_AMOUNT = "Amount must be an integer between 1 and 1 000 000 000 TON."
|
||||||
@@ -78,9 +79,10 @@ class OperationError(FragmentError):
|
|||||||
class WalletError(OperationError):
|
class WalletError(OperationError):
|
||||||
"""Raised for TON wallet issues (connection, balance, account info)."""
|
"""Raised for TON wallet issues (connection, balance, account info)."""
|
||||||
|
|
||||||
LOW_BALANCE = "TON wallet balance is too low: {balance:.2f} TON. Minimum required is 0.056 TON."
|
LOW_BALANCE = "TON wallet balance is too low: {balance:.4f} TON available, {required:.4f} TON required."
|
||||||
BALANCE_CHECK_FAILED = "Wallet balance check failed: {exc}"
|
BALANCE_CHECK_FAILED = "Wallet balance check failed: {exc}"
|
||||||
ACCOUNT_INFO_FAILED = "Failed to retrieve wallet account info: {exc}"
|
ACCOUNT_INFO_FAILED = "Failed to retrieve wallet account info: {exc}"
|
||||||
|
WALLET_INFO_FAILED = "Failed to retrieve wallet info: {exc}"
|
||||||
|
|
||||||
|
|
||||||
class UnexpectedError(OperationError):
|
class UnexpectedError(OperationError):
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
__all__ = ["AdsTopupResult", "PremiumResult", "StarsResult"]
|
__all__ = ["AdsTopupResult", "PremiumResult", "StarsResult", "WalletInfo"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WalletInfo:
|
||||||
|
"""Wallet state returned by :meth:`FragmentClient.get_wallet`."""
|
||||||
|
|
||||||
|
address: str
|
||||||
|
state: str
|
||||||
|
balance: float
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
+36
-10
@@ -5,16 +5,13 @@ from tonutils.clients import TonapiClient
|
|||||||
from tonutils.types import NetworkGlobalID
|
from tonutils.types import NetworkGlobalID
|
||||||
|
|
||||||
from pyfragment.types import MIN_TON_BALANCE, WALLET_CLASSES, TransactionError, WalletError
|
from pyfragment.types import MIN_TON_BALANCE, WALLET_CLASSES, TransactionError, WalletError
|
||||||
|
from pyfragment.types.results import WalletInfo
|
||||||
from pyfragment.utils.decoder import clean_decode
|
from pyfragment.utils.decoder import clean_decode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pyfragment.client import FragmentClient
|
from pyfragment.client import FragmentClient
|
||||||
|
|
||||||
|
|
||||||
def _init_ton_client(client: "FragmentClient") -> TonapiClient:
|
|
||||||
return TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key)
|
|
||||||
|
|
||||||
|
|
||||||
async def process_transaction(client: "FragmentClient", transaction_data: dict) -> str:
|
async def process_transaction(client: "FragmentClient", transaction_data: dict) -> str:
|
||||||
"""Sign and broadcast a Fragment transaction to the TON network.
|
"""Sign and broadcast a Fragment transaction to the TON network.
|
||||||
|
|
||||||
@@ -35,26 +32,29 @@ async def process_transaction(client: "FragmentClient", transaction_data: dict)
|
|||||||
if "transaction" not in transaction_data or "messages" not in transaction_data["transaction"]:
|
if "transaction" not in transaction_data or "messages" not in transaction_data["transaction"]:
|
||||||
raise TransactionError(TransactionError.INVALID_PAYLOAD)
|
raise TransactionError(TransactionError.INVALID_PAYLOAD)
|
||||||
|
|
||||||
|
message = transaction_data["transaction"]["messages"][0]
|
||||||
|
amount_ton = int(message["amount"]) / 1_000_000_000
|
||||||
|
|
||||||
# TODO: Investigate 406 'inbound external message rejected before smart-contract execution'.
|
# TODO: Investigate 406 'inbound external message rejected before smart-contract execution'.
|
||||||
# This happens when the previous transaction's seqno hasn't been confirmed on-chain yet,
|
# This happens when the previous transaction's seqno hasn't been confirmed on-chain yet,
|
||||||
# causing the wallet contract to reject the new message.
|
# causing the wallet contract to reject the new message.
|
||||||
async with _init_ton_client(client) as ton:
|
async with TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key) as ton:
|
||||||
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
||||||
wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
||||||
|
|
||||||
# Check balance before broadcasting
|
# Check balance covers transaction amount + gas reserve
|
||||||
try:
|
try:
|
||||||
await wallet.refresh()
|
await wallet.refresh()
|
||||||
balance_ton = wallet.balance / 1_000_000_000
|
balance_ton = wallet.balance / 1_000_000_000
|
||||||
if balance_ton < MIN_TON_BALANCE:
|
required = amount_ton + MIN_TON_BALANCE
|
||||||
raise WalletError(WalletError.LOW_BALANCE.format(balance=balance_ton))
|
if balance_ton < required:
|
||||||
|
raise WalletError(WalletError.LOW_BALANCE.format(balance=balance_ton, required=required))
|
||||||
except WalletError:
|
except WalletError:
|
||||||
raise
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise WalletError(WalletError.BALANCE_CHECK_FAILED.format(exc=exc)) from exc
|
raise WalletError(WalletError.BALANCE_CHECK_FAILED.format(exc=exc)) from exc
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = transaction_data["transaction"]["messages"][0]
|
|
||||||
payload = clean_decode(message["payload"])
|
payload = clean_decode(message["payload"])
|
||||||
|
|
||||||
result = await wallet.transfer(
|
result = await wallet.transfer(
|
||||||
@@ -85,7 +85,7 @@ async def get_account_info(client: "FragmentClient") -> dict[str, Any]:
|
|||||||
Raises:
|
Raises:
|
||||||
WalletError: If account info cannot be retrieved.
|
WalletError: If account info cannot be retrieved.
|
||||||
"""
|
"""
|
||||||
async with _init_ton_client(client) as ton:
|
async with TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key) as ton:
|
||||||
try:
|
try:
|
||||||
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
||||||
wallet, pub_key, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
wallet, pub_key, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
||||||
@@ -98,3 +98,29 @@ async def get_account_info(client: "FragmentClient") -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise WalletError(WalletError.ACCOUNT_INFO_FAILED.format(exc=exc)) from exc
|
raise WalletError(WalletError.ACCOUNT_INFO_FAILED.format(exc=exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def get_wallet_info(client: "FragmentClient") -> "WalletInfo":
|
||||||
|
"""Return the address, state and balance of the TON wallet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Authenticated :class:`FragmentClient` instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`WalletInfo` with ``address``, ``state``, and ``balance`` in TON.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
WalletError: If the wallet state cannot be fetched.
|
||||||
|
"""
|
||||||
|
async with TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key) as ton:
|
||||||
|
try:
|
||||||
|
wallet_cls = WALLET_CLASSES[client.wallet_version]
|
||||||
|
wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
|
||||||
|
await wallet.refresh()
|
||||||
|
return WalletInfo(
|
||||||
|
address=wallet.address.to_str(is_user_friendly=True, is_bounceable=False),
|
||||||
|
state=wallet.state.value,
|
||||||
|
balance=round(wallet.balance / 1_000_000_000, 4),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
raise WalletError(WalletError.WALLET_INFO_FAILED.format(exc=exc)) from exc
|
||||||
|
|||||||
@@ -79,3 +79,16 @@ def test_whitespace_cookie_value_raises() -> None:
|
|||||||
bad = {**VALID_COOKIES, "stel_ton_token": " "}
|
bad = {**VALID_COOKIES, "stel_ton_token": " "}
|
||||||
with pytest.raises(CookieError):
|
with pytest.raises(CookieError):
|
||||||
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=bad)
|
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=bad)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_mnemonic_length_raises() -> None:
|
||||||
|
bad_seed = " ".join(["word"] * 23)
|
||||||
|
with pytest.raises(ConfigurationError):
|
||||||
|
FragmentClient(seed=bad_seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_mnemonic_lengths() -> None:
|
||||||
|
for length in (12, 18, 24):
|
||||||
|
seed = " ".join(["abandon"] * (length - 1) + ["about"])
|
||||||
|
client = FragmentClient(seed=seed, api_key=VALID_API_KEY, cookies=VALID_COOKIES)
|
||||||
|
assert len(client.seed.split()) == length
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""Unit tests for process_transaction() — balance checks before broadcast."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyfragment.types import WalletError
|
||||||
|
from pyfragment.utils.wallet import process_transaction
|
||||||
|
|
||||||
|
VALID_SEED = "abandon " * 23 + "about"
|
||||||
|
|
||||||
|
# Minimal transaction payload: 0.5 TON = 500_000_000 nanotons
|
||||||
|
TRANSACTION_DATA = {
|
||||||
|
"transaction": {
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"address": "0:852443f8599fe6a5da34fe43049ac4e0beb3071bb2bfb56635ea9421287c283a",
|
||||||
|
"amount": "500000000",
|
||||||
|
"payload": "",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _make_client(api_key: str = "test_key") -> MagicMock:
|
||||||
|
client = MagicMock()
|
||||||
|
client.api_key = api_key
|
||||||
|
client.seed = VALID_SEED.split()
|
||||||
|
client.wallet_version = "V5R1"
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def _make_wallet(balance_nanotons: int) -> MagicMock:
|
||||||
|
wallet = MagicMock()
|
||||||
|
wallet.refresh = AsyncMock()
|
||||||
|
wallet.balance = balance_nanotons
|
||||||
|
wallet.transfer = AsyncMock(return_value=MagicMock(normalized_hash="abc123"))
|
||||||
|
return wallet
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_sufficient_balance_broadcasts() -> None:
|
||||||
|
# 0.5 TON amount + 0.056 TON gas = 0.556 TON required; wallet has 1 TON
|
||||||
|
client = _make_client()
|
||||||
|
wallet = _make_wallet(balance_nanotons=1_000_000_000)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pyfragment.utils.wallet.TonapiClient") as mock_tonapi,
|
||||||
|
patch("pyfragment.utils.wallet.WALLET_CLASSES") as mock_classes,
|
||||||
|
patch("pyfragment.utils.wallet.clean_decode", return_value="50 Telegram Stars"),
|
||||||
|
):
|
||||||
|
mock_tonapi.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
|
||||||
|
mock_tonapi.return_value.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
mock_classes["V5R1"].from_mnemonic.return_value = (wallet, MagicMock(), None, None)
|
||||||
|
|
||||||
|
result = await process_transaction(client, TRANSACTION_DATA)
|
||||||
|
|
||||||
|
assert result == "abc123"
|
||||||
|
wallet.transfer.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_insufficient_balance_raises_wallet_error() -> None:
|
||||||
|
# wallet has 0.1 TON, needs 0.556 TON
|
||||||
|
client = _make_client()
|
||||||
|
wallet = _make_wallet(balance_nanotons=100_000_000)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pyfragment.utils.wallet.TonapiClient") as mock_tonapi,
|
||||||
|
patch("pyfragment.utils.wallet.WALLET_CLASSES") as mock_classes,
|
||||||
|
):
|
||||||
|
mock_tonapi.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
|
||||||
|
mock_tonapi.return_value.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
mock_classes["V5R1"].from_mnemonic.return_value = (wallet, MagicMock(), None, None)
|
||||||
|
|
||||||
|
with pytest.raises(WalletError, match="required"):
|
||||||
|
await process_transaction(client, TRANSACTION_DATA)
|
||||||
|
|
||||||
|
wallet.transfer.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_exactly_minimum_balance_broadcasts() -> None:
|
||||||
|
# exactly amount + gas: 500_000_000 + 56_000_000 = 556_000_000 nanotons
|
||||||
|
client = _make_client()
|
||||||
|
wallet = _make_wallet(balance_nanotons=556_000_000)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pyfragment.utils.wallet.TonapiClient") as mock_tonapi,
|
||||||
|
patch("pyfragment.utils.wallet.WALLET_CLASSES") as mock_classes,
|
||||||
|
patch("pyfragment.utils.wallet.clean_decode", return_value="50 Telegram Stars"),
|
||||||
|
):
|
||||||
|
mock_tonapi.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
|
||||||
|
mock_tonapi.return_value.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
mock_classes["V5R1"].from_mnemonic.return_value = (wallet, MagicMock(), None, None)
|
||||||
|
|
||||||
|
result = await process_transaction(client, TRANSACTION_DATA)
|
||||||
|
|
||||||
|
assert result == "abc123"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_one_nanoton_below_minimum_raises() -> None:
|
||||||
|
# 556_000_000 - 1 nanoton: just below threshold
|
||||||
|
client = _make_client()
|
||||||
|
wallet = _make_wallet(balance_nanotons=555_999_999)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pyfragment.utils.wallet.TonapiClient") as mock_tonapi,
|
||||||
|
patch("pyfragment.utils.wallet.WALLET_CLASSES") as mock_classes,
|
||||||
|
):
|
||||||
|
mock_tonapi.return_value.__aenter__ = AsyncMock(return_value=MagicMock())
|
||||||
|
mock_tonapi.return_value.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
mock_classes["V5R1"].from_mnemonic.return_value = (wallet, MagicMock(), None, None)
|
||||||
|
|
||||||
|
with pytest.raises(WalletError, match="required"):
|
||||||
|
await process_transaction(client, TRANSACTION_DATA)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_invalid_payload_raises_transaction_error() -> None:
|
||||||
|
from pyfragment.types import TransactionError
|
||||||
|
|
||||||
|
client = _make_client()
|
||||||
|
with pytest.raises(TransactionError):
|
||||||
|
await process_transaction(client, {"transaction": {}})
|
||||||
Reference in New Issue
Block a user