mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
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:
@@ -37,3 +37,7 @@ systests/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
uv.lock
|
||||
|
||||
#docs
|
||||
*-docs/
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
[](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)**
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__":
|
||||
|
||||
+15
-15
@@ -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"``.
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
+13
-11
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)"
|
||||
)
|
||||
|
||||
|
||||
|
||||
+9
-3
@@ -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]]
|
||||
|
||||
+10
-10
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+17
-17
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user