Refactor for GRAM (ex TON) integration

- Updated imports and references from TON to GRAM across multiple modules.
- Renamed functions and variables to reflect the change from TON to GRAM.
- Adjusted validation checks for top-up and recharge amounts to use GRAM limits.
- Modified error messages and exceptions to indicate GRAM instead of TON.
- Updated tests to ensure they reflect the new GRAM terminology and functionality.
- Introduced new constants for GRAM-related configurations.
This commit is contained in:
bohd4nx
2026-06-16 00:47:49 +03:00
parent 3481769757
commit 8f865f4911
38 changed files with 227 additions and 181 deletions
+4
View File
@@ -37,3 +37,7 @@ systests/
dist/ dist/
build/ build/
*.egg-info/ *.egg-info/
uv.lock
#docs
*-docs/
+30
View File
@@ -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 `DEVICE_INFO` fingerprint: Tonkeeper `appVersion` -> `26.05.0`.
- Updated client docstrings and purchase examples to document all supported payment methods. - 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 ## [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 - `py.typed` marker — full PEP 561 typing support for type-checkers
- `__repr__` on all result types for readable debug output - `__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.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.3]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.2.3
[2026.2.2]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.2.2 [2026.2.2]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.2.2
+1 -1
View File
@@ -30,7 +30,7 @@ pyfragment/
client.py — FragmentClient (public entry point) client.py — FragmentClient (public entry point)
core/ — transport, cookies, constants core/ — transport, cookies, constants
domains/ — one package per feature domain domains/ — one package per feature domain
ads/ — recharge_ads, topup_ton ads/ — recharge_ads, topup_gram
anonymous_numbers/— buy_number, manage_number anonymous_numbers/— buy_number, manage_number
giveaways/ — giveaway_stars, giveaway_premium giveaways/ — giveaway_stars, giveaway_premium
marketplace/ — search_usernames, search_numbers, search_gifts marketplace/ — search_usernames, search_numbers, search_gifts
+9 -9
View File
@@ -10,10 +10,10 @@
[![License](https://img.shields.io/github/license/bohd4nx/pyfragment?style=flat&color=blue&label=License)](LICENSE) [![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. 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. 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)**
</div> </div>
@@ -53,11 +53,11 @@ async def main() -> None:
}, },
) as client: ) as client:
wallet = await client.get_wallet() 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 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: if stars.auto_paid:
print(f"Sent {stars.amount} Stars to {stars.username} | tx: {stars.transaction_id}") print(f"Sent {stars.amount} Stars to {stars.username} | tx: {stars.transaction_id}")
else: else:
@@ -67,7 +67,7 @@ async def main() -> None:
print(f"Expires: {stars.payment_expires_at}") print(f"Expires: {stars.payment_expires_at}")
print(f"QR link: {stars.payment_qr_link}") 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}") 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 | | 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) | | `api_key` | `str` | — | Tonapi key from [tonconsole.com](https://tonconsole.com) |
| `cookies` | `dict \| str` | — | Fragment session cookies | | `cookies` | `dict \| str` | — | Fragment session cookies |
| `wallet_version` | `str` | `"V5R1"` | `"V4R2"` or `"V5R1"` — also accepts `WalletVersion` literal | | `wallet_version` | `str` | `"V5R1"` | `"V4R2"` or `"V5R1"` — also accepts `WalletVersion` literal |
@@ -92,7 +92,7 @@ asyncio.run(main())
### Fragment cookies ### 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: **Automatically** (recommended) — reads directly from your browser, no extension needed:
@@ -118,7 +118,7 @@ Generate at [tonconsole.com](https://tonconsole.com).
### Seed phrase ### 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 ( from pyfragment import (
ConfigurationError, # invalid arguments (amount, months, payment_method…) ConfigurationError, # invalid arguments (amount, months, payment_method…)
UserNotFoundError, # recipient not found on Fragment 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 TransactionError, # broadcast failed, duplicate seqno, invalid payload
FragmentAPIError, # Fragment API returned an error response FragmentAPIError, # Fragment API returned an error response
FragmentPageError, # page fetch or hash extraction failed FragmentPageError, # page fetch or hash extraction failed
+1 -1
View File
@@ -16,4 +16,4 @@ You will receive a response within 72 hours. Once the fix is released, the advis
## Scope ## 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.
+2 -2
View File
@@ -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. Cookies can be passed as a dict or as a JSON string.
wallet_version defaults to "V5R1" — change to "V4R2" for older wallets. wallet_version defaults to "V5R1" — change to "V4R2" for older wallets.
@@ -34,7 +34,7 @@ async def main() -> None:
wallet = await client.get_wallet() wallet = await client.get_wallet()
print(f"Address: {wallet.address}") print(f"Address: {wallet.address}")
print(f"State: {wallet.state}") 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") print(f"Balance: {wallet.usdt_balance} USDT")
+5 -5
View File
@@ -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. Amount must be an integer between 1 and 1 000 000 000 GRAM (ex TON).
Your wallet must satisfy the current minimum TON threshold and transaction cost. Your wallet must satisfy the current minimum GRAM (ex TON) threshold and transaction cost.
""" """
import asyncio import asyncio
@@ -29,7 +29,7 @@ COOKIES = {
} }
ACCOUNT = "@mychannel" # channel or bot username linked to your Telegram Ads account ACCOUNT = "@mychannel" # channel or bot username linked to your Telegram Ads account
AMOUNT = 10 # 11 000 000 000 TON AMOUNT = 10 # 11 000 000 000 GRAM (ex TON)
async def main() -> None: async def main() -> None:
@@ -43,7 +43,7 @@ async def main() -> None:
print(f"Invalid argument: {e}") print(f"Invalid argument: {e}")
return 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__": if __name__ == "__main__":
+1 -1
View File
@@ -28,7 +28,7 @@ COOKIES = {
CHANNEL = "https://t.me/channel" CHANNEL = "https://t.me/channel"
WINNERS = 10 # 124 000 WINNERS = 10 # 124 000
MONTHS = 3 # 3, 6 or 12 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: async def main() -> None:
+1 -1
View File
@@ -28,7 +28,7 @@ COOKIES = {
CHANNEL = "https://t.me/channel" CHANNEL = "https://t.me/channel"
WINNERS = 3 # 115 WINNERS = 3 # 115
AMOUNT = 1000 # 5001 000 000 stars per winner AMOUNT = 1000 # 5001 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: async def main() -> None:
+1 -1
View File
@@ -27,7 +27,7 @@ COOKIES = {
USERNAME = "https://t.me/username" USERNAME = "https://t.me/username"
MONTHS = 3 # 3, 6 or 12 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: async def main() -> None:
+1 -1
View File
@@ -27,7 +27,7 @@ COOKIES = {
USERNAME = "https://t.me/username" USERNAME = "https://t.me/username"
AMOUNT = 500 # 5010 000 000 stars AMOUNT = 500 # 5010 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: async def main() -> None:
+7 -7
View File
@@ -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. Amount must be an integer between 1 and 1 000 000 000 GRAM (ex TON).
Your wallet must satisfy the current minimum TON threshold and transaction cost. Your wallet must satisfy the current minimum GRAM (ex TON) threshold and transaction cost.
""" """
import asyncio import asyncio
@@ -31,13 +31,13 @@ COOKIES = {
} }
USERNAME = "@username" USERNAME = "@username"
AMOUNT = 10 # 11 000 000 000 TON AMOUNT = 10 # 11 000 000 000 GRAM (ex TON)
async def main() -> None: async def main() -> None:
async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client:
try: 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: except UserNotFoundError:
print(f"User {USERNAME} was not found on fragment.com — check the username and try again.") print(f"User {USERNAME} was not found on fragment.com — check the username and try again.")
return return
@@ -48,7 +48,7 @@ async def main() -> None:
print(f"Invalid argument: {e}") print(f"Invalid argument: {e}")
return 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__": if __name__ == "__main__":
+15 -15
View File
@@ -36,7 +36,7 @@ class FragmentClient:
connected with Fragment or Telegram. connected with Fragment or Telegram.
Args: 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. api_key: Tonapi API key — get one at https://tonconsole.com.
cookies: Fragment session cookies as a dict or JSON string. cookies: Fragment session cookies as a dict or JSON string.
wallet_version: Wallet contract version — ``"V4R2"`` or ``"V5R1"`` (default). wallet_version: Wallet contract version — ``"V4R2"`` or ``"V5R1"`` (default).
@@ -140,7 +140,7 @@ class FragmentClient:
username: str, username: str,
months: int, months: int,
show_sender: bool = True, show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumResult: ) -> PremiumResult:
"""Gift Telegram Premium to a user. """Gift Telegram Premium to a user.
@@ -148,7 +148,7 @@ class FragmentClient:
username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``. username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``.
months: Duration — ``3``, ``6``, or ``12``. months: Duration — ``3``, ``6``, or ``12``.
show_sender: Show your name as the sender. Defaults to ``True``. 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"``, ``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
or ``"usdc_pol"``. or ``"usdc_pol"``.
@@ -162,7 +162,7 @@ class FragmentClient:
username: str, username: str,
amount: int, amount: int,
show_sender: bool = True, show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsResult: ) -> StarsResult:
"""Send Telegram Stars to a user. """Send Telegram Stars to a user.
@@ -170,7 +170,7 @@ class FragmentClient:
username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``. username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``.
amount: Number of stars — integer from ``50`` to ``10 000 000``. amount: Number of stars — integer from ``50`` to ``10 000 000``.
show_sender: Show your name as the gift sender. Defaults to ``True``. 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"``, ``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
or ``"usdc_pol"``. 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) 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: async def topup_gram(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
"""Top up TON to a recipient's Telegram balance. """Top up GRAM (ex TON) to a recipient's Telegram balance.
Args: Args:
username: Recipient's Telegram username (with or without ``@``). 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``. show_sender: Show your name as the sender. Defaults to ``True``.
Returns: Returns:
:class:`AdsTopupResult` with ``transaction_id``, ``username``, and ``amount``. :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: async def recharge_ads(self, account: str, amount: int) -> AdsRechargeResult:
"""Add funds to your own Telegram Ads account. """Add funds to your own Telegram Ads account.
@@ -198,7 +198,7 @@ class FragmentClient:
Args: Args:
account: Your Fragment Ads account identifier — the channel or bot username account: Your Fragment Ads account identifier — the channel or bot username
the Ads account is linked to (e.g. ``"@mychannel"``). 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: Returns:
:class:`AdsRechargeResult` with ``transaction_id`` and ``amount``. :class:`AdsRechargeResult` with ``transaction_id`` and ``amount``.
@@ -211,7 +211,7 @@ class FragmentClient:
Returns: Returns:
:class:`WalletInfo` with ``address`` (``"UQ..."``), ``state`` :class:`WalletInfo` with ``address`` (``"UQ..."``), ``state``
(``"active"``, ``"uninit"``, ``"nonexist"``, or ``"frozen"``), (``"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() return await self.tonapi.get_wallet()
@@ -220,7 +220,7 @@ class FragmentClient:
channel: str, channel: str,
winners: int, winners: int,
amount: int, amount: int,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsGiveawayResult: ) -> StarsGiveawayResult:
"""Run a Telegram Stars giveaway for a channel. """Run a Telegram Stars giveaway for a channel.
@@ -228,7 +228,7 @@ class FragmentClient:
channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``. channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``.
winners: Number of winners — integer from ``1`` to ``15``. winners: Number of winners — integer from ``1`` to ``15``.
amount: Stars each winner receives — integer from ``500`` to ``1 000 000``. 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"``, ``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
or ``"usdc_pol"``. or ``"usdc_pol"``.
@@ -243,7 +243,7 @@ class FragmentClient:
channel: str, channel: str,
winners: int, winners: int,
months: int = 3, months: int = 3,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumGiveawayResult: ) -> PremiumGiveawayResult:
"""Run a Telegram Premium giveaway for a channel. """Run a Telegram Premium giveaway for a channel.
@@ -251,7 +251,7 @@ class FragmentClient:
channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``. channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``.
winners: Number of winners — integer from ``1`` to ``24 000``. winners: Number of winners — integer from ``1`` to ``24 000``.
months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``. 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"``, ``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
or ``"usdc_pol"``. or ``"usdc_pol"``.
+8 -8
View File
@@ -1,8 +1,11 @@
from __future__ import annotations 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.headers import BASE_HEADERS, DEFAULT_TIMEOUT, REQUIRED_COOKIE_KEYS
from pyfragment.core.constants.limits import ( from pyfragment.core.constants.limits import (
MIN_TON_BALANCE, GRAM_TOPUP_MAX,
GRAM_TOPUP_MIN,
MIN_GRAM_BALANCE,
MIN_USDT_BALANCE, MIN_USDT_BALANCE,
MNEMONIC_WORD_COUNTS_VALID, MNEMONIC_WORD_COUNTS_VALID,
PREMIUM_MONTHS_VALID, PREMIUM_MONTHS_VALID,
@@ -14,11 +17,8 @@ from pyfragment.core.constants.limits import (
STARS_PURCHASE_MIN, STARS_PURCHASE_MIN,
STARS_WINNERS_MAX, STARS_WINNERS_MAX,
STARS_WINNERS_MIN, STARS_WINNERS_MIN,
TON_TOPUP_MAX,
TON_TOPUP_MIN,
TONAPI_KEY_MIN_LENGTH, TONAPI_KEY_MIN_LENGTH,
) )
from pyfragment.core.constants.ton import DEVICE_INFO, USDT_TON_MASTER_ADDRESS
from pyfragment.core.constants.urls import ( from pyfragment.core.constants.urls import (
ADS_TOPUP_PAGE, ADS_TOPUP_PAGE,
FRAGMENT_BASE_URL, FRAGMENT_BASE_URL,
@@ -39,7 +39,9 @@ __all__ = [
"FRAGMENT_BASE_URL", "FRAGMENT_BASE_URL",
"FRAGMENT_DOMAIN", "FRAGMENT_DOMAIN",
"GIFTS_PAGE", "GIFTS_PAGE",
"MIN_TON_BALANCE", "GRAM_TOPUP_MAX",
"GRAM_TOPUP_MIN",
"MIN_GRAM_BALANCE",
"MIN_USDT_BALANCE", "MIN_USDT_BALANCE",
"MNEMONIC_WORD_COUNTS_VALID", "MNEMONIC_WORD_COUNTS_VALID",
"NUMBERS_PAGE", "NUMBERS_PAGE",
@@ -58,7 +60,5 @@ __all__ = [
"STARS_WINNERS_MAX", "STARS_WINNERS_MAX",
"STARS_WINNERS_MIN", "STARS_WINNERS_MIN",
"TONAPI_KEY_MIN_LENGTH", "TONAPI_KEY_MIN_LENGTH",
"TON_TOPUP_MAX", "USDT_GRAM_MASTER_ADDRESS",
"TON_TOPUP_MIN",
"USDT_TON_MASTER_ADDRESS",
] ]
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any from typing import Any
USDT_TON_MASTER_ADDRESS: str = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" USDT_GRAM_MASTER_ADDRESS: str = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"
DEVICE_INFO: dict[str, Any] = { DEVICE_INFO: dict[str, Any] = {
"platform": "iphone", "platform": "iphone",
+4 -4
View File
@@ -16,12 +16,12 @@ STARS_WINNERS_MAX: int = 15
PREMIUM_WINNERS_MIN: int = 1 PREMIUM_WINNERS_MIN: int = 1
PREMIUM_WINNERS_MAX: int = 24_000 PREMIUM_WINNERS_MAX: int = 24_000
# TON topup / Ads recharge amount # GRAM (ex TON) topup / Ads recharge amount
TON_TOPUP_MIN: int = 1 GRAM_TOPUP_MIN: int = 1
TON_TOPUP_MAX: int = 1_000_000_000 GRAM_TOPUP_MAX: int = 1_000_000_000
# Wallet minimum balances # Wallet minimum balances
MIN_TON_BALANCE: float = 0.33 MIN_GRAM_BALANCE: float = 0.33
MIN_USDT_BALANCE: float = 0.75 MIN_USDT_BALANCE: float = 0.75
# Premium subscription durations (months) # Premium subscription durations (months)
+2 -2
View File
@@ -1,5 +1,5 @@
from pyfragment.domains.ads.recharge import recharge_ads from pyfragment.domains.ads.recharge import recharge_ads
from pyfragment.domains.ads.service import AdsService 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"]
+5 -5
View File
@@ -4,7 +4,7 @@ import json
import logging import logging
from typing import TYPE_CHECKING 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.account import get_account_info
from pyfragment.domains.tonapi.transaction import process_transaction from pyfragment.domains.tonapi.transaction import process_transaction
from pyfragment.exceptions import ConfigurationError, FragmentAPIError, FragmentError, UnexpectedError, VerificationError 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: 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): if not isinstance(amount, int) or not (GRAM_TOPUP_MIN <= amount <= GRAM_TOPUP_MAX):
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT) raise ConfigurationError(ConfigurationError.INVALID_GRAM_AMOUNT)
try: try:
await client.call("updateAdsState", {"mode": "new"}, page_url=ADS_TOPUP_PAGE) 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) return AdsRechargeResult(transaction_id=tx_hash, amount=amount)
except FragmentError as exc: 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 raise
except Exception as exc: 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 raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+3 -3
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pyfragment.domains.ads.recharge import recharge_ads 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.domains.base import BaseService
from pyfragment.models.payments import AdsRechargeResult, AdsTopupResult from pyfragment.models.payments import AdsRechargeResult, AdsTopupResult
@@ -15,5 +15,5 @@ class AdsService(BaseService):
async def recharge_ads(self, account: str, amount: int) -> AdsRechargeResult: async def recharge_ads(self, account: str, amount: int) -> AdsRechargeResult:
return await recharge_ads(self._client, account, amount) return await recharge_ads(self._client, account, amount)
async def topup_ton(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: async def topup_gram(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
return await topup_ton(self._client, username, amount, show_sender=show_sender) return await topup_gram(self._client, username, amount, show_sender=show_sender)
+11 -7
View File
@@ -4,7 +4,7 @@ import json
import logging import logging
from typing import TYPE_CHECKING 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.payments import parse_required_payment_amount
from pyfragment.domains.tonapi.account import get_account_info from pyfragment.domains.tonapi.account import get_account_info
from pyfragment.domains.tonapi.transaction import process_transaction from pyfragment.domains.tonapi.transaction import process_transaction
@@ -25,9 +25,9 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def topup_ton(client: FragmentClient, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: async def topup_gram(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): if not isinstance(amount, int) or not (GRAM_TOPUP_MIN <= amount <= GRAM_TOPUP_MAX):
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT) raise ConfigurationError(ConfigurationError.INVALID_GRAM_AMOUNT)
try: try:
await client.call("updateAdsTopupState", {"mode": "new"}, page_url=ADS_TOPUP_PAGE) 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) required_payment_amount = parse_required_payment_amount(result)
req_id = result.get("req_id") req_id = result.get("req_id")
if not 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) account = await get_account_info(client)
transaction = await client.call( 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) return AdsTopupResult(transaction_id=tx_hash, username=username, amount=amount)
except FragmentError as exc: 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 raise
except Exception as exc: 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 raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+2 -2
View File
@@ -48,7 +48,7 @@ async def giveaway_stars(
channel: str, channel: str,
winners: int, winners: int,
amount: int, amount: int,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsGiveawayResult: ) -> StarsGiveawayResult:
if not isinstance(winners, int) or not (STARS_WINNERS_MIN <= winners <= STARS_WINNERS_MAX): if not isinstance(winners, int) or not (STARS_WINNERS_MIN <= winners <= STARS_WINNERS_MAX):
raise ConfigurationError(ConfigurationError.INVALID_WINNERS_STARS) raise ConfigurationError(ConfigurationError.INVALID_WINNERS_STARS)
@@ -143,7 +143,7 @@ async def giveaway_premium(
channel: str, channel: str,
winners: int, winners: int,
months: int = 3, months: int = 3,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumGiveawayResult: ) -> PremiumGiveawayResult:
if not isinstance(winners, int) or not (PREMIUM_WINNERS_MIN <= winners <= PREMIUM_WINNERS_MAX): if not isinstance(winners, int) or not (PREMIUM_WINNERS_MIN <= winners <= PREMIUM_WINNERS_MAX):
raise ConfigurationError(ConfigurationError.INVALID_WINNERS_PREMIUM) raise ConfigurationError(ConfigurationError.INVALID_WINNERS_PREMIUM)
+2 -2
View File
@@ -17,7 +17,7 @@ class GiveawaysService(BaseService):
channel: str, channel: str,
winners: int, winners: int,
amount: int, amount: int,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsGiveawayResult: ) -> StarsGiveawayResult:
return await giveaway_stars(self._client, channel, winners, amount, payment_method=payment_method) return await giveaway_stars(self._client, channel, winners, amount, payment_method=payment_method)
@@ -26,6 +26,6 @@ class GiveawaysService(BaseService):
channel: str, channel: str,
winners: int, winners: int,
months: int = 3, months: int = 3,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumGiveawayResult: ) -> PremiumGiveawayResult:
return await giveaway_premium(self._client, channel, winners, months, payment_method=payment_method) return await giveaway_premium(self._client, channel, winners, months, payment_method=payment_method)
+2 -2
View File
@@ -45,7 +45,7 @@ async def purchase_stars(
username: str, username: str,
amount: int, amount: int,
show_sender: bool = True, show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsResult: ) -> StarsResult:
if not isinstance(amount, int) or not (STARS_PURCHASE_MIN <= amount <= STARS_PURCHASE_MAX): if not isinstance(amount, int) or not (STARS_PURCHASE_MIN <= amount <= STARS_PURCHASE_MAX):
raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT) raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT)
@@ -128,7 +128,7 @@ async def purchase_premium(
username: str, username: str,
months: int, months: int,
show_sender: bool = True, show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumResult: ) -> PremiumResult:
if months not in PREMIUM_MONTHS_VALID: if months not in PREMIUM_MONTHS_VALID:
raise ConfigurationError(ConfigurationError.INVALID_MONTHS) raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
+2 -2
View File
@@ -17,7 +17,7 @@ class PurchasesService(BaseService):
username: str, username: str,
amount: int, amount: int,
show_sender: bool = True, show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsResult: ) -> StarsResult:
return await purchase_stars(self._client, username, amount, show_sender=show_sender, payment_method=payment_method) 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, username: str,
months: int, months: int,
show_sender: bool = True, show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumResult: ) -> PremiumResult:
return await purchase_premium(self._client, username, months, show_sender=show_sender, payment_method=payment_method) return await purchase_premium(self._client, username, months, show_sender=show_sender, payment_method=payment_method)
+23 -23
View File
@@ -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.contracts.jetton import get_wallet_address_get_method, get_wallet_data_get_method
from tonutils.exceptions import ProviderResponseError 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.exceptions import WalletError
from pyfragment.models.enums import WALLET_CLASSES from pyfragment.models.enums import WALLET_CLASSES
from pyfragment.models.wallet import WalletInfo 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: 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: try:
jetton_wallet_address = await get_wallet_address_get_method( jetton_wallet_address = await get_wallet_address_get_method(
client=ton, client=ton,
address=USDT_TON_MASTER_ADDRESS, address=USDT_GRAM_MASTER_ADDRESS,
owner_address=wallet_address, owner_address=wallet_address,
) )
wallet_data = await get_wallet_data_get_method(client=ton, address=jetton_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 raise WalletError(WalletError.USDT_BALANCE_CHECK_FAILED.format(exc=exc)) from exc
async def check_ton_payment_balance( async def check_gram_payment_balance(
balance_ton: float, balance_gram: float,
amount_ton: float, amount_gram: float,
required_payment_amount: float | None, required_payment_amount: float | None,
) -> None: ) -> None:
"""Validate that the TON wallet can cover a TON-denominated payment.""" """Validate that the GRAM (ex TON) wallet can cover a GRAM (ex TON)-denominated payment."""
tx_price_ton = amount_ton tx_price_gram = amount_gram
if required_payment_amount is not None and required_payment_amount > 0: 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) required_gram = max(tx_price_gram, MIN_GRAM_BALANCE)
if balance_ton < required_ton: if balance_gram < required_gram:
logger.error( logger.error(
"Failed TON balance check: balance=%s TON, required=%s TON", "Failed GRAM (ex TON) balance check: balance=%s GRAM (ex TON), required=%s GRAM (ex TON)",
round(balance_ton, 6), round(balance_gram, 6),
round(required_ton, 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( async def check_usdt_payment_balance(
balance_ton: float, balance_gram: float,
required_payment_amount: float | None, required_payment_amount: float | None,
ton: Any, ton: Any,
wallet_address: str, wallet_address: str,
) -> None: ) -> None:
"""Validate that the wallet can cover a USDT-denominated payment.""" """Validate that the wallet can cover a USDT-denominated payment."""
if balance_ton < MIN_TON_BALANCE: if balance_gram < MIN_GRAM_BALANCE:
logger.error( logger.error(
"Failed TON gas reserve check for USDT payment: balance=%s TON, required=%s TON", "Failed GRAM (ex TON) gas reserve check for USDT payment: balance=%s GRAM (ex TON), required=%s GRAM (ex TON)",
round(balance_ton, 6), round(balance_gram, 6),
MIN_TON_BALANCE, 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) 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 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: 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: 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]
@@ -120,7 +120,7 @@ async def get_wallet_info(client: FragmentClient) -> WalletInfo:
return WalletInfo( return WalletInfo(
address=wallet.address.to_str(is_user_friendly=True, is_bounceable=False), address=wallet.address.to_str(is_user_friendly=True, is_bounceable=False),
state=wallet.state.value, 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), usdt_balance=round(usdt_balance, 4),
) )
except Exception as exc: except Exception as exc:
+14 -14
View File
@@ -11,7 +11,7 @@ from ton_core import Cell, NetworkGlobalID
from tonutils.clients import TonapiClient from tonutils.clients import TonapiClient
from tonutils.exceptions import ProviderResponseError 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.exceptions import ParseError, TransactionError, WalletError
from pyfragment.models.enums import WALLET_CLASSES, PaymentMethod 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. """Decode a base64 BOC comment from Fragment into text when possible.
Some Fragment payloads are plain text comments, while others are structured 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. `Cell` so the caller can keep the raw binary structure.
""" """
s = payload.strip() s = payload.strip()
@@ -39,7 +39,7 @@ def clean_decode(payload: str) -> str | Cell:
sl = cell.begin_parse() sl = cell.begin_parse()
op = sl.load_uint(32) op = sl.load_uint(32)
if op != 0: 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 return cell
try: try:
return sl.load_snake_string().strip() 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( async def _check_payment_balances(
wallet: Any, wallet: Any,
payment_method: PaymentMethod, payment_method: PaymentMethod,
amount_ton: float, amount_gram: float,
required_payment_amount: float | None, required_payment_amount: float | None,
transaction_data: dict[str, Any], transaction_data: dict[str, Any],
ton: Any, ton: Any,
@@ -70,18 +70,18 @@ async def _check_payment_balances(
"""Refresh wallet and verify sufficient balance before broadcasting.""" """Refresh wallet and verify sufficient balance before broadcasting."""
try: try:
await wallet.refresh() await wallet.refresh()
balance_ton = wallet.balance / 1_000_000_000 balance_gram = wallet.balance / 1_000_000_000
if payment_method == "ton": 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: else:
# USDT is paid from the Fragment-linked wallet, not the signing wallet. # USDT is paid from the Fragment-linked wallet, not the signing wallet.
fragment_wallet_address = transaction_data["transaction"].get("from", "") 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: except WalletError:
raise raise
except Exception as exc: except Exception as exc:
logger.exception("Failed to validate balances before broadcasting transaction") 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: 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: try:
result = await wallet.transfer( result = await wallet.transfer(
destination=message["address"], destination=message["address"],
amount=int(message["amount"]), # nanotons, not TON amount=int(message["amount"]), # nanograms, not GRAM (ex TON)
body=payload, body=payload,
) )
return str(result.normalized_hash) 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( async def process_transaction(
client: FragmentClient, client: FragmentClient,
transaction_data: dict[str, Any], transaction_data: dict[str, Any],
payment_method: PaymentMethod = PaymentMethod.TON, payment_method: PaymentMethod = PaymentMethod.GRAM,
required_payment_amount: float | None = None, required_payment_amount: float | None = None,
) -> str: ) -> 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: Args:
client: Authenticated `FragmentClient` instance. client: Authenticated `FragmentClient` instance.
@@ -139,13 +139,13 @@ async def process_transaction(
Normalized transaction hash string. Normalized transaction hash string.
""" """
message = _extract_message(transaction_data) 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: 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)
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", ""))) 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 raise TransactionError(TransactionError.BROADCAST_FAILED_SSL.format(exc=exc)) from exc
cause = cause.__cause__ or cause.__context__ cause = cause.__cause__ or cause.__context__
logger.exception( 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["address"],
message["amount"], message["amount"],
payment_method, payment_method,
+13 -11
View File
@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
from pyfragment.core.constants.limits import ( from pyfragment.core.constants.limits import (
GRAM_TOPUP_MAX,
GRAM_TOPUP_MIN,
MNEMONIC_WORD_COUNTS_VALID, MNEMONIC_WORD_COUNTS_VALID,
PREMIUM_MONTHS_VALID, PREMIUM_MONTHS_VALID,
PREMIUM_WINNERS_MAX, PREMIUM_WINNERS_MAX,
@@ -11,8 +13,6 @@ from pyfragment.core.constants.limits import (
STARS_PURCHASE_MIN, STARS_PURCHASE_MIN,
STARS_WINNERS_MAX, STARS_WINNERS_MAX,
STARS_WINNERS_MIN, STARS_WINNERS_MIN,
TON_TOPUP_MAX,
TON_TOPUP_MIN,
TONAPI_KEY_MIN_LENGTH, TONAPI_KEY_MIN_LENGTH,
) )
@@ -39,7 +39,7 @@ class ConfigurationError(ClientError):
INVALID_STARS_AMOUNT = ( INVALID_STARS_AMOUNT = (
f"Invalid Stars amount: must be an integer between {STARS_PURCHASE_MIN:,} and {STARS_PURCHASE_MAX:,}." 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 = (
"Invalid username '{username}'. " "Invalid username '{username}'. "
"Must be 5-32 characters and contain only letters (A-Z, a-z), digits (0-9), or underscores (_)." "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 = ( MISSING_BROWSER_KEYS = (
"Fragment cookies not found in {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." 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): 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." INVALID_PAYLOAD = "Fragment returned an invalid transaction payload: 'transaction.messages' is missing or empty."
BROADCAST_FAILED = "Transaction broadcast failed: {exc}" BROADCAST_FAILED = "Transaction broadcast failed: {exc}"
@@ -123,7 +123,7 @@ class TransactionError(FragmentAPIError):
"located in your Python installation folder." "located in your Python installation folder."
) )
DUPLICATE_SEQNO = ( 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) " "because a previous transaction with the same sequence number (seqno) "
"is still pending confirmation on-chain.\n" "is still pending confirmation on-chain.\n"
"Wait a few seconds for the previous transaction to confirm, then retry." "Wait a few seconds for the previous transaction to confirm, then retry."
@@ -150,14 +150,16 @@ class OperationError(FragmentError):
class WalletError(OperationError): 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." 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}" USDT_BALANCE_CHECK_FAILED = "Failed to fetch USDT balance: {exc}"
ACCOUNT_INFO_FAILED = "Failed to retrieve wallet account 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 TON network: {exc}" WALLET_INFO_FAILED = "Failed to retrieve wallet info from GRAM (ex TON) network: {exc}"
class UnexpectedError(OperationError): class UnexpectedError(OperationError):
+2 -2
View File
@@ -15,8 +15,8 @@ else:
class PaymentMethod(StrEnum): class PaymentMethod(StrEnum):
TON = "ton" GRAM = "ton"
USDT_TON = "usdt_ton" USDT_GRAM = "usdt_ton"
# Not supported yet # Not supported yet
USDT_ETH = "usdt_eth" USDT_ETH = "usdt_eth"
+2 -2
View File
@@ -30,7 +30,7 @@ class AdsTopupResult:
amount: int amount: int
def __repr__(self) -> str: 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 @dataclass
@@ -39,7 +39,7 @@ class AdsRechargeResult:
amount: int amount: int
def __repr__(self) -> str: 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"] __all__ = ["AdsRechargeResult", "AdsTopupResult", "PremiumResult", "StarsResult"]
+2 -2
View File
@@ -7,13 +7,13 @@ from dataclasses import dataclass
class WalletInfo: class WalletInfo:
address: str address: str
state: str state: str
ton_balance: float gram_balance: float
usdt_balance: float usdt_balance: float
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"WalletInfo(address='{self.address}', state='{self.state}', " 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)"
) )
+9 -3
View File
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "pyfragment" name = "pyfragment"
version = "2026.3.2" 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" readme = "README.md"
license = { text = "MIT" } license = { text = "MIT" }
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -91,10 +91,16 @@ ignore = ["E501", "UP017"]
[tool.mypy] [tool.mypy]
python_version = "3.10" python_version = "3.10"
strict = true strict = true
exclude = ["^examples/", "^logs/"] exclude = ["^examples/", "^logs/", "^tests/"]
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = "rookiepy" module = [
"rookiepy",
"httpx",
"ton_core",
"tonutils",
"tonutils.*",
]
ignore_missing_imports = true ignore_missing_imports = true
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
+10 -10
View File
@@ -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 collections.abc import Generator
from contextlib import contextmanager from contextlib import contextmanager
@@ -22,7 +22,7 @@ TRANSACTION_DATA = {
"messages": [ "messages": [
{ {
"address": "0:852443f8599fe6a5da34fe43049ac4e0beb3071bb2bfb56635ea9421287c283a", "address": "0:852443f8599fe6a5da34fe43049ac4e0beb3071bb2bfb56635ea9421287c283a",
"amount": "500000000", # 0.5 TON "amount": "500000000", # 0.5 GRAM
"payload": "", "payload": "",
} }
] ]
@@ -63,7 +63,7 @@ def _patch_wallet(wallet: MagicMock) -> Generator[None, None, None]:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_sufficient_balance_broadcasts() -> None: 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"): with _patch_wallet(wallet), patch("pyfragment.domains.tonapi.transaction.clean_decode", return_value="50 Telegram Stars"):
result = await process_transaction(_make_client(), TRANSACTION_DATA) result = await process_transaction(_make_client(), TRANSACTION_DATA)
assert result == "abc123" assert result == "abc123"
@@ -72,7 +72,7 @@ async def test_sufficient_balance_broadcasts() -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_insufficient_balance_raises() -> None: 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 _patch_wallet(wallet):
with pytest.raises(WalletError, match="required"): with pytest.raises(WalletError, match="required"):
await process_transaction(_make_client(), TRANSACTION_DATA) await process_transaction(_make_client(), TRANSACTION_DATA)
@@ -89,7 +89,7 @@ async def test_exact_minimum_balance_broadcasts() -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_one_nanoton_below_minimum_raises() -> None: 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 _patch_wallet(wallet):
with pytest.raises(WalletError, match="required"): with pytest.raises(WalletError, match="required"):
await process_transaction(_make_client(), TRANSACTION_DATA) await process_transaction(_make_client(), TRANSACTION_DATA)
@@ -142,11 +142,11 @@ async def test_duplicate_seqno_raises_after_retries() -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_usdt_payment_requires_min_ton_gas_reserve() -> None: async def test_usdt_payment_requires_min_gram_gas_reserve() -> None:
wallet = _make_wallet(balance_nanotons=10_000_000) # 0.01 TON below MIN_TON_BALANCE 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 _patch_wallet(wallet), patch("pyfragment.domains.tonapi.account.get_usdt_balance", AsyncMock(return_value=100.0)):
with pytest.raises(WalletError, match="Insufficient TON balance"): with pytest.raises(WalletError, match="Insufficient GRAM"):
await process_transaction(_make_client(), TRANSACTION_DATA, payment_method=PaymentMethod.USDT_TON) await process_transaction(_make_client(), TRANSACTION_DATA, payment_method=PaymentMethod.USDT_GRAM)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -174,6 +174,6 @@ async def test_usdt_payment_checks_usdt_balance() -> None:
await process_transaction( await process_transaction(
_make_client(), _make_client(),
transaction, transaction,
payment_method=PaymentMethod.USDT_TON, payment_method=PaymentMethod.USDT_GRAM,
required_payment_amount=12.5, required_payment_amount=12.5,
) )
+2 -2
View File
@@ -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, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
patch.object(_purchase_stars_mod, "process_transaction", proc_mock), 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] init_call = call_mock.await_args_list[2]
assert init_call.args[0] == "initBuyStarsRequest" 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, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
patch.object(_giveaway_stars_mod, "process_transaction", proc_mock), 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] init_call = call_mock.await_args_list[3]
assert init_call.args[0] == "initGiveawayStarsRequest" assert init_call.args[0] == "initGiveawayStarsRequest"
+2 -2
View File
@@ -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, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
patch.object(_purchase_premium_mod, "process_transaction", proc_mock), 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] init_call = call_mock.await_args_list[2]
assert init_call.args[0] == "initGiftPremiumRequest" 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, "get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
patch.object(_giveaway_premium_mod, "process_transaction", proc_mock), 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] init_call = call_mock.await_args_list[3]
assert init_call.args[0] == "initGiveawayPremiumRequest" assert init_call.args[0] == "initGiveawayPremiumRequest"
+17 -17
View File
@@ -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 from unittest.mock import AsyncMock, patch
import pytest 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 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 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 @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): 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 @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): 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 @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): 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 @pytest.mark.asyncio
async def test_topup_ton_success(client: FragmentClient) -> None: async def test_topup_gram_success(client: FragmentClient) -> None:
with ( with (
patch.object( patch.object(
client, 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_gram_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, "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 isinstance(result, AdsTopupResult)
assert result.transaction_id == FAKE_TX_HASH assert result.transaction_id == FAKE_TX_HASH
@@ -60,7 +60,7 @@ async def test_topup_ton_success(client: FragmentClient) -> None:
@pytest.mark.asyncio @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( with patch.object(
client, client,
"call", "call",
@@ -72,4 +72,4 @@ async def test_topup_ton_user_not_found(client: FragmentClient) -> None:
), ),
): ):
with pytest.raises(UserNotFoundError): with pytest.raises(UserNotFoundError):
await client.topup_ton("@ghost", amount=10) await client.topup_gram("@ghost", amount=10)
+6 -6
View File
@@ -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 from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
from pyfragment import FragmentClient, WalletInfo 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 @pytest.mark.asyncio
async def test_get_wallet_returns_wallet_info(client: FragmentClient) -> None: async def test_get_wallet_returns_wallet_info(client: FragmentClient) -> None:
mock_wallet = MagicMock() mock_wallet = MagicMock()
mock_wallet.refresh = AsyncMock() mock_wallet.refresh = AsyncMock()
mock_wallet.balance = FAKE_BALANCE_NANOTON mock_wallet.balance = FAKE_BALANCE_NANOGRAM
mock_wallet.state = MagicMock(value="active") mock_wallet.state = MagicMock(value="active")
mock_wallet.address.to_str.return_value = FAKE_ADDRESS 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 isinstance(result, WalletInfo)
assert result.address == FAKE_ADDRESS assert result.address == FAKE_ADDRESS
assert result.state == "active" 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 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() 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.usdt_balance == 0.0
assert result.state == "uninit" assert result.state == "uninit"
+3 -3
View File
@@ -6,7 +6,7 @@ import pytest
import pyfragment.domains.ads.recharge as _recharge_ads_mod import pyfragment.domains.ads.recharge as _recharge_ads_mod
from pyfragment import AdsRechargeResult, ConfigurationError, FragmentClient 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 from tests.shared import FAKE_ACCOUNT, FAKE_ADS_ACCOUNT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
# recharge_ads validation tests # 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 @pytest.mark.asyncio
async def test_recharge_ads_amount_zero(client: FragmentClient) -> None: async def test_recharge_ads_amount_zero(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError): 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 @pytest.mark.asyncio
async def test_recharge_ads_amount_too_high(client: FragmentClient) -> None: async def test_recharge_ads_amount_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError): 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 @pytest.mark.asyncio
+2 -2
View File
@@ -1,7 +1,7 @@
"""Shared test constants for the pyfragment test suite. """Shared test constants for the pyfragment test suite.
pyfragment is an async Python client for the Fragment API — a unified toolkit 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, run giveaways, manage anonymous numbers, and explore the marketplace for usernames,
numbers, and gifts. numbers, and gifts.
""" """
@@ -31,7 +31,7 @@ FAKE_RESPONSE: dict[str, Any] = {"status": "ok", "data": {"value": 42}}
# get_wallet() # get_wallet()
FAKE_ADDRESS: str = "UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5" 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 # recharge_ads
FAKE_ADS_ACCOUNT: str = "@mychannel" FAKE_ADS_ACCOUNT: str = "@mychannel"