mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
refactor: rename error classes for consistency and clarity; update related code and tests
This commit is contained in:
@@ -53,7 +53,7 @@ body:
|
||||
attributes:
|
||||
label: Current behavior
|
||||
description: Describe what is actually happening.
|
||||
placeholder: e.g. RequestError is raised with status 400.
|
||||
placeholder: e.g. ParseError is raised with status 400.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -91,7 +91,7 @@ body:
|
||||
Traceback (most recent call last):
|
||||
File "main.py", line 7, in main
|
||||
...
|
||||
fragmentapi.types.RequestError: ...
|
||||
fragmentapi.types.ParseError: ...
|
||||
render: sh
|
||||
|
||||
- type: textarea
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<div align="center">
|
||||
<img src="fragment.svg" alt="Fragment Logo" width="120" height="120" style="border-radius: 24px;">
|
||||
|
||||
<h1 style="margin-top: 24px;">💎 Fragment API by @bohd4nx</h1>
|
||||
<h1 style="margin-top: 24px;">💎 Fragment API</h1>
|
||||
|
||||
<p style="font-size: 18px; margin-bottom: 24px;">
|
||||
<b>Automate TON topups, Telegram Premium purchases, and Stars transactions via Fragment.com</b>
|
||||
<b>Python library for the Fragment.com API — gift Telegram Stars, Premium, and top up TON Ads balance.</b>
|
||||
</p>
|
||||
|
||||
[](https://python.org)
|
||||
[](https://github.com/nessshon/tonutils)
|
||||
[](https://pypi.org/project/fragmentapi/)
|
||||
[](https://pypi.org/project/fragmentapi/)
|
||||
[](https://python.org)
|
||||
[](LICENSE)
|
||||
[](https://github.com/bohd4nx/FragmentAPI/stargazers)
|
||||
[](https://github.com/bohd4nx/FragmentAPI/issues)
|
||||
[](https://github.com/bohd4nx/FragmentAPI/actions)
|
||||
|
||||
[Report Bug](https://github.com/bohd4nx/fragmentapi/issues) · [Request Feature](https://github.com/bohd4nx/fragmentapi/issues) · [**Donate TON**](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5)
|
||||
[Report Bug](https://github.com/bohd4nx/FragmentAPI/issues) · [Request Feature](https://github.com/bohd4nx/FragmentAPI/issues) · [**Donate TON**](https://app.tonkeeper.com/transfer/UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -21,51 +22,94 @@
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 💰 **TON Advertisement Topups** — Send TON directly to Fragment ad accounts (1–1,000,000,000 TON)
|
||||
- 👑 **Telegram Premium Gifts** — Purchase Premium subscriptions for any user (3, 6, or 12 months)
|
||||
- ⭐ **Telegram Stars Purchases** — Buy Stars and send them to any Telegram user (50–1,000,000 Stars)
|
||||
- 🔐 **Multi-wallet support** — Configurable wallet contract version (V4R2 / V5R1)
|
||||
- 💰 **TON Advertisement Topups** — Top up Telegram Ads balance (1–1,000,000,000 TON)
|
||||
- 👑 **Telegram Premium Gifts** — Gift Premium to any user (3, 6, or 12 months)
|
||||
- ⭐ **Telegram Stars Purchases** — Gift Stars to any Telegram user (50–1,000,000 Stars)
|
||||
- 🔐 **Multi-wallet support** — V4R2 and V5R1 wallet contract versions
|
||||
- ⚡ **Async-first** — Built on `httpx` and `asyncio`
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
pip install fragmentapi
|
||||
```
|
||||
|
||||
Requires **Python 3.12+**.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Installation
|
||||
```python
|
||||
import asyncio
|
||||
from fragmentapi import FragmentClient
|
||||
|
||||
```bash
|
||||
git clone https://github.com/bohd4nx/FragmentAPI.git
|
||||
cd FragmentAPI
|
||||
pip install -r requirements.txt
|
||||
client = FragmentClient(
|
||||
seed="word1 word2 ... word24",
|
||||
api_key="YOUR_TONAPI_KEY",
|
||||
cookies={
|
||||
"stel_ssid": "...",
|
||||
"stel_dt": "...",
|
||||
"stel_token": "...",
|
||||
"stel_ton_token": "...",
|
||||
},
|
||||
)
|
||||
|
||||
async def main():
|
||||
# Gift 6 months of Telegram Premium
|
||||
result = await client.gift_premium("@username", months=6)
|
||||
print(result.transaction_id)
|
||||
|
||||
# Gift 500 Stars
|
||||
result = await client.gift_stars("@username", amount=500)
|
||||
print(result.transaction_id)
|
||||
|
||||
# Top up 10 TON to Ads balance
|
||||
result = await client.topup_ton("@username", amount=10)
|
||||
print(result.transaction_id)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 2. Configuration
|
||||
See the [`examples/`](examples/) folder for ready-to-run scripts.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
cp cookies.example.json cookies.json
|
||||
```
|
||||
---
|
||||
|
||||
Edit `.env`:
|
||||
## 🔧 Configuration
|
||||
|
||||
```env
|
||||
# 24-word TON wallet seed phrase
|
||||
SEED = word1 word2 word3 ... word24
|
||||
### `FragmentClient` parameters
|
||||
|
||||
# API key from @tonapibot on Telegram
|
||||
API_KEY = your_tonapi_key_here
|
||||
| Parameter | Type | Required | Default | Description |
|
||||
| ---------------- | ------------- | -------- | -------- | -------------------------------------------------------- |
|
||||
| `seed` | `str` | ✅ | — | 24-word TON wallet mnemonic phrase |
|
||||
| `api_key` | `str` | ✅ | — | Tonapi key from [tonconsole.com](https://tonconsole.com) |
|
||||
| `cookies` | `dict \| str` | ✅ | — | Fragment session cookies (dict or JSON string) |
|
||||
| `wallet_version` | `str` | ❌ | `"V5R1"` | Wallet contract version: `"V4R2"` or `"V5R1"` |
|
||||
|
||||
# Wallet contract version: V4R2 or V5R1 (default: V5R1)
|
||||
WALLET_VERSION = V5R1
|
||||
```
|
||||
### Methods
|
||||
|
||||
### 3. Getting Required Data
|
||||
> Usernames can be passed with or without `@`.
|
||||
|
||||
#### 🍪 Fragment.com Cookies
|
||||
| Method | Returns | Description | Limits |
|
||||
| -------------------------------------------------- | ---------------- | ---------------------------------- | ------------------------- |
|
||||
| `gift_premium(username, months, show_sender=True)` | `PremiumResult` | Gift Telegram Premium subscription | `months`: 3, 6, or 12 |
|
||||
| `gift_stars(username, amount, show_sender=True)` | `StarsResult` | Gift Telegram Stars | `amount`: 50–1,000,000 |
|
||||
| `topup_ton(username, amount, show_sender=True)` | `AdsTopupResult` | Top up Telegram Ads balance | `amount`: 1–1,000,000,000 |
|
||||
|
||||
**Prerequisites**: Log in to Telegram on Fragment and connect the TON wallet you'll use for payments.
|
||||
---
|
||||
|
||||
1. Install [Cookie Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm) extension
|
||||
2. Open [fragment.com](https://fragment.com) and make sure you're logged in
|
||||
3. Click the Cookie Editor icon → **Export** → **Header String**
|
||||
4. Split the result into the four fields in `cookies.json`:
|
||||
## ⚙️ Getting Required Credentials
|
||||
|
||||
### 🍪 Fragment.com Cookies
|
||||
|
||||
**Prerequisites**: Log in to [fragment.com](https://fragment.com), connect your TON wallet.
|
||||
|
||||
1. Install [Cookie Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm)
|
||||
2. Open [fragment.com](https://fragment.com) while logged in
|
||||
3. Click the extension → **Export** → **JSON**
|
||||
4. Extract these four fields:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -76,107 +120,48 @@ WALLET_VERSION = V5R1
|
||||
}
|
||||
```
|
||||
|
||||
#### 🔐 TON Wallet Seed Phrase
|
||||
> ⚠️ Cookies expire. Refresh them if you start getting `FragmentPageError` or auth errors.
|
||||
|
||||
If you don't have a TON wallet, create one in [Tonkeeper](https://tonkeeper.com) (iOS / Android).
|
||||
Go to **Settings → Backup**, copy the 24 words and paste them into `SEED` in `.env`.
|
||||
|
||||
> ⚠️ Never share your seed phrase with anyone. Store it offline.
|
||||
|
||||
#### 🔑 TON API Key
|
||||
### 🔑 Tonapi Key
|
||||
|
||||
1. Go to [tonconsole.com](https://tonconsole.com)
|
||||
2. Create an account and log in
|
||||
3. Generate a new API key
|
||||
4. Paste it into `API_KEY` in `.env`
|
||||
2. Register and generate a new API key
|
||||
3. Pass it as `api_key` to `FragmentClient`
|
||||
|
||||
#### 🔐 Wallet Version
|
||||
### 🌱 Wallet Seed Phrase
|
||||
|
||||
If you don't have a TON wallet, create one in [Tonkeeper](https://tonkeeper.com).
|
||||
Go to **Settings → Backup** → copy the 24 words.
|
||||
|
||||
> ⚠️ Never share your seed phrase. Store it offline.
|
||||
|
||||
### 🔐 Wallet Version
|
||||
|
||||
| Version | Use when |
|
||||
| ------- | -------------------------------------------------------------- |
|
||||
| `V5R1` | Default — Tonkeeper / MyTonWallet (wallets created after 2024) |
|
||||
| `V4R2` | Older Tonkeeper wallets |
|
||||
| `V4R2` | Older Tonkeeper or hardware wallets |
|
||||
|
||||
Not sure? Run this to check which address matches your wallet:
|
||||
---
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import asyncio
|
||||
from tonutils.clients import TonapiClient
|
||||
from tonutils.contracts.wallet import WalletV4R2, WalletV5R1
|
||||
from tonutils.types import NetworkGlobalID
|
||||
from app.core import config
|
||||
## 🗂️ Error Handling
|
||||
|
||||
client = TonapiClient(network=NetworkGlobalID.MAINNET, api_key=config.API_KEY)
|
||||
w4, _, _, _ = WalletV4R2.from_mnemonic(client=client, mnemonic=config.SEED)
|
||||
w5, _, _, _ = WalletV5R1.from_mnemonic(client=client, mnemonic=config.SEED)
|
||||
print('V4R2:', w4.address.to_str(True, True))
|
||||
print('V5R1:', w5.address.to_str(True, True))
|
||||
"
|
||||
```
|
||||
|
||||
### 4. Usage
|
||||
|
||||
#### Run Examples
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
#### Programmatic Usage
|
||||
All exceptions inherit from `FragmentError` — see [`fragmentapi/types/exceptions.py`](fragmentapi/types/exceptions.py) for the full list.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from app.methods import topup_ton, buy_premium, buy_stars
|
||||
from fragmentapi import FragmentClient, UserNotFoundError, ConfigurationError, WalletError
|
||||
|
||||
async def main():
|
||||
# Send 10 TON to @username
|
||||
result = await topup_ton("@username", 10)
|
||||
print(result)
|
||||
|
||||
# Gift 6 months of Telegram Premium (anonymous — recipient won't see sender)
|
||||
result = await buy_premium("@username", 6, show_sender=False)
|
||||
print(result)
|
||||
|
||||
# Buy 500 Stars for @username
|
||||
result = await buy_stars("@username", 500)
|
||||
print(result)
|
||||
|
||||
asyncio.run(main())
|
||||
try:
|
||||
result = await client.gift_stars("@unknown", amount=100)
|
||||
except UserNotFoundError:
|
||||
print("User not found on Fragment")
|
||||
except WalletError as e:
|
||||
print(f"Wallet issue: {e}")
|
||||
except ConfigurationError as e:
|
||||
print(f"Bad params: {e}")
|
||||
```
|
||||
|
||||
**Return format** (on success):
|
||||
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"data": {
|
||||
"transaction_id": "<TL-B ExternalMessage ...>",
|
||||
"username": "@username",
|
||||
"amount": 10, # or "months" for Premium
|
||||
"timestamp": 1741234567
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Return format** (on failure):
|
||||
|
||||
```python
|
||||
{
|
||||
"success": False,
|
||||
"error": "Telegram user '@unknown' was not found on Fragment."
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Operations
|
||||
|
||||
| Operation | Function | Parameters | Limits |
|
||||
| ------------------ | ----------------------------------------------------- | ----------------------------------- | ------------------- |
|
||||
| **TON Topup** | `topup_ton(username, amount, show_sender=True)` | Username, TON amount, show sender | 1–1,000,000,000 TON |
|
||||
| **Premium Gift** | `buy_premium(username, months, show_sender=True)` | Username, duration, show sender | 3, 6, or 12 months |
|
||||
| **Stars Purchase** | `buy_stars(username, amount, show_sender=True)` | Username, Stars amount, show sender | 50–1,000,000 Stars |
|
||||
|
||||
Usernames can be passed with or without `@`.
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Example: initializing FragmentClient.
|
||||
|
||||
Cookies can be passed as a dict or as a JSON string.
|
||||
wallet_version defaults to "V5R1" — change to "V4R2" for older wallets.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fragmentapi import FragmentClient
|
||||
|
||||
SEED = "word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 word13 word14 word15 word16 word17 word18 word19 word20 word21 word22 word23 word24"
|
||||
API_KEY = "YOUR_TONAPI_KEY"
|
||||
COOKIES = {
|
||||
"stel_ssid": "YOUR_STEL_SSID",
|
||||
"stel_dt": "YOUR_STEL_DT",
|
||||
"stel_token": "YOUR_STEL_TOKEN",
|
||||
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
|
||||
}
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = FragmentClient(
|
||||
seed=SEED,
|
||||
api_key=API_KEY,
|
||||
cookies=COOKIES,
|
||||
wallet_version="V5R1", # or "V4R2"
|
||||
)
|
||||
|
||||
print("FragmentClient initialized")
|
||||
print(" %-16s %s" % ("Wallet version:", client.wallet_version))
|
||||
print(" %-16s %s..." % ("API key:", client.api_key[:8]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Example: gift Telegram Premium to a user.
|
||||
|
||||
Supported durations: 3, 6, or 12 months.
|
||||
Set show_sender=False to send anonymously.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fragmentapi import ConfigurationError, FragmentClient, UserNotFoundError
|
||||
|
||||
SEED = "word1 word2 ... word24"
|
||||
API_KEY = "YOUR_TONAPI_KEY"
|
||||
COOKIES = {
|
||||
"stel_ssid": "YOUR_STEL_SSID",
|
||||
"stel_dt": "YOUR_STEL_DT",
|
||||
"stel_token": "YOUR_STEL_TOKEN",
|
||||
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
|
||||
}
|
||||
|
||||
USERNAME = "@username"
|
||||
MONTHS = 3 # 3, 6, or 12
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES)
|
||||
|
||||
try:
|
||||
result = await client.gift_premium(USERNAME, months=MONTHS, show_sender=True)
|
||||
except UserNotFoundError:
|
||||
print(f"User {USERNAME!r} not found on Fragment.")
|
||||
return
|
||||
except ConfigurationError as e:
|
||||
print(f"Invalid parameters: {e}")
|
||||
return
|
||||
|
||||
print("Premium gifted")
|
||||
print(" %-14s %s" % ("Username:", result.username))
|
||||
print(" %-14s %s months" % ("Duration:", result.months))
|
||||
print(" %-14s %s" % ("Transaction:", result.transaction_id))
|
||||
print(" %-14s %s" % ("Timestamp:", result.timestamp))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Example: gift Telegram Stars to a user.
|
||||
|
||||
Amount must be an integer between 50 and 1 000 000.
|
||||
Set show_sender=False to send anonymously.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fragmentapi import ConfigurationError, FragmentClient, UserNotFoundError
|
||||
|
||||
SEED = "word1 word2 ... word24"
|
||||
API_KEY = "YOUR_TONAPI_KEY"
|
||||
COOKIES = {
|
||||
"stel_ssid": "YOUR_STEL_SSID",
|
||||
"stel_dt": "YOUR_STEL_DT",
|
||||
"stel_token": "YOUR_STEL_TOKEN",
|
||||
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
|
||||
}
|
||||
|
||||
USERNAME = "@username"
|
||||
AMOUNT = 500 # 50–1 000 000
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES)
|
||||
|
||||
try:
|
||||
result = await client.gift_stars(USERNAME, amount=AMOUNT, show_sender=True)
|
||||
except UserNotFoundError:
|
||||
print(f"User {USERNAME!r} not found on Fragment.")
|
||||
return
|
||||
except ConfigurationError as e:
|
||||
print(f"Invalid parameters: {e}")
|
||||
return
|
||||
|
||||
print("Stars gifted")
|
||||
print(" %-14s %s" % ("Username:", result.username))
|
||||
print(" %-14s %s" % ("Stars:", result.stars))
|
||||
print(" %-14s %s" % ("Transaction:", result.transaction_id))
|
||||
print(" %-14s %s" % ("Timestamp:", result.timestamp))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Example: top up a Telegram Ads account with TON.
|
||||
|
||||
Amount must be an integer between 1 and 1 000 000 000 TON.
|
||||
Your wallet must hold at least the topup amount + ~0.056 TON for gas.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fragmentapi import ConfigurationError, FragmentClient, UserNotFoundError, WalletError
|
||||
|
||||
SEED = "word1 word2 ... word24"
|
||||
API_KEY = "YOUR_TONAPI_KEY"
|
||||
COOKIES = {
|
||||
"stel_ssid": "YOUR_STEL_SSID",
|
||||
"stel_dt": "YOUR_STEL_DT",
|
||||
"stel_token": "YOUR_STEL_TOKEN",
|
||||
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
|
||||
}
|
||||
|
||||
USERNAME = "@username"
|
||||
AMOUNT = 10 # TON, integer — 1–1 000 000 000
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES)
|
||||
|
||||
try:
|
||||
result = await client.topup_ton(USERNAME, amount=AMOUNT, show_sender=True)
|
||||
except UserNotFoundError:
|
||||
print(f"User {USERNAME!r} not found on Fragment.")
|
||||
return
|
||||
except WalletError as e:
|
||||
print(f"Wallet error: {e}")
|
||||
return
|
||||
except ConfigurationError as e:
|
||||
print(f"Invalid parameters: {e}")
|
||||
return
|
||||
|
||||
print("TON topped up")
|
||||
print(" %-14s %s" % ("Username:", result.username))
|
||||
print(" %-14s %s TON" % ("Amount:", result.amount))
|
||||
print(" %-14s %s" % ("Transaction:", result.transaction_id))
|
||||
print(" %-14s %s" % ("Timestamp:", result.timestamp))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -7,14 +7,14 @@ from fragmentapi.client import FragmentClient
|
||||
from fragmentapi.types import (
|
||||
AdsTopupResult,
|
||||
ClientError,
|
||||
ConfigError,
|
||||
CookiesError,
|
||||
ConfigurationError,
|
||||
CookieError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
HashFetchError,
|
||||
FragmentPageError,
|
||||
OperationError,
|
||||
ParseError,
|
||||
PremiumResult,
|
||||
RequestError,
|
||||
StarsResult,
|
||||
TransactionError,
|
||||
UnexpectedError,
|
||||
@@ -29,13 +29,13 @@ __all__ = [
|
||||
"PremiumResult",
|
||||
"StarsResult",
|
||||
"ClientError",
|
||||
"ConfigError",
|
||||
"CookiesError",
|
||||
"ConfigurationError",
|
||||
"CookieError",
|
||||
"FragmentAPIError",
|
||||
"FragmentError",
|
||||
"HashFetchError",
|
||||
"FragmentPageError",
|
||||
"OperationError",
|
||||
"RequestError",
|
||||
"ParseError",
|
||||
"TransactionError",
|
||||
"UnexpectedError",
|
||||
"UserNotFoundError",
|
||||
|
||||
+11
-9
@@ -7,8 +7,8 @@ from fragmentapi.types import (
|
||||
REQUIRED_COOKIE_KEYS,
|
||||
SUPPORTED_WALLET_VERSIONS,
|
||||
AdsTopupResult,
|
||||
ConfigError,
|
||||
CookiesError,
|
||||
ConfigurationError,
|
||||
CookieError,
|
||||
PremiumResult,
|
||||
StarsResult,
|
||||
WalletVersion,
|
||||
@@ -26,8 +26,8 @@ class FragmentClient:
|
||||
wallet_version: Wallet contract version — ``"V4R2"`` or ``"V5R1"`` (default).
|
||||
|
||||
Raises:
|
||||
ConfigError: If ``seed``, ``api_key``, or ``wallet_version`` are missing or invalid.
|
||||
CookiesError: If ``cookies`` cannot be parsed or are missing required keys.
|
||||
ConfigurationError: If ``seed``, ``api_key``, or ``wallet_version`` are missing or invalid.
|
||||
CookieError: If ``cookies`` cannot be parsed or are missing required keys.
|
||||
|
||||
Example::
|
||||
|
||||
@@ -49,22 +49,24 @@ class FragmentClient:
|
||||
) -> None:
|
||||
missing = [name for name, val in (("seed", seed), ("api_key", api_key)) if not val or not str(val).strip()]
|
||||
if missing:
|
||||
raise ConfigError(ConfigError.MISSING_VARS.format(keys=", ".join(missing)))
|
||||
raise ConfigurationError(ConfigurationError.MISSING_VARS.format(keys=", ".join(missing)))
|
||||
|
||||
if isinstance(cookies, str):
|
||||
try:
|
||||
cookies = json.loads(cookies)
|
||||
except Exception as exc:
|
||||
raise CookiesError(CookiesError.READ_FAILED.format(exc=exc)) from exc
|
||||
raise CookieError(CookieError.READ_FAILED.format(exc=exc)) from exc
|
||||
|
||||
missing_keys = [k for k in REQUIRED_COOKIE_KEYS if not str(cookies.get(k, "")).strip()]
|
||||
if missing_keys:
|
||||
raise CookiesError(CookiesError.MISSING_KEYS.format(keys=", ".join(missing_keys)))
|
||||
raise CookieError(CookieError.MISSING_KEYS.format(keys=", ".join(missing_keys)))
|
||||
|
||||
version = wallet_version.strip().upper()
|
||||
if version not in SUPPORTED_WALLET_VERSIONS:
|
||||
raise ConfigError(
|
||||
ConfigError.UNSUPPORTED_VERSION.format(version=version, supported=", ".join(sorted(SUPPORTED_WALLET_VERSIONS)))
|
||||
raise ConfigurationError(
|
||||
ConfigurationError.UNSUPPORTED_VERSION.format(
|
||||
version=version, supported=", ".join(sorted(SUPPORTED_WALLET_VERSIONS))
|
||||
)
|
||||
)
|
||||
|
||||
self.seed: str = seed.strip()
|
||||
|
||||
@@ -8,7 +8,7 @@ from fragmentapi.types import (
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
PREMIUM_PAGE,
|
||||
ConfigError,
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
PremiumResult,
|
||||
@@ -91,7 +91,7 @@ async def _init_request(
|
||||
|
||||
async def gift_premium(client: "FragmentClient", username: str, months: int, show_sender: bool = True) -> PremiumResult:
|
||||
if months not in (3, 6, 12):
|
||||
raise ConfigError(ConfigError.INVALID_MONTHS)
|
||||
raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, PREMIUM_PAGE)
|
||||
|
||||
@@ -7,7 +7,7 @@ from fragmentapi.types import (
|
||||
BASE_HEADERS,
|
||||
DEVICE,
|
||||
STARS_PAGE,
|
||||
ConfigError,
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
StarsResult,
|
||||
@@ -78,7 +78,7 @@ async def _init_request(
|
||||
|
||||
async def gift_stars(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> StarsResult:
|
||||
if not isinstance(amount, int) or not (50 <= amount <= 1_000_000):
|
||||
raise ConfigError(ConfigError.INVALID_STARS_AMOUNT)
|
||||
raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT)
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, STARS_PAGE)
|
||||
|
||||
@@ -8,7 +8,7 @@ from fragmentapi.types import (
|
||||
DEVICE,
|
||||
TON_PAGE,
|
||||
AdsTopupResult,
|
||||
ConfigError,
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
UnexpectedError,
|
||||
@@ -78,7 +78,7 @@ async def _init_request(
|
||||
|
||||
async def topup_ton(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
|
||||
if not isinstance(amount, int) or not (1 <= amount <= 1_000_000_000):
|
||||
raise ConfigError(ConfigError.INVALID_TON_AMOUNT)
|
||||
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT)
|
||||
|
||||
try:
|
||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, TON_PAGE)
|
||||
|
||||
@@ -12,13 +12,13 @@ from fragmentapi.types.constants import (
|
||||
)
|
||||
from fragmentapi.types.exceptions import (
|
||||
ClientError,
|
||||
ConfigError,
|
||||
CookiesError,
|
||||
ConfigurationError,
|
||||
CookieError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
HashFetchError,
|
||||
FragmentPageError,
|
||||
OperationError,
|
||||
RequestError,
|
||||
ParseError,
|
||||
TransactionError,
|
||||
UnexpectedError,
|
||||
UserNotFoundError,
|
||||
@@ -41,14 +41,14 @@ __all__ = [
|
||||
"WalletVersion",
|
||||
# client exceptions
|
||||
"ClientError",
|
||||
"ConfigError",
|
||||
"CookiesError",
|
||||
"ConfigurationError",
|
||||
"CookieError",
|
||||
# fragment exceptions
|
||||
"FragmentAPIError",
|
||||
"FragmentError",
|
||||
"HashFetchError",
|
||||
"FragmentPageError",
|
||||
"OperationError",
|
||||
"RequestError",
|
||||
"ParseError",
|
||||
"TransactionError",
|
||||
"UnexpectedError",
|
||||
"UserNotFoundError",
|
||||
|
||||
@@ -6,7 +6,7 @@ class ClientError(FragmentError):
|
||||
"""Raised for client configuration and setup issues (bad params, invalid cookies)."""
|
||||
|
||||
|
||||
class ConfigError(ClientError):
|
||||
class ConfigurationError(ClientError):
|
||||
"""Raised when required client parameters are missing or invalid."""
|
||||
|
||||
MISSING_VARS = "Missing required parameter(s): {keys}."
|
||||
@@ -16,7 +16,7 @@ class ConfigError(ClientError):
|
||||
INVALID_TON_AMOUNT = "Amount must be an integer between 1 and 1 000 000 000 TON."
|
||||
|
||||
|
||||
class CookiesError(ClientError):
|
||||
class CookieError(ClientError):
|
||||
"""Raised when cookies are unreadable or missing required fields."""
|
||||
|
||||
READ_FAILED = "Failed to parse cookies: {exc}"
|
||||
@@ -33,8 +33,8 @@ class FragmentAPIError(FragmentError):
|
||||
)
|
||||
|
||||
|
||||
class HashFetchError(FragmentAPIError):
|
||||
"""Raised when the Fragment API hash cannot be fetched from the page."""
|
||||
class FragmentPageError(FragmentAPIError):
|
||||
"""Raised when the Fragment page cannot be fetched or the API hash is not found."""
|
||||
|
||||
BAD_STATUS = "Fragment returned HTTP {status} for {url}. " "Check that your cookies are valid and not expired."
|
||||
NOT_FOUND = (
|
||||
@@ -59,8 +59,8 @@ class TransactionError(FragmentAPIError):
|
||||
BROADCAST_FAILED = "Transaction broadcast failed: {exc}"
|
||||
|
||||
|
||||
class RequestError(FragmentAPIError):
|
||||
"""Raised when a Fragment API response cannot be parsed."""
|
||||
class ParseError(FragmentAPIError):
|
||||
"""Raised when a Fragment API response or payload cannot be parsed."""
|
||||
|
||||
UNPARSEABLE = "Fragment API returned an unparseable response for '{context}': {exc}"
|
||||
|
||||
@@ -92,13 +92,13 @@ class UnexpectedError(OperationError):
|
||||
__all__ = [
|
||||
"FragmentError",
|
||||
"ClientError",
|
||||
"ConfigError",
|
||||
"CookiesError",
|
||||
"ConfigurationError",
|
||||
"CookieError",
|
||||
"FragmentAPIError",
|
||||
"HashFetchError",
|
||||
"FragmentPageError",
|
||||
"UserNotFoundError",
|
||||
"TransactionError",
|
||||
"RequestError",
|
||||
"ParseError",
|
||||
"VerificationError",
|
||||
"OperationError",
|
||||
"WalletError",
|
||||
|
||||
@@ -2,7 +2,7 @@ import base64
|
||||
|
||||
from pytoniq_core import Cell
|
||||
|
||||
from fragmentapi.types import RequestError
|
||||
from fragmentapi.types import ParseError
|
||||
|
||||
|
||||
def clean_decode(payload: str) -> str:
|
||||
@@ -19,7 +19,7 @@ def clean_decode(payload: str) -> str:
|
||||
Decoded comment string, or ``""`` for an empty payload.
|
||||
|
||||
Raises:
|
||||
RequestError: If the payload cannot be decoded or parsed.
|
||||
ParseError: If the payload cannot be decoded or parsed.
|
||||
"""
|
||||
s = payload.strip()
|
||||
if not s:
|
||||
@@ -32,4 +32,4 @@ def clean_decode(payload: str) -> str:
|
||||
sl.load_uint(32) # op code — always 0 for text comment
|
||||
return sl.load_snake_string().strip()
|
||||
except Exception as exc:
|
||||
raise RequestError(RequestError.UNPARSEABLE.format(context="payload decode", exc=exc)) from exc
|
||||
raise ParseError(ParseError.UNPARSEABLE.format(context="payload decode", exc=exc)) from exc
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from fragmentapi.types import HashFetchError, RequestError, VerificationError
|
||||
from fragmentapi.types import FragmentPageError, ParseError, VerificationError
|
||||
|
||||
|
||||
async def get_fragment_hash(
|
||||
@@ -26,7 +26,7 @@ async def get_fragment_hash(
|
||||
Lowercase hex hash string.
|
||||
|
||||
Raises:
|
||||
HashFetchError: If the page returns a non-200 status or the hash
|
||||
FragmentPageError: If the page returns a non-200 status or the hash
|
||||
is not found in the response HTML.
|
||||
"""
|
||||
page_headers = {
|
||||
@@ -48,11 +48,11 @@ async def get_fragment_hash(
|
||||
response = await session.get(page_url, headers=page_headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise HashFetchError(HashFetchError.BAD_STATUS.format(status=response.status_code, url=page_url))
|
||||
raise FragmentPageError(FragmentPageError.BAD_STATUS.format(status=response.status_code, url=page_url))
|
||||
|
||||
match = re.search(r"(?:https://fragment\.com)?/api\?hash=([a-f0-9]+)", response.text)
|
||||
if not match:
|
||||
raise HashFetchError(HashFetchError.NOT_FOUND.format(url=page_url))
|
||||
raise FragmentPageError(FragmentPageError.NOT_FOUND.format(url=page_url))
|
||||
|
||||
return match.group(1)
|
||||
|
||||
@@ -68,12 +68,12 @@ def parse_json_response(response: httpx.Response, context: str) -> dict[str, Any
|
||||
Parsed response as a dict.
|
||||
|
||||
Raises:
|
||||
RequestError: If the response body cannot be decoded as JSON.
|
||||
ParseError: If the response body cannot be decoded as JSON.
|
||||
"""
|
||||
try:
|
||||
return response.json()
|
||||
except Exception as exc:
|
||||
raise RequestError(RequestError.UNPARSEABLE.format(context=context, exc=exc)) from exc
|
||||
raise ParseError(ParseError.UNPARSEABLE.format(context=context, exc=exc)) from exc
|
||||
|
||||
|
||||
async def fragment_post(
|
||||
@@ -124,7 +124,7 @@ async def execute_transaction_request(
|
||||
|
||||
Raises:
|
||||
VerificationError: If Fragment requires KYC verification.
|
||||
RequestError: If the response cannot be parsed.
|
||||
ParseError: If the response cannot be parsed.
|
||||
"""
|
||||
transaction = await fragment_post(session, fragment_hash, headers, tx_data)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
|
||||
import pytest
|
||||
|
||||
from fragmentapi.types import RequestError
|
||||
from fragmentapi.types import ParseError
|
||||
from fragmentapi.utils.decoder import clean_decode
|
||||
|
||||
PAYLOADS = [
|
||||
@@ -35,6 +35,6 @@ def test_empty_payload_returns_empty_string() -> None:
|
||||
assert clean_decode("") == ""
|
||||
|
||||
|
||||
def test_invalid_payload_raises_request_error() -> None:
|
||||
with pytest.raises(RequestError):
|
||||
def test_invalid_payload_raises_parse_error() -> None:
|
||||
with pytest.raises(ParseError):
|
||||
clean_decode("!!!not-valid-base64!!!")
|
||||
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
import pytest
|
||||
|
||||
from fragmentapi import FragmentClient
|
||||
from fragmentapi.types import ConfigError, CookiesError
|
||||
from fragmentapi.types import ConfigurationError, CookieError
|
||||
|
||||
VALID_SEED = "abandon " * 23 + "about"
|
||||
VALID_API_KEY = "test_api_key"
|
||||
@@ -35,22 +35,22 @@ def test_wallet_version_is_case_insensitive() -> None:
|
||||
|
||||
|
||||
def test_missing_seed_raises() -> None:
|
||||
with pytest.raises(ConfigError):
|
||||
with pytest.raises(ConfigurationError):
|
||||
FragmentClient(seed="", api_key=VALID_API_KEY, cookies=VALID_COOKIES)
|
||||
|
||||
|
||||
def test_whitespace_only_seed_raises() -> None:
|
||||
with pytest.raises(ConfigError):
|
||||
with pytest.raises(ConfigurationError):
|
||||
FragmentClient(seed=" ", api_key=VALID_API_KEY, cookies=VALID_COOKIES)
|
||||
|
||||
|
||||
def test_missing_api_key_raises() -> None:
|
||||
with pytest.raises(ConfigError):
|
||||
with pytest.raises(ConfigurationError):
|
||||
FragmentClient(seed=VALID_SEED, api_key="", cookies=VALID_COOKIES)
|
||||
|
||||
|
||||
def test_unsupported_wallet_version_raises() -> None:
|
||||
with pytest.raises(ConfigError):
|
||||
with pytest.raises(ConfigurationError):
|
||||
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=VALID_COOKIES, wallet_version="V3R2")
|
||||
|
||||
|
||||
@@ -60,22 +60,22 @@ def test_cookies_as_json_string() -> None:
|
||||
|
||||
|
||||
def test_invalid_cookies_json_raises() -> None:
|
||||
with pytest.raises(CookiesError):
|
||||
with pytest.raises(CookieError):
|
||||
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies="{not valid json}")
|
||||
|
||||
|
||||
def test_missing_cookie_key_raises() -> None:
|
||||
with pytest.raises(CookiesError):
|
||||
with pytest.raises(CookieError):
|
||||
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies={"stel_ssid": "x"})
|
||||
|
||||
|
||||
def test_empty_cookie_value_raises() -> None:
|
||||
bad = {**VALID_COOKIES, "stel_token": ""}
|
||||
with pytest.raises(CookiesError):
|
||||
with pytest.raises(CookieError):
|
||||
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=bad)
|
||||
|
||||
|
||||
def test_whitespace_cookie_value_raises() -> None:
|
||||
bad = {**VALID_COOKIES, "stel_ton_token": " "}
|
||||
with pytest.raises(CookiesError):
|
||||
with pytest.raises(CookieError):
|
||||
FragmentClient(seed=VALID_SEED, api_key=VALID_API_KEY, cookies=bad)
|
||||
|
||||
Reference in New Issue
Block a user