diff --git a/.gitignore b/.gitignore index 7e968df..9abeaf6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,7 @@ systests/ dist/ build/ *.egg-info/ +uv.lock + +#docs +*-docs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e5604..71b9d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,34 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI - Updated `DEVICE_INFO` fingerprint: Tonkeeper `appVersion` -> `26.05.0`. - Updated client docstrings and purchase examples to document all supported payment methods. +### Renamed — TON -> GRAM (ex TON) + +The TON blockchain has been rebranded to **GRAM (ex TON)**. All identifiers, messages, and documentation have been updated accordingly. + +**Public API** + +- `FragmentClient.topup_ton()` → `topup_gram()` +- `PaymentMethod.TON` → `PaymentMethod.GRAM` +- `PaymentMethod.USDT_TON` → `PaymentMethod.USDT_GRAM` +- `WalletInfo.ton_balance` → `WalletInfo.gram_balance` + +**Constants** + +- `TON_TOPUP_MIN` / `TON_TOPUP_MAX` → `GRAM_TOPUP_MIN` / `GRAM_TOPUP_MAX` +- `MIN_TON_BALANCE` → `MIN_GRAM_BALANCE` +- `USDT_TON_MASTER_ADDRESS` → `USDT_GRAM_MASTER_ADDRESS` + +**Exceptions** + +- `ConfigurationError.INVALID_TON_AMOUNT` → `INVALID_GRAM_AMOUNT` +- `WalletError.LOW_TON_BALANCE` → `LOW_GRAM_BALANCE` +- `WalletError.TON_BALANCE_CHECK_FAILED` → `GRAM_BALANCE_CHECK_FAILED` + +**Internals** + +- `pyfragment/core/constants/ton.py` → `gram.py` +- `check_ton_payment_balance()` → `check_gram_payment_balance()` + --- ## [2026.3.1] — 2026-05-29 @@ -239,6 +267,8 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI - `py.typed` marker — full PEP 561 typing support for type-checkers - `__repr__` on all result types for readable debug output +[2026.3.2]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.3.2 +[2026.3.1]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.3.1 [2026.3.0]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.3.0 [2026.2.3]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.2.3 [2026.2.2]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.2.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 581a776..991a561 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ pyfragment/ client.py — FragmentClient (public entry point) core/ — transport, cookies, constants domains/ — one package per feature domain - ads/ — recharge_ads, topup_ton + ads/ — recharge_ads, topup_gram anonymous_numbers/— buy_number, manage_number giveaways/ — giveaway_stars, giveaway_premium marketplace/ — search_usernames, search_numbers, search_gifts diff --git a/README.md b/README.md index 5d8fdb9..e2be12e 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ [![License](https://img.shields.io/github/license/bohd4nx/pyfragment?style=flat&color=blue&label=License)](LICENSE) Async Python client for the **[Fragment](https://fragment.com)** marketplace API. -Buy Stars & Premium, run giveaways, top up TON and Ads balances, +Buy Stars & Premium, run giveaways, top up GRAM (ex TON) and Ads balances, manage anonymous numbers, and search Fragment listings. -**[Documentation](https://bohd4nx.gitbook.io/pyfragment/)** · **[Examples](https://github.com/bohd4nx/pyfragment/tree/master/examples)** · **[Changelog](CHANGELOG.md)** · **[Donate TON](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5)** +**[Documentation](https://bohd4nx.gitbook.io/pyfragment/)** · **[Examples](https://github.com/bohd4nx/pyfragment/tree/master/examples)** · **[Changelog](CHANGELOG.md)** · **[Donate GRAM](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5)** @@ -53,11 +53,11 @@ async def main() -> None: }, ) as client: wallet = await client.get_wallet() - print(f"TON: {wallet.ton_balance} | USDT: {wallet.usdt_balance}") + print(f"GRAM: {wallet.gram_balance} | USDT: {wallet.usdt_balance}") recipient = "https://t.me/username" # also: @username, username - stars = await client.purchase_stars(recipient, amount=500, payment_method=PaymentMethod.USDT_TON) + stars = await client.purchase_stars(recipient, amount=500, payment_method=PaymentMethod.USDT_GRAM) if stars.auto_paid: print(f"Sent {stars.amount} Stars to {stars.username} | tx: {stars.transaction_id}") else: @@ -67,7 +67,7 @@ async def main() -> None: print(f"Expires: {stars.payment_expires_at}") print(f"QR link: {stars.payment_qr_link}") - premium = await client.purchase_premium(recipient, months=6, payment_method=PaymentMethod.TON) + premium = await client.purchase_premium(recipient, months=6, payment_method=PaymentMethod.GRAM) print(f"Sent Premium {premium.amount}m to {premium.username} | tx: {premium.transaction_id}") @@ -80,7 +80,7 @@ asyncio.run(main()) | Parameter | Type | Default | Description | | ---------------- | ------------- | -------- | ----------------------------------------------------------- | -| `seed` | `str` | — | 12- or 24-word TON wallet mnemonic | +| `seed` | `str` | — | 12- or 24-word GRAM (ex TON) wallet mnemonic | | `api_key` | `str` | — | Tonapi key from [tonconsole.com](https://tonconsole.com) | | `cookies` | `dict \| str` | — | Fragment session cookies | | `wallet_version` | `str` | `"V5R1"` | `"V4R2"` or `"V5R1"` — also accepts `WalletVersion` literal | @@ -92,7 +92,7 @@ asyncio.run(main()) ### Fragment cookies -Log in to [fragment.com](https://fragment.com) and connect your TON wallet. +Log in to [fragment.com](https://fragment.com) and connect your GRAM (ex TON) wallet. **Automatically** (recommended) — reads directly from your browser, no extension needed: @@ -118,7 +118,7 @@ Generate at [tonconsole.com](https://tonconsole.com). ### Seed phrase -12- or 24-word mnemonic from your TON wallet (**Tonkeeper → Settings → Backup**). Never share it. +12- or 24-word mnemonic from your GRAM (ex TON) wallet (**Tonkeeper → Settings → Backup**). Never share it. --- @@ -143,7 +143,7 @@ All exceptions inherit from `FragmentError`: from pyfragment import ( ConfigurationError, # invalid arguments (amount, months, payment_method…) UserNotFoundError, # recipient not found on Fragment - WalletError, # insufficient TON or USDT balance + WalletError, # insufficient GRAM (ex TON) or USDT balance TransactionError, # broadcast failed, duplicate seqno, invalid payload FragmentAPIError, # Fragment API returned an error response FragmentPageError, # page fetch or hash extraction failed diff --git a/SECURITY.md b/SECURITY.md index 475c2e7..1800853 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,4 +16,4 @@ You will receive a response within 72 hours. Once the fix is released, the advis ## Scope -This library handles sensitive credentials (TON seed phrases, Fragment session cookies, Tonapi keys). Please treat any finding that could expose or misuse these credentials as high severity. +This library handles sensitive credentials (GRAM (ex TON) seed phrases, Fragment session cookies, Tonapi keys). Please treat any finding that could expose or misuse these credentials as high severity. diff --git a/examples/client/wallet_info.py b/examples/client/wallet_info.py index 7631288..1b0de85 100644 --- a/examples/client/wallet_info.py +++ b/examples/client/wallet_info.py @@ -1,5 +1,5 @@ """ -Example: fetch wallet address, state, and separate TON/USDT balances. +Example: fetch wallet address, state, and separate GRAM (ex TON)/USDT balances. Cookies can be passed as a dict or as a JSON string. wallet_version defaults to "V5R1" — change to "V4R2" for older wallets. @@ -34,7 +34,7 @@ async def main() -> None: wallet = await client.get_wallet() print(f"Address: {wallet.address}") print(f"State: {wallet.state}") - print(f"Balance: {wallet.ton_balance} TON") + print(f"Balance: {wallet.gram_balance} GRAM (ex TON)") print(f"Balance: {wallet.usdt_balance} USDT") diff --git a/examples/purchase/recharge_ads_balance.py b/examples/purchase/recharge_ads_balance.py index 7dc0162..a3fc005 100644 --- a/examples/purchase/recharge_ads_balance.py +++ b/examples/purchase/recharge_ads_balance.py @@ -1,8 +1,8 @@ """ -Example: recharge your own Telegram Ads account with TON. +Example: recharge your own Telegram Ads account with GRAM (ex TON). -Amount must be an integer between 1 and 1 000 000 000 TON. -Your wallet must satisfy the current minimum TON threshold and transaction cost. +Amount must be an integer between 1 and 1 000 000 000 GRAM (ex TON). +Your wallet must satisfy the current minimum GRAM (ex TON) threshold and transaction cost. """ import asyncio @@ -29,7 +29,7 @@ COOKIES = { } ACCOUNT = "@mychannel" # channel or bot username linked to your Telegram Ads account -AMOUNT = 10 # 1–1 000 000 000 TON +AMOUNT = 10 # 1–1 000 000 000 GRAM (ex TON) async def main() -> None: @@ -43,7 +43,7 @@ async def main() -> None: print(f"Invalid argument: {e}") return - print(f"{result.amount} TON recharged to Ads account {ACCOUNT} | tx: {result.transaction_id}") + print(f"{result.amount} GRAM (ex TON) recharged to Ads account {ACCOUNT} | tx: {result.transaction_id}") if __name__ == "__main__": diff --git a/examples/purchase/run_premium_giveaway.py b/examples/purchase/run_premium_giveaway.py index 115fa1d..184b518 100644 --- a/examples/purchase/run_premium_giveaway.py +++ b/examples/purchase/run_premium_giveaway.py @@ -28,7 +28,7 @@ COOKIES = { CHANNEL = "https://t.me/channel" WINNERS = 10 # 1–24 000 MONTHS = 3 # 3, 6 or 12 -PAYMENT_METHOD = PaymentMethod.TON # TON, USDT_TON, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL +PAYMENT_METHOD = PaymentMethod.GRAM # GRAM, USDT_GRAM, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL async def main() -> None: diff --git a/examples/purchase/run_stars_giveaway.py b/examples/purchase/run_stars_giveaway.py index ea4d886..f685069 100644 --- a/examples/purchase/run_stars_giveaway.py +++ b/examples/purchase/run_stars_giveaway.py @@ -28,7 +28,7 @@ COOKIES = { CHANNEL = "https://t.me/channel" WINNERS = 3 # 1–15 AMOUNT = 1000 # 500–1 000 000 stars per winner -PAYMENT_METHOD = PaymentMethod.USDT_TON # TON, USDT_TON, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL +PAYMENT_METHOD = PaymentMethod.USDT_GRAM # GRAM, USDT_GRAM, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL async def main() -> None: diff --git a/examples/purchase/send_premium.py b/examples/purchase/send_premium.py index f215f45..c02d02b 100644 --- a/examples/purchase/send_premium.py +++ b/examples/purchase/send_premium.py @@ -27,7 +27,7 @@ COOKIES = { USERNAME = "https://t.me/username" MONTHS = 3 # 3, 6 or 12 -PAYMENT_METHOD = PaymentMethod.TON # TON, USDT_TON, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL +PAYMENT_METHOD = PaymentMethod.GRAM # GRAM, USDT_GRAM, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL async def main() -> None: diff --git a/examples/purchase/send_stars.py b/examples/purchase/send_stars.py index f490e65..2dacdc6 100644 --- a/examples/purchase/send_stars.py +++ b/examples/purchase/send_stars.py @@ -27,7 +27,7 @@ COOKIES = { USERNAME = "https://t.me/username" AMOUNT = 500 # 50–10 000 000 stars -PAYMENT_METHOD = PaymentMethod.USDT_TON # TON, USDT_TON, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL +PAYMENT_METHOD = PaymentMethod.USDT_GRAM # GRAM, USDT_GRAM, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL async def main() -> None: diff --git a/examples/purchase/topup_ton_balance.py b/examples/purchase/topup_ton_balance.py index e44731c..ee26860 100644 --- a/examples/purchase/topup_ton_balance.py +++ b/examples/purchase/topup_ton_balance.py @@ -1,10 +1,10 @@ """ -Example: top up TON to a recipient's Telegram balance. +Example: top up GRAM (ex TON) to a recipient's Telegram balance. -For adding TON to a Telegram Ads account, use recharge_ads() instead. +For adding GRAM (ex TON) to a Telegram Ads account, use recharge_ads() instead. -Amount must be an integer between 1 and 1 000 000 000 TON. -Your wallet must satisfy the current minimum TON threshold and transaction cost. +Amount must be an integer between 1 and 1 000 000 000 GRAM (ex TON). +Your wallet must satisfy the current minimum GRAM (ex TON) threshold and transaction cost. """ import asyncio @@ -31,13 +31,13 @@ COOKIES = { } USERNAME = "@username" -AMOUNT = 10 # 1–1 000 000 000 TON +AMOUNT = 10 # 1–1 000 000 000 GRAM (ex TON) async def main() -> None: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: try: - result = await client.topup_ton(USERNAME, amount=AMOUNT, show_sender=True) + result = await client.topup_gram(USERNAME, amount=AMOUNT, show_sender=True) except UserNotFoundError: print(f"User {USERNAME} was not found on fragment.com — check the username and try again.") return @@ -48,7 +48,7 @@ async def main() -> None: print(f"Invalid argument: {e}") return - print(f"{result.amount} TON successfully topped up for {result.username} | tx: {result.transaction_id}") + print(f"{result.amount} GRAM (ex TON) successfully topped up for {result.username} | tx: {result.transaction_id}") if __name__ == "__main__": diff --git a/pyfragment/client.py b/pyfragment/client.py index 2a90711..3b81841 100644 --- a/pyfragment/client.py +++ b/pyfragment/client.py @@ -36,7 +36,7 @@ class FragmentClient: connected with Fragment or Telegram. Args: - seed: 12- or 24-word mnemonic phrase for the TON wallet. + seed: 12- or 24-word mnemonic phrase for the GRAM (ex TON) wallet. api_key: Tonapi API key — get one at https://tonconsole.com. cookies: Fragment session cookies as a dict or JSON string. wallet_version: Wallet contract version — ``"V4R2"`` or ``"V5R1"`` (default). @@ -140,7 +140,7 @@ class FragmentClient: username: str, months: int, show_sender: bool = True, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> PremiumResult: """Gift Telegram Premium to a user. @@ -148,7 +148,7 @@ class FragmentClient: username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``. months: Duration — ``3``, ``6``, or ``12``. show_sender: Show your name as the sender. Defaults to ``True``. - payment_method: Payment currency — ``"ton"`` (default), ``"usdt_ton"``, + payment_method: Payment currency — ``"ton"`` (GRAM (ex TON), default), ``"usdt_ton"`` (USDT on GRAM (ex TON)), ``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``, or ``"usdc_pol"``. @@ -162,7 +162,7 @@ class FragmentClient: username: str, amount: int, show_sender: bool = True, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> StarsResult: """Send Telegram Stars to a user. @@ -170,7 +170,7 @@ class FragmentClient: username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``. amount: Number of stars — integer from ``50`` to ``10 000 000``. show_sender: Show your name as the gift sender. Defaults to ``True``. - payment_method: Payment currency — ``"ton"`` (default), ``"usdt_ton"``, + payment_method: Payment currency — ``"ton"`` (GRAM (ex TON), default), ``"usdt_ton"`` (USDT on GRAM (ex TON)), ``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``, or ``"usdc_pol"``. @@ -179,18 +179,18 @@ class FragmentClient: """ return await self.purchases.purchase_stars(username, amount, show_sender=show_sender, payment_method=payment_method) - async def topup_ton(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: - """Top up TON to a recipient's Telegram balance. + async def topup_gram(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: + """Top up GRAM (ex TON) to a recipient's Telegram balance. Args: username: Recipient's Telegram username (with or without ``@``). - amount: Amount in TON — integer from ``1`` to ``1 000 000 000``. + amount: Amount in GRAM (ex TON) — integer from ``1`` to ``1 000 000 000``. show_sender: Show your name as the sender. Defaults to ``True``. Returns: :class:`AdsTopupResult` with ``transaction_id``, ``username``, and ``amount``. """ - return await self.ads.topup_ton(username, amount, show_sender=show_sender) + return await self.ads.topup_gram(username, amount, show_sender=show_sender) async def recharge_ads(self, account: str, amount: int) -> AdsRechargeResult: """Add funds to your own Telegram Ads account. @@ -198,7 +198,7 @@ class FragmentClient: Args: account: Your Fragment Ads account identifier — the channel or bot username the Ads account is linked to (e.g. ``"@mychannel"``). - amount: Amount in TON — integer from ``1`` to ``1 000 000 000``. + amount: Amount in GRAM (ex TON) — integer from ``1`` to ``1 000 000 000``. Returns: :class:`AdsRechargeResult` with ``transaction_id`` and ``amount``. @@ -211,7 +211,7 @@ class FragmentClient: Returns: :class:`WalletInfo` with ``address`` (``"UQ..."``), ``state`` (``"active"``, ``"uninit"``, ``"nonexist"``, or ``"frozen"``), - ``ton_balance`` in TON, and ``usdt_balance`` in USDT. + ``gram_balance`` in GRAM (ex TON), and ``usdt_balance`` in USDT. """ return await self.tonapi.get_wallet() @@ -220,7 +220,7 @@ class FragmentClient: channel: str, winners: int, amount: int, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> StarsGiveawayResult: """Run a Telegram Stars giveaway for a channel. @@ -228,7 +228,7 @@ class FragmentClient: channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``. winners: Number of winners — integer from ``1`` to ``15``. amount: Stars each winner receives — integer from ``500`` to ``1 000 000``. - payment_method: Payment currency — ``"ton"`` (default), ``"usdt_ton"``, + payment_method: Payment currency — ``"ton"`` (GRAM (ex TON), default), ``"usdt_ton"`` (USDT on GRAM (ex TON)), ``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``, or ``"usdc_pol"``. @@ -243,7 +243,7 @@ class FragmentClient: channel: str, winners: int, months: int = 3, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> PremiumGiveawayResult: """Run a Telegram Premium giveaway for a channel. @@ -251,7 +251,7 @@ class FragmentClient: channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``. winners: Number of winners — integer from ``1`` to ``24 000``. months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``. - payment_method: Payment currency — ``"ton"`` (default), ``"usdt_ton"``, + payment_method: Payment currency — ``"ton"`` (GRAM (ex TON), default), ``"usdt_ton"`` (USDT on GRAM (ex TON)), ``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``, or ``"usdc_pol"``. diff --git a/pyfragment/core/constants/__init__.py b/pyfragment/core/constants/__init__.py index 4e5be9a..7dd2c93 100644 --- a/pyfragment/core/constants/__init__.py +++ b/pyfragment/core/constants/__init__.py @@ -1,8 +1,11 @@ from __future__ import annotations +from pyfragment.core.constants.gram import DEVICE_INFO, USDT_GRAM_MASTER_ADDRESS from pyfragment.core.constants.headers import BASE_HEADERS, DEFAULT_TIMEOUT, REQUIRED_COOKIE_KEYS from pyfragment.core.constants.limits import ( - MIN_TON_BALANCE, + GRAM_TOPUP_MAX, + GRAM_TOPUP_MIN, + MIN_GRAM_BALANCE, MIN_USDT_BALANCE, MNEMONIC_WORD_COUNTS_VALID, PREMIUM_MONTHS_VALID, @@ -14,11 +17,8 @@ from pyfragment.core.constants.limits import ( STARS_PURCHASE_MIN, STARS_WINNERS_MAX, STARS_WINNERS_MIN, - TON_TOPUP_MAX, - TON_TOPUP_MIN, TONAPI_KEY_MIN_LENGTH, ) -from pyfragment.core.constants.ton import DEVICE_INFO, USDT_TON_MASTER_ADDRESS from pyfragment.core.constants.urls import ( ADS_TOPUP_PAGE, FRAGMENT_BASE_URL, @@ -39,7 +39,9 @@ __all__ = [ "FRAGMENT_BASE_URL", "FRAGMENT_DOMAIN", "GIFTS_PAGE", - "MIN_TON_BALANCE", + "GRAM_TOPUP_MAX", + "GRAM_TOPUP_MIN", + "MIN_GRAM_BALANCE", "MIN_USDT_BALANCE", "MNEMONIC_WORD_COUNTS_VALID", "NUMBERS_PAGE", @@ -58,7 +60,5 @@ __all__ = [ "STARS_WINNERS_MAX", "STARS_WINNERS_MIN", "TONAPI_KEY_MIN_LENGTH", - "TON_TOPUP_MAX", - "TON_TOPUP_MIN", - "USDT_TON_MASTER_ADDRESS", + "USDT_GRAM_MASTER_ADDRESS", ] diff --git a/pyfragment/core/constants/ton.py b/pyfragment/core/constants/gram.py similarity index 82% rename from pyfragment/core/constants/ton.py rename to pyfragment/core/constants/gram.py index 9f6cbd2..f1cc6cc 100644 --- a/pyfragment/core/constants/ton.py +++ b/pyfragment/core/constants/gram.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any -USDT_TON_MASTER_ADDRESS: str = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" +USDT_GRAM_MASTER_ADDRESS: str = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" DEVICE_INFO: dict[str, Any] = { "platform": "iphone", diff --git a/pyfragment/core/constants/limits.py b/pyfragment/core/constants/limits.py index b054c80..bb35890 100644 --- a/pyfragment/core/constants/limits.py +++ b/pyfragment/core/constants/limits.py @@ -16,12 +16,12 @@ STARS_WINNERS_MAX: int = 15 PREMIUM_WINNERS_MIN: int = 1 PREMIUM_WINNERS_MAX: int = 24_000 -# TON topup / Ads recharge amount -TON_TOPUP_MIN: int = 1 -TON_TOPUP_MAX: int = 1_000_000_000 +# GRAM (ex TON) topup / Ads recharge amount +GRAM_TOPUP_MIN: int = 1 +GRAM_TOPUP_MAX: int = 1_000_000_000 # Wallet minimum balances -MIN_TON_BALANCE: float = 0.33 +MIN_GRAM_BALANCE: float = 0.33 MIN_USDT_BALANCE: float = 0.75 # Premium subscription durations (months) diff --git a/pyfragment/domains/ads/__init__.py b/pyfragment/domains/ads/__init__.py index 50fd95a..682fbb5 100644 --- a/pyfragment/domains/ads/__init__.py +++ b/pyfragment/domains/ads/__init__.py @@ -1,5 +1,5 @@ from pyfragment.domains.ads.recharge import recharge_ads from pyfragment.domains.ads.service import AdsService -from pyfragment.domains.ads.tonup import topup_ton +from pyfragment.domains.ads.tonup import topup_gram -__all__ = ["AdsService", "recharge_ads", "topup_ton"] +__all__ = ["AdsService", "recharge_ads", "topup_gram"] diff --git a/pyfragment/domains/ads/recharge.py b/pyfragment/domains/ads/recharge.py index c79806a..ac7bf7c 100644 --- a/pyfragment/domains/ads/recharge.py +++ b/pyfragment/domains/ads/recharge.py @@ -4,7 +4,7 @@ import json import logging from typing import TYPE_CHECKING -from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE_INFO, TON_TOPUP_MAX, TON_TOPUP_MIN +from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE_INFO, GRAM_TOPUP_MAX, GRAM_TOPUP_MIN from pyfragment.domains.tonapi.account import get_account_info from pyfragment.domains.tonapi.transaction import process_transaction from pyfragment.exceptions import ConfigurationError, FragmentAPIError, FragmentError, UnexpectedError, VerificationError @@ -18,8 +18,8 @@ logger = logging.getLogger(__name__) async def recharge_ads(client: FragmentClient, account: str, amount: int) -> AdsRechargeResult: - if not isinstance(amount, int) or not (TON_TOPUP_MIN <= amount <= TON_TOPUP_MAX): - raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT) + if not isinstance(amount, int) or not (GRAM_TOPUP_MIN <= amount <= GRAM_TOPUP_MAX): + raise ConfigurationError(ConfigurationError.INVALID_GRAM_AMOUNT) try: await client.call("updateAdsState", {"mode": "new"}, page_url=ADS_TOPUP_PAGE) @@ -47,8 +47,8 @@ async def recharge_ads(client: FragmentClient, account: str, amount: int) -> Ads return AdsRechargeResult(transaction_id=tx_hash, amount=amount) except FragmentError as exc: - logger.error("Failed to recharge Ads account '%s' for %s TON: %s", account, amount, exc, exc_info=True) + logger.error("Failed to recharge Ads account '%s' for %s GRAM (ex TON): %s", account, amount, exc, exc_info=True) raise except Exception as exc: - logger.exception("Failed to recharge Ads account '%s' for %s TON due to an unexpected error", account, amount) + logger.exception("Failed to recharge Ads account '%s' for %s GRAM (ex TON) due to an unexpected error", account, amount) raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc diff --git a/pyfragment/domains/ads/service.py b/pyfragment/domains/ads/service.py index ce310cc..35f3640 100644 --- a/pyfragment/domains/ads/service.py +++ b/pyfragment/domains/ads/service.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING from pyfragment.domains.ads.recharge import recharge_ads -from pyfragment.domains.ads.tonup import topup_ton +from pyfragment.domains.ads.tonup import topup_gram from pyfragment.domains.base import BaseService from pyfragment.models.payments import AdsRechargeResult, AdsTopupResult @@ -15,5 +15,5 @@ class AdsService(BaseService): async def recharge_ads(self, account: str, amount: int) -> AdsRechargeResult: return await recharge_ads(self._client, account, amount) - async def topup_ton(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: - return await topup_ton(self._client, username, amount, show_sender=show_sender) + async def topup_gram(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: + return await topup_gram(self._client, username, amount, show_sender=show_sender) diff --git a/pyfragment/domains/ads/tonup.py b/pyfragment/domains/ads/tonup.py index 40d999b..f04bb56 100644 --- a/pyfragment/domains/ads/tonup.py +++ b/pyfragment/domains/ads/tonup.py @@ -4,7 +4,7 @@ import json import logging from typing import TYPE_CHECKING -from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE_INFO, TON_TOPUP_MAX, TON_TOPUP_MIN +from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE_INFO, GRAM_TOPUP_MAX, GRAM_TOPUP_MIN from pyfragment.domains.payments import parse_required_payment_amount from pyfragment.domains.tonapi.account import get_account_info from pyfragment.domains.tonapi.transaction import process_transaction @@ -25,9 +25,9 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -async def topup_ton(client: FragmentClient, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: - if not isinstance(amount, int) or not (TON_TOPUP_MIN <= amount <= TON_TOPUP_MAX): - raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT) +async def topup_gram(client: FragmentClient, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: + if not isinstance(amount, int) or not (GRAM_TOPUP_MIN <= amount <= GRAM_TOPUP_MAX): + raise ConfigurationError(ConfigurationError.INVALID_GRAM_AMOUNT) try: await client.call("updateAdsTopupState", {"mode": "new"}, page_url=ADS_TOPUP_PAGE) @@ -41,7 +41,7 @@ async def topup_ton(client: FragmentClient, username: str, amount: int, show_sen required_payment_amount = parse_required_payment_amount(result) req_id = result.get("req_id") if not req_id: - raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="TON topup")) + raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="GRAM (ex TON) topup")) account = await get_account_info(client) transaction = await client.call( @@ -62,8 +62,12 @@ async def topup_ton(client: FragmentClient, username: str, amount: int, show_sen return AdsTopupResult(transaction_id=tx_hash, username=username, amount=amount) except FragmentError as exc: - logger.error("Failed to top up TON for user '%s' with %s TON: %s", username, amount, exc, exc_info=True) + logger.error( + "Failed to top up GRAM (ex TON) for user '%s' with %s GRAM (ex TON): %s", username, amount, exc, exc_info=True + ) raise except Exception as exc: - logger.exception("Failed to top up TON for user '%s' with %s TON due to an unexpected error", username, amount) + logger.exception( + "Failed to top up GRAM (ex TON) for user '%s' with %s GRAM (ex TON) due to an unexpected error", username, amount + ) raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc diff --git a/pyfragment/domains/giveaways/giveaway.py b/pyfragment/domains/giveaways/giveaway.py index b92adfb..3ca2245 100644 --- a/pyfragment/domains/giveaways/giveaway.py +++ b/pyfragment/domains/giveaways/giveaway.py @@ -48,7 +48,7 @@ async def giveaway_stars( channel: str, winners: int, amount: int, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> StarsGiveawayResult: if not isinstance(winners, int) or not (STARS_WINNERS_MIN <= winners <= STARS_WINNERS_MAX): raise ConfigurationError(ConfigurationError.INVALID_WINNERS_STARS) @@ -143,7 +143,7 @@ async def giveaway_premium( channel: str, winners: int, months: int = 3, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> PremiumGiveawayResult: if not isinstance(winners, int) or not (PREMIUM_WINNERS_MIN <= winners <= PREMIUM_WINNERS_MAX): raise ConfigurationError(ConfigurationError.INVALID_WINNERS_PREMIUM) diff --git a/pyfragment/domains/giveaways/service.py b/pyfragment/domains/giveaways/service.py index db70b05..fd85eb6 100644 --- a/pyfragment/domains/giveaways/service.py +++ b/pyfragment/domains/giveaways/service.py @@ -17,7 +17,7 @@ class GiveawaysService(BaseService): channel: str, winners: int, amount: int, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> StarsGiveawayResult: return await giveaway_stars(self._client, channel, winners, amount, payment_method=payment_method) @@ -26,6 +26,6 @@ class GiveawaysService(BaseService): channel: str, winners: int, months: int = 3, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> PremiumGiveawayResult: return await giveaway_premium(self._client, channel, winners, months, payment_method=payment_method) diff --git a/pyfragment/domains/purchases/purchase.py b/pyfragment/domains/purchases/purchase.py index 8449dc6..515de8d 100644 --- a/pyfragment/domains/purchases/purchase.py +++ b/pyfragment/domains/purchases/purchase.py @@ -45,7 +45,7 @@ async def purchase_stars( username: str, amount: int, show_sender: bool = True, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> StarsResult: if not isinstance(amount, int) or not (STARS_PURCHASE_MIN <= amount <= STARS_PURCHASE_MAX): raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT) @@ -128,7 +128,7 @@ async def purchase_premium( username: str, months: int, show_sender: bool = True, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> PremiumResult: if months not in PREMIUM_MONTHS_VALID: raise ConfigurationError(ConfigurationError.INVALID_MONTHS) diff --git a/pyfragment/domains/purchases/service.py b/pyfragment/domains/purchases/service.py index 638d3db..cc3c18d 100644 --- a/pyfragment/domains/purchases/service.py +++ b/pyfragment/domains/purchases/service.py @@ -17,7 +17,7 @@ class PurchasesService(BaseService): username: str, amount: int, show_sender: bool = True, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> StarsResult: return await purchase_stars(self._client, username, amount, show_sender=show_sender, payment_method=payment_method) @@ -26,6 +26,6 @@ class PurchasesService(BaseService): username: str, months: int, show_sender: bool = True, - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, ) -> PremiumResult: return await purchase_premium(self._client, username, months, show_sender=show_sender, payment_method=payment_method) diff --git a/pyfragment/domains/tonapi/account.py b/pyfragment/domains/tonapi/account.py index 52aab22..c386c74 100644 --- a/pyfragment/domains/tonapi/account.py +++ b/pyfragment/domains/tonapi/account.py @@ -9,7 +9,7 @@ from tonutils.clients import TonapiClient from tonutils.contracts.jetton import get_wallet_address_get_method, get_wallet_data_get_method from tonutils.exceptions import ProviderResponseError -from pyfragment.core.constants import MIN_TON_BALANCE, MIN_USDT_BALANCE, USDT_TON_MASTER_ADDRESS +from pyfragment.core.constants import MIN_GRAM_BALANCE, MIN_USDT_BALANCE, USDT_GRAM_MASTER_ADDRESS from pyfragment.exceptions import WalletError from pyfragment.models.enums import WALLET_CLASSES from pyfragment.models.wallet import WalletInfo @@ -22,11 +22,11 @@ logger = logging.getLogger(__name__) async def get_usdt_balance(ton: Any, wallet_address: str) -> float: - """Return the USDT balance for a Fragment-linked TON wallet.""" + """Return the USDT balance for a Fragment-linked GRAM (ex TON) wallet.""" try: jetton_wallet_address = await get_wallet_address_get_method( client=ton, - address=USDT_TON_MASTER_ADDRESS, + address=USDT_GRAM_MASTER_ADDRESS, owner_address=wallet_address, ) wallet_data = await get_wallet_data_get_method(client=ton, address=jetton_wallet_address) @@ -43,40 +43,40 @@ async def get_usdt_balance(ton: Any, wallet_address: str) -> float: raise WalletError(WalletError.USDT_BALANCE_CHECK_FAILED.format(exc=exc)) from exc -async def check_ton_payment_balance( - balance_ton: float, - amount_ton: float, +async def check_gram_payment_balance( + balance_gram: float, + amount_gram: float, required_payment_amount: float | None, ) -> None: - """Validate that the TON wallet can cover a TON-denominated payment.""" - tx_price_ton = amount_ton + """Validate that the GRAM (ex TON) wallet can cover a GRAM (ex TON)-denominated payment.""" + tx_price_gram = amount_gram if required_payment_amount is not None and required_payment_amount > 0: - tx_price_ton = max(tx_price_ton, required_payment_amount) + tx_price_gram = max(tx_price_gram, required_payment_amount) - required_ton = max(tx_price_ton, MIN_TON_BALANCE) - if balance_ton < required_ton: + required_gram = max(tx_price_gram, MIN_GRAM_BALANCE) + if balance_gram < required_gram: logger.error( - "Failed TON balance check: balance=%s TON, required=%s TON", - round(balance_ton, 6), - round(required_ton, 6), + "Failed GRAM (ex TON) balance check: balance=%s GRAM (ex TON), required=%s GRAM (ex TON)", + round(balance_gram, 6), + round(required_gram, 6), ) - raise WalletError(WalletError.LOW_TON_BALANCE.format(balance=balance_ton, required=required_ton)) + raise WalletError(WalletError.LOW_GRAM_BALANCE.format(balance=balance_gram, required=required_gram)) async def check_usdt_payment_balance( - balance_ton: float, + balance_gram: float, required_payment_amount: float | None, ton: Any, wallet_address: str, ) -> None: """Validate that the wallet can cover a USDT-denominated payment.""" - if balance_ton < MIN_TON_BALANCE: + if balance_gram < MIN_GRAM_BALANCE: logger.error( - "Failed TON gas reserve check for USDT payment: balance=%s TON, required=%s TON", - round(balance_ton, 6), - MIN_TON_BALANCE, + "Failed GRAM (ex TON) gas reserve check for USDT payment: balance=%s GRAM (ex TON), required=%s GRAM (ex TON)", + round(balance_gram, 6), + MIN_GRAM_BALANCE, ) - raise WalletError(WalletError.LOW_TON_BALANCE.format(balance=balance_ton, required=MIN_TON_BALANCE)) + raise WalletError(WalletError.LOW_GRAM_BALANCE.format(balance=balance_gram, required=MIN_GRAM_BALANCE)) usdt_balance = await get_usdt_balance(ton, wallet_address) required_usdt = required_payment_amount if required_payment_amount is not None else MIN_USDT_BALANCE @@ -109,7 +109,7 @@ async def get_account_info(client: FragmentClient) -> dict[str, Any]: async def get_wallet_info(client: FragmentClient) -> WalletInfo: - """Fetch the wallet address, chain state, and TON/USDT balances.""" + """Fetch the wallet address, chain state, and GRAM (ex TON)/USDT balances.""" async with TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key) as ton: try: wallet_cls = WALLET_CLASSES[client.wallet_version] @@ -120,7 +120,7 @@ async def get_wallet_info(client: FragmentClient) -> WalletInfo: return WalletInfo( address=wallet.address.to_str(is_user_friendly=True, is_bounceable=False), state=wallet.state.value, - ton_balance=round(wallet.balance / 1_000_000_000, 4), + gram_balance=round(wallet.balance / 1_000_000_000, 4), usdt_balance=round(usdt_balance, 4), ) except Exception as exc: diff --git a/pyfragment/domains/tonapi/transaction.py b/pyfragment/domains/tonapi/transaction.py index 731c1c5..49910a8 100644 --- a/pyfragment/domains/tonapi/transaction.py +++ b/pyfragment/domains/tonapi/transaction.py @@ -11,7 +11,7 @@ from ton_core import Cell, NetworkGlobalID from tonutils.clients import TonapiClient from tonutils.exceptions import ProviderResponseError -from pyfragment.domains.tonapi.account import check_ton_payment_balance, check_usdt_payment_balance +from pyfragment.domains.tonapi.account import check_gram_payment_balance, check_usdt_payment_balance from pyfragment.exceptions import ParseError, TransactionError, WalletError from pyfragment.models.enums import WALLET_CLASSES, PaymentMethod @@ -26,7 +26,7 @@ def clean_decode(payload: str) -> str | Cell: """Decode a base64 BOC comment from Fragment into text when possible. Some Fragment payloads are plain text comments, while others are structured - TON messages such as jetton transfers. Non-text payloads are returned as a + GRAM (ex TON) messages such as jetton transfers. Non-text payloads are returned as a `Cell` so the caller can keep the raw binary structure. """ s = payload.strip() @@ -39,7 +39,7 @@ def clean_decode(payload: str) -> str | Cell: sl = cell.begin_parse() op = sl.load_uint(32) if op != 0: - # Non-zero op code means this is a structured TON message, not a plain text comment. + # Non-zero op code means this is a structured GRAM (ex TON) message, not a plain text comment. return cell try: return sl.load_snake_string().strip() @@ -62,7 +62,7 @@ def _extract_message(transaction_data: dict[str, Any]) -> dict[str, Any]: async def _check_payment_balances( wallet: Any, payment_method: PaymentMethod, - amount_ton: float, + amount_gram: float, required_payment_amount: float | None, transaction_data: dict[str, Any], ton: Any, @@ -70,18 +70,18 @@ async def _check_payment_balances( """Refresh wallet and verify sufficient balance before broadcasting.""" try: await wallet.refresh() - balance_ton = wallet.balance / 1_000_000_000 + balance_gram = wallet.balance / 1_000_000_000 if payment_method == "ton": - await check_ton_payment_balance(balance_ton, amount_ton, required_payment_amount) + await check_gram_payment_balance(balance_gram, amount_gram, required_payment_amount) else: # USDT is paid from the Fragment-linked wallet, not the signing wallet. fragment_wallet_address = transaction_data["transaction"].get("from", "") - await check_usdt_payment_balance(balance_ton, required_payment_amount, ton, fragment_wallet_address) + await check_usdt_payment_balance(balance_gram, required_payment_amount, ton, fragment_wallet_address) except WalletError: raise except Exception as exc: logger.exception("Failed to validate balances before broadcasting transaction") - raise WalletError(WalletError.TON_BALANCE_CHECK_FAILED.format(exc=exc)) from exc + raise WalletError(WalletError.GRAM_BALANCE_CHECK_FAILED.format(exc=exc)) from exc async def _broadcast_with_retry(wallet: Any, message: dict[str, Any], payload: str | Cell) -> str: @@ -90,7 +90,7 @@ async def _broadcast_with_retry(wallet: Any, message: dict[str, Any], payload: s try: result = await wallet.transfer( destination=message["address"], - amount=int(message["amount"]), # nanotons, not TON + amount=int(message["amount"]), # nanograms, not GRAM (ex TON) body=payload, ) return str(result.normalized_hash) @@ -124,10 +124,10 @@ async def _broadcast_with_retry(wallet: Any, message: dict[str, Any], payload: s async def process_transaction( client: FragmentClient, transaction_data: dict[str, Any], - payment_method: PaymentMethod = PaymentMethod.TON, + payment_method: PaymentMethod = PaymentMethod.GRAM, required_payment_amount: float | None = None, ) -> str: - """Sign and broadcast a Fragment transaction with the seeded TON wallet. + """Sign and broadcast a Fragment transaction with the seeded GRAM (ex TON) wallet. Args: client: Authenticated `FragmentClient` instance. @@ -139,13 +139,13 @@ async def process_transaction( Normalized transaction hash string. """ message = _extract_message(transaction_data) - amount_ton = int(message["amount"]) / 1_000_000_000 + amount_gram = int(message["amount"]) / 1_000_000_000 async with TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key) as ton: wallet_cls = WALLET_CLASSES[client.wallet_version] wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed) - await _check_payment_balances(wallet, payment_method, amount_ton, required_payment_amount, transaction_data, ton) + await _check_payment_balances(wallet, payment_method, amount_gram, required_payment_amount, transaction_data, ton) payload = clean_decode(str(message.get("payload", ""))) @@ -161,7 +161,7 @@ async def process_transaction( raise TransactionError(TransactionError.BROADCAST_FAILED_SSL.format(exc=exc)) from exc cause = cause.__cause__ or cause.__context__ logger.exception( - "Failed to broadcast transaction to '%s' for %s nanotons using payment method '%s'", + "Failed to broadcast transaction to '%s' for %s nanograms using payment method '%s'", message["address"], message["amount"], payment_method, diff --git a/pyfragment/exceptions.py b/pyfragment/exceptions.py index 898396c..e49a7b1 100644 --- a/pyfragment/exceptions.py +++ b/pyfragment/exceptions.py @@ -1,6 +1,8 @@ from __future__ import annotations from pyfragment.core.constants.limits import ( + GRAM_TOPUP_MAX, + GRAM_TOPUP_MIN, MNEMONIC_WORD_COUNTS_VALID, PREMIUM_MONTHS_VALID, PREMIUM_WINNERS_MAX, @@ -11,8 +13,6 @@ from pyfragment.core.constants.limits import ( STARS_PURCHASE_MIN, STARS_WINNERS_MAX, STARS_WINNERS_MIN, - TON_TOPUP_MAX, - TON_TOPUP_MIN, TONAPI_KEY_MIN_LENGTH, ) @@ -39,7 +39,7 @@ class ConfigurationError(ClientError): INVALID_STARS_AMOUNT = ( f"Invalid Stars amount: must be an integer between {STARS_PURCHASE_MIN:,} and {STARS_PURCHASE_MAX:,}." ) - INVALID_TON_AMOUNT = f"Invalid TON amount: must be an integer between {TON_TOPUP_MIN:,} and {TON_TOPUP_MAX:,}." + INVALID_GRAM_AMOUNT = f"Invalid GRAM (ex TON) amount: must be an integer between {GRAM_TOPUP_MIN:,} and {GRAM_TOPUP_MAX:,}." INVALID_USERNAME = ( "Invalid username '{username}'. " "Must be 5-32 characters and contain only letters (A-Z, a-z), digits (0-9), or underscores (_)." @@ -70,7 +70,7 @@ class CookieError(ClientError): ) MISSING_BROWSER_KEYS = ( "Fragment cookies not found in {browser}: {keys}. " - "Make sure you are logged in to {url} and have connected your TON wallet in {browser}." + "Make sure you are logged in to {url} and have connected your GRAM (ex TON) wallet in {browser}." ) EXPIRED = "Fragment session cookie expired at {expires}. Log in to fragment.com in your browser and extract fresh cookies." @@ -111,7 +111,7 @@ class AnonymousNumberError(FragmentAPIError): class TransactionError(FragmentAPIError): - """Raised when a TON transaction fails to build or broadcast.""" + """Raised when a GRAM (ex TON) transaction fails to build or broadcast.""" INVALID_PAYLOAD = "Fragment returned an invalid transaction payload: 'transaction.messages' is missing or empty." BROADCAST_FAILED = "Transaction broadcast failed: {exc}" @@ -123,7 +123,7 @@ class TransactionError(FragmentAPIError): "located in your Python installation folder." ) DUPLICATE_SEQNO = ( - "Transaction broadcast failed: the TON wallet rejected the message " + "Transaction broadcast failed: the GRAM (ex TON) wallet rejected the message " "because a previous transaction with the same sequence number (seqno) " "is still pending confirmation on-chain.\n" "Wait a few seconds for the previous transaction to confirm, then retry." @@ -150,14 +150,16 @@ class OperationError(FragmentError): class WalletError(OperationError): - """Raised for TON wallet issues (connection, balance, account info).""" + """Raised for GRAM (ex TON) wallet issues (connection, balance, account info).""" - LOW_TON_BALANCE = "Insufficient TON balance: {balance:.4f} TON available, {required:.4f} TON required." + LOW_GRAM_BALANCE = ( + "Insufficient GRAM (ex TON) balance: {balance:.4f} GRAM (ex TON) available, {required:.4f} GRAM (ex TON) required." + ) LOW_USDT_BALANCE = "Insufficient USDT balance: {balance:.4f} USDT available, {required:.4f} USDT required." - TON_BALANCE_CHECK_FAILED = "Failed to fetch TON balance: {exc}" + GRAM_BALANCE_CHECK_FAILED = "Failed to fetch GRAM (ex TON) balance: {exc}" USDT_BALANCE_CHECK_FAILED = "Failed to fetch USDT balance: {exc}" - ACCOUNT_INFO_FAILED = "Failed to retrieve wallet account info from TON network: {exc}" - WALLET_INFO_FAILED = "Failed to retrieve wallet info from TON network: {exc}" + ACCOUNT_INFO_FAILED = "Failed to retrieve wallet account info from GRAM (ex TON) network: {exc}" + WALLET_INFO_FAILED = "Failed to retrieve wallet info from GRAM (ex TON) network: {exc}" class UnexpectedError(OperationError): diff --git a/pyfragment/models/enums.py b/pyfragment/models/enums.py index 5b0a4a7..3d7a94f 100644 --- a/pyfragment/models/enums.py +++ b/pyfragment/models/enums.py @@ -15,8 +15,8 @@ else: class PaymentMethod(StrEnum): - TON = "ton" - USDT_TON = "usdt_ton" + GRAM = "ton" + USDT_GRAM = "usdt_ton" # Not supported yet USDT_ETH = "usdt_eth" diff --git a/pyfragment/models/payments.py b/pyfragment/models/payments.py index 9eb14f5..408b005 100644 --- a/pyfragment/models/payments.py +++ b/pyfragment/models/payments.py @@ -30,7 +30,7 @@ class AdsTopupResult: amount: int def __repr__(self) -> str: - return f"AdsTopupResult(username='{self.username}', amount={self.amount} TON, tx='{self.transaction_id}')" + return f"AdsTopupResult(username='{self.username}', amount={self.amount} GRAM (ex TON), tx='{self.transaction_id}')" @dataclass @@ -39,7 +39,7 @@ class AdsRechargeResult: amount: int def __repr__(self) -> str: - return f"AdsRechargeResult(amount={self.amount} TON, tx='{self.transaction_id}')" + return f"AdsRechargeResult(amount={self.amount} GRAM (ex TON), tx='{self.transaction_id}')" __all__ = ["AdsRechargeResult", "AdsTopupResult", "PremiumResult", "StarsResult"] diff --git a/pyfragment/models/wallet.py b/pyfragment/models/wallet.py index c52fdac..5f15a51 100644 --- a/pyfragment/models/wallet.py +++ b/pyfragment/models/wallet.py @@ -7,13 +7,13 @@ from dataclasses import dataclass class WalletInfo: address: str state: str - ton_balance: float + gram_balance: float usdt_balance: float def __repr__(self) -> str: return ( f"WalletInfo(address='{self.address}', state='{self.state}', " - f"ton_balance={self.ton_balance} TON, usdt_balance={self.usdt_balance} USDT)" + f"gram_balance={self.gram_balance} GRAM (ex TON), usdt_balance={self.usdt_balance} USDT)" ) diff --git a/pyproject.toml b/pyproject.toml index 82974a4..615706b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "pyfragment" version = "2026.3.2" -description = "Async Python client for the Fragment API. Buy Stars and Premium, top up TON and Ads balances, run giveaways, manage anonymous numbers, and search Fragment listings." +description = "Async Python client for the Fragment API. Buy Stars and Premium, top up GRAM (ex TON) and Ads balances, run giveaways, manage anonymous numbers, and search Fragment listings." readme = "README.md" license = { text = "MIT" } requires-python = ">=3.10" @@ -91,10 +91,16 @@ ignore = ["E501", "UP017"] [tool.mypy] python_version = "3.10" strict = true -exclude = ["^examples/", "^logs/"] +exclude = ["^examples/", "^logs/", "^tests/"] [[tool.mypy.overrides]] -module = "rookiepy" +module = [ + "rookiepy", + "httpx", + "ton_core", + "tonutils", + "tonutils.*", +] ignore_missing_imports = true [[tool.mypy.overrides]] diff --git a/tests/003_test_balance.py b/tests/003_test_balance.py index cedd4b5..fbe0d73 100644 --- a/tests/003_test_balance.py +++ b/tests/003_test_balance.py @@ -1,4 +1,4 @@ -"""Exercise transaction signing, balance checks, and retry behavior for TON and USDT flows.""" +"""Exercise transaction signing, balance checks, and retry behavior for GRAM (ex TON) and USDT flows.""" from collections.abc import Generator from contextlib import contextmanager @@ -22,7 +22,7 @@ TRANSACTION_DATA = { "messages": [ { "address": "0:852443f8599fe6a5da34fe43049ac4e0beb3071bb2bfb56635ea9421287c283a", - "amount": "500000000", # 0.5 TON + "amount": "500000000", # 0.5 GRAM "payload": "", } ] @@ -63,7 +63,7 @@ def _patch_wallet(wallet: MagicMock) -> Generator[None, None, None]: @pytest.mark.asyncio async def test_sufficient_balance_broadcasts() -> None: - wallet = _make_wallet(balance_nanotons=1_000_000_000) # 1 TON, above threshold + wallet = _make_wallet(balance_nanotons=1_000_000_000) # 1 GRAM, above threshold with _patch_wallet(wallet), patch("pyfragment.domains.tonapi.transaction.clean_decode", return_value="50 Telegram Stars"): result = await process_transaction(_make_client(), TRANSACTION_DATA) assert result == "abc123" @@ -72,7 +72,7 @@ async def test_sufficient_balance_broadcasts() -> None: @pytest.mark.asyncio async def test_insufficient_balance_raises() -> None: - wallet = _make_wallet(balance_nanotons=100_000_000) # 0.1 TON, below threshold + wallet = _make_wallet(balance_nanotons=100_000_000) # 0.1 GRAM, below threshold with _patch_wallet(wallet): with pytest.raises(WalletError, match="required"): await process_transaction(_make_client(), TRANSACTION_DATA) @@ -89,7 +89,7 @@ async def test_exact_minimum_balance_broadcasts() -> None: @pytest.mark.asyncio async def test_one_nanoton_below_minimum_raises() -> None: - wallet = _make_wallet(balance_nanotons=499_999_999) # 1 nanoton below transaction amount threshold + wallet = _make_wallet(balance_nanotons=499_999_999) # 1 nanogram below transaction amount threshold with _patch_wallet(wallet): with pytest.raises(WalletError, match="required"): await process_transaction(_make_client(), TRANSACTION_DATA) @@ -142,11 +142,11 @@ async def test_duplicate_seqno_raises_after_retries() -> None: @pytest.mark.asyncio -async def test_usdt_payment_requires_min_ton_gas_reserve() -> None: - wallet = _make_wallet(balance_nanotons=10_000_000) # 0.01 TON below MIN_TON_BALANCE +async def test_usdt_payment_requires_min_gram_gas_reserve() -> None: + wallet = _make_wallet(balance_nanotons=10_000_000) # 0.01 GRAM below MIN_GRAM_BALANCE with _patch_wallet(wallet), patch("pyfragment.domains.tonapi.account.get_usdt_balance", AsyncMock(return_value=100.0)): - with pytest.raises(WalletError, match="Insufficient TON balance"): - await process_transaction(_make_client(), TRANSACTION_DATA, payment_method=PaymentMethod.USDT_TON) + with pytest.raises(WalletError, match="Insufficient GRAM"): + await process_transaction(_make_client(), TRANSACTION_DATA, payment_method=PaymentMethod.USDT_GRAM) @pytest.mark.asyncio @@ -174,6 +174,6 @@ async def test_usdt_payment_checks_usdt_balance() -> None: await process_transaction( _make_client(), transaction, - payment_method=PaymentMethod.USDT_TON, + payment_method=PaymentMethod.USDT_GRAM, required_payment_amount=12.5, ) diff --git a/tests/004_test_stars.py b/tests/004_test_stars.py index 1dc6b08..4a6d7df 100644 --- a/tests/004_test_stars.py +++ b/tests/004_test_stars.py @@ -87,7 +87,7 @@ async def test_purchase_stars_passes_payment_method(client: FragmentClient) -> N patch.object(_purchase_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), patch.object(_purchase_stars_mod, "process_transaction", proc_mock), ): - await client.purchase_stars("@user", amount=500, payment_method=PaymentMethod.USDT_TON) + await client.purchase_stars("@user", amount=500, payment_method=PaymentMethod.USDT_GRAM) init_call = call_mock.await_args_list[2] assert init_call.args[0] == "initBuyStarsRequest" @@ -209,7 +209,7 @@ async def test_giveaway_stars_passes_payment_method(client: FragmentClient) -> N patch.object(_giveaway_stars_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), patch.object(_giveaway_stars_mod, "process_transaction", proc_mock), ): - await client.giveaway_stars("@channel", winners=3, amount=1000, payment_method=PaymentMethod.USDT_TON) + await client.giveaway_stars("@channel", winners=3, amount=1000, payment_method=PaymentMethod.USDT_GRAM) init_call = call_mock.await_args_list[3] assert init_call.args[0] == "initGiveawayStarsRequest" diff --git a/tests/005_test_premium.py b/tests/005_test_premium.py index 8ecae0c..babf5f7 100644 --- a/tests/005_test_premium.py +++ b/tests/005_test_premium.py @@ -78,7 +78,7 @@ async def test_purchase_premium_passes_payment_method(client: FragmentClient) -> patch.object(_purchase_premium_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), patch.object(_purchase_premium_mod, "process_transaction", proc_mock), ): - await client.purchase_premium("@user", months=6, payment_method=PaymentMethod.USDT_TON) + await client.purchase_premium("@user", months=6, payment_method=PaymentMethod.USDT_GRAM) init_call = call_mock.await_args_list[2] assert init_call.args[0] == "initGiftPremiumRequest" @@ -207,7 +207,7 @@ async def test_giveaway_premium_passes_payment_method(client: FragmentClient) -> patch.object(_giveaway_premium_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), patch.object(_giveaway_premium_mod, "process_transaction", proc_mock), ): - await client.giveaway_premium("@channel", winners=10, months=6, payment_method=PaymentMethod.USDT_TON) + await client.giveaway_premium("@channel", winners=10, months=6, payment_method=PaymentMethod.USDT_GRAM) init_call = call_mock.await_args_list[3] assert init_call.args[0] == "initGiveawayPremiumRequest" diff --git a/tests/006_test_topup.py b/tests/006_test_topup.py index e1807da..0b06b7a 100644 --- a/tests/006_test_topup.py +++ b/tests/006_test_topup.py @@ -1,40 +1,40 @@ -"""Cover TON top-up through Telegram Ads, including recipient lookup and transaction building.""" +"""Cover GRAM (ex TON) top-up through Telegram Ads, including recipient lookup and transaction building.""" from unittest.mock import AsyncMock, patch import pytest -import pyfragment.domains.ads.tonup as _topup_ton_mod +import pyfragment.domains.ads.tonup as _topup_gram_mod from pyfragment import AdsTopupResult, ConfigurationError, FragmentClient, UserNotFoundError -from pyfragment.core.constants.limits import TON_TOPUP_MAX, TON_TOPUP_MIN +from pyfragment.core.constants.limits import GRAM_TOPUP_MAX, GRAM_TOPUP_MIN from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH -# Topup TON validation tests +# Topup GRAM (ex TON) validation tests @pytest.mark.asyncio -async def test_topup_ton_amount_zero(client: FragmentClient) -> None: +async def test_topup_gram_amount_zero(client: FragmentClient) -> None: with pytest.raises(ConfigurationError): - await client.topup_ton("@user", amount=TON_TOPUP_MIN - 1) + await client.topup_gram("@user", amount=GRAM_TOPUP_MIN - 1) @pytest.mark.asyncio -async def test_topup_ton_amount_too_high(client: FragmentClient) -> None: +async def test_topup_gram_amount_too_high(client: FragmentClient) -> None: with pytest.raises(ConfigurationError): - await client.topup_ton("@user", amount=TON_TOPUP_MAX + 1) + await client.topup_gram("@user", amount=GRAM_TOPUP_MAX + 1) @pytest.mark.asyncio -async def test_topup_ton_float_amount(client: FragmentClient) -> None: +async def test_topup_gram_float_amount(client: FragmentClient) -> None: with pytest.raises(ConfigurationError): - await client.topup_ton("@user", amount=1.5) # type: ignore[arg-type] + await client.topup_gram("@user", amount=1.5) # type: ignore[arg-type] -# Topup TON mocked tests +# Topup GRAM (ex TON) mocked tests @pytest.mark.asyncio -async def test_topup_ton_success(client: FragmentClient) -> None: +async def test_topup_gram_success(client: FragmentClient) -> None: with ( patch.object( client, @@ -48,10 +48,10 @@ async def test_topup_ton_success(client: FragmentClient) -> None: ] ), ), - patch.object(_topup_ton_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), - patch.object(_topup_ton_mod, "process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), + patch.object(_topup_gram_mod, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch.object(_topup_gram_mod, "process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), ): - result = await client.topup_ton("@user", amount=10) + result = await client.topup_gram("@user", amount=10) assert isinstance(result, AdsTopupResult) assert result.transaction_id == FAKE_TX_HASH @@ -60,7 +60,7 @@ async def test_topup_ton_success(client: FragmentClient) -> None: @pytest.mark.asyncio -async def test_topup_ton_user_not_found(client: FragmentClient) -> None: +async def test_topup_gram_user_not_found(client: FragmentClient) -> None: with patch.object( client, "call", @@ -72,4 +72,4 @@ async def test_topup_ton_user_not_found(client: FragmentClient) -> None: ), ): with pytest.raises(UserNotFoundError): - await client.topup_ton("@ghost", amount=10) + await client.topup_gram("@ghost", amount=10) diff --git a/tests/007_test_wallet.py b/tests/007_test_wallet.py index 2010aa6..9f699c3 100644 --- a/tests/007_test_wallet.py +++ b/tests/007_test_wallet.py @@ -1,20 +1,20 @@ -"""Verify wallet inspection returns friendly TON and USDT balances from Tonapi.""" +"""Verify wallet inspection returns friendly GRAM (ex TON) and USDT balances from Tonapi.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from pyfragment import FragmentClient, WalletInfo -from tests.shared import FAKE_ADDRESS, FAKE_BALANCE_NANOTON +from tests.shared import FAKE_ADDRESS, FAKE_BALANCE_NANOGRAM -# Wallet mocked tests (TON and USDT balances are returned separately) +# Wallet mocked tests (GRAM and USDT balances are returned separately) @pytest.mark.asyncio async def test_get_wallet_returns_wallet_info(client: FragmentClient) -> None: mock_wallet = MagicMock() mock_wallet.refresh = AsyncMock() - mock_wallet.balance = FAKE_BALANCE_NANOTON + mock_wallet.balance = FAKE_BALANCE_NANOGRAM mock_wallet.state = MagicMock(value="active") mock_wallet.address.to_str.return_value = FAKE_ADDRESS @@ -32,7 +32,7 @@ async def test_get_wallet_returns_wallet_info(client: FragmentClient) -> None: assert isinstance(result, WalletInfo) assert result.address == FAKE_ADDRESS assert result.state == "active" - assert result.ton_balance == round(FAKE_BALANCE_NANOTON / 1_000_000_000, 4) + assert result.gram_balance == round(FAKE_BALANCE_NANOGRAM / 1_000_000_000, 4) assert result.usdt_balance == 12.3456 @@ -55,6 +55,6 @@ async def test_get_wallet_balance_is_zero(client: FragmentClient) -> None: result = await client.get_wallet() - assert result.ton_balance == 0.0 + assert result.gram_balance == 0.0 assert result.usdt_balance == 0.0 assert result.state == "uninit" diff --git a/tests/010_test_recharge_ads.py b/tests/010_test_recharge_ads.py index 51f56a2..283e1de 100644 --- a/tests/010_test_recharge_ads.py +++ b/tests/010_test_recharge_ads.py @@ -6,7 +6,7 @@ import pytest import pyfragment.domains.ads.recharge as _recharge_ads_mod from pyfragment import AdsRechargeResult, ConfigurationError, FragmentClient -from pyfragment.core.constants.limits import TON_TOPUP_MAX, TON_TOPUP_MIN +from pyfragment.core.constants.limits import GRAM_TOPUP_MAX, GRAM_TOPUP_MIN from tests.shared import FAKE_ACCOUNT, FAKE_ADS_ACCOUNT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH # recharge_ads validation tests @@ -15,13 +15,13 @@ from tests.shared import FAKE_ACCOUNT, FAKE_ADS_ACCOUNT, FAKE_REQ_ID, FAKE_TRANS @pytest.mark.asyncio async def test_recharge_ads_amount_zero(client: FragmentClient) -> None: with pytest.raises(ConfigurationError): - await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=TON_TOPUP_MIN - 1) + await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=GRAM_TOPUP_MIN - 1) @pytest.mark.asyncio async def test_recharge_ads_amount_too_high(client: FragmentClient) -> None: with pytest.raises(ConfigurationError): - await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=TON_TOPUP_MAX + 1) + await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=GRAM_TOPUP_MAX + 1) @pytest.mark.asyncio diff --git a/tests/shared.py b/tests/shared.py index 1ce6be4..161cf9e 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -1,7 +1,7 @@ """Shared test constants for the pyfragment test suite. pyfragment is an async Python client for the Fragment API — a unified toolkit -to manage Telegram assets: purchase Stars and Premium, top up TON and Ads balances, +to manage Telegram assets: purchase Stars and Premium, top up GRAM (ex TON) and Ads balances, run giveaways, manage anonymous numbers, and explore the marketplace for usernames, numbers, and gifts. """ @@ -31,7 +31,7 @@ FAKE_RESPONSE: dict[str, Any] = {"status": "ok", "data": {"value": 42}} # get_wallet() FAKE_ADDRESS: str = "UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5" -FAKE_BALANCE_NANOTON: int = 1_500_000_000 # 1.5 TON +FAKE_BALANCE_NANOGRAM: int = 1_500_000_000 # 1.5 GRAM (ex TON) # recharge_ads FAKE_ADS_ACCOUNT: str = "@mychannel"