139 Commits

Author SHA1 Message Date
bohd4nx f7225b9d16 chore: update GitHub Actions to use latest versions of checkout and setup-python; add icon.svg 2026-07-23 00:28:48 +01:00
bohd4nx 27ce28fd32 fix: correct regex pattern in get_fragment_hash function to properly escape backslashes 2026-07-06 11:27:10 +03:00
bohd4nx b11b9f299b fix: correct regex pattern in get_fragment_hash function to properly match fragment hash 2026-07-05 16:47:26 +03:00
bohd4nx 0a361c8c3a chore: add CONTRIBUTING and SECURITY guidelines to improve project documentation 2026-07-05 16:25:23 +03:00
bohd4nx f46551c905 Refactor: remove httpx from mypy overrides in pyproject.toml 2026-07-05 16:18:40 +03:00
bohd4nx 2bd8cc177a chore: update version to 2026.3.3 and dependencies; improve string formatting in transport functions 2026-07-05 16:16:14 +03:00
bohd4nx fd31342d03 Refactor: Replace httpx with curl-cffi for HTTP requests and update dependencies 2026-07-05 16:13:16 +03:00
bohd4nx d78be012a3 chore: remove author credit from README footer 2026-06-27 02:46:41 +03:00
bohd4nx ef831d12a0 chore: update Python version to 3.11 in CI and project files; remove outdated CONTRIBUTING and SECURITY documents 2026-06-27 02:25:09 +03:00
bohd4nx 4539dc9803 Refactor tests: Add error handling tests for API provider and recharge flows 2026-06-16 01:49:11 +03:00
bohd4nx a9663d0b08 chore: update setup-uv action to v8.2.0 and update changelog date 2026-06-16 01:41:03 +03:00
bohd4nx 228fbfcd1c Refactor tonapi module: Move account and transaction logic to services
- Moved account-related functions and classes from `pyfragment.domains.tonapi.account` to `pyfragment.services.tonapi.account`.
- Moved transaction-related functions and classes from `pyfragment.domains.tonapi.transaction` to `pyfragment.services.tonapi.transaction`.
- Updated imports across the codebase to reflect the new structure.
- Removed unused `tonapi` module files and cleaned up related code.
- Introduced `ApiProvider` enum to manage API provider types.
- Added validation functions for cookies and wallet versions in a new `validation.py` module.
2026-06-16 01:37:26 +03:00
bohd4nx dc9661134d Refactor cookie handling and service structure
- Removed the old CookieResult model from pyfragment.core.models and created a new one in pyfragment.services.cookies.models.
- Implemented get_cookies_from_browser function in pyfragment.services.cookies.service to extract cookies directly from supported browsers.
- Updated imports across various modules to reflect the new structure and removed unused imports.
- Cleaned up models in ads, anonymous_numbers, giveaways, marketplace, and purchases domains by removing unnecessary __all__ declarations.
- Added new example scripts for cookie extraction and various purchase functionalities.
- Consolidated constants into a new pyfragment.core.constants module for better organization.
2026-06-16 01:19:33 +03:00
bohd4nx 9f11ccb3ee Refactor pyfragment structure: Move models to domain-specific modules
- Moved PaymentMethod and other enums from models to enums.py for better organization.
- Refactored imports in various files to reflect the new structure.
- Created new model files for ads, anonymous numbers, giveaways, marketplace, purchases, and tonapi.
- Removed the old models.py file to clean up the codebase.
- Updated all relevant imports in the codebase to use the new model locations.
2026-06-16 01:02:28 +03:00
bohd4nx 8f865f4911 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.
2026-06-16 00:47:49 +03:00
bohd4nx 3481769757 feat: add Highload wallet versions and update mypy exclusion patterns 2026-06-05 02:04:44 +03:00
bohd4nx b85b34f2b1 feat: update changelog for new payment methods; add UserNotFoundError handling in purchase flows 2026-05-30 00:17:06 +03:00
bohd4nx d01c45d85b feat: update version to 2026.3.2; enhance changelog with new payment methods and updated client documentation 2026-05-29 23:16:19 +03:00
bohd4nx 695430744a feat: update payment method options and validation across purchase and giveaway examples; add AlreadySubscribedError exception handling 2026-05-29 23:14:59 +03:00
bohd4nx afaa42776b feat: update payment method handling and validation across giveaway examples; adjust limits for winners and amounts 2026-05-29 22:59:41 +03:00
bohd4nx ed54d1f1a0 feat: enhance FragmentClient to support custom headers; update limits for TON topup and stars purchases 2026-05-29 22:51:27 +03:00
bohd4nx 6aa7037380 feat: update payment method defaults to use PaymentMethod enum; refactor related validation checks 2026-05-29 22:31:30 +03:00
bohd4nx 5124af17ef feat: reorganize constants into separate modules; update validation logic for mnemonic and API key lengths 2026-05-29 22:22:09 +03:00
bohd4nx 66bcd22198 feat: update constants and validation for stars and payment methods; refactor enums for clarity 2026-05-29 22:11:03 +03:00
bohd4nx 5c472b68a2 docs: update changelog for version 2026.3.1 with new features, changes, fixes, and removals 2026-05-29 01:53:57 +03:00
bohd4nx b2d06a7f22 feat: add support for Python 3.13 and 3.14 in classifiers; update mypy overrides 2026-05-29 01:50:24 +03:00
bohd4nx 34ee31e299 style: format YAML files for consistency and remove unused environment variables 2026-05-29 01:43:05 +03:00
bohd4nx c1ff78c484 docs: clarify seed parameter description in FragmentClient 2026-05-29 01:37:49 +03:00
bohd4nx f8ad801e0c feat: enhance documentation with contributing guidelines and security policy
refactor: update README configuration parameters and add error handling section
refactor: streamline tonapi imports and add WalletVersion to __all__
chore: update project metadata in pyproject.toml
2026-05-29 01:27:29 +03:00
bohd4nx 6cb6e3fe05 refactor: remove unused transfer functions and streamline imports in wallet and models 2026-05-29 01:17:57 +03:00
bohd4nx d4e3d3491b chore: bump version to 2026.3.1 in pyproject.toml 2026-05-29 01:04:51 +03:00
bohd4nx 4a7131acdb refactor: update patch targets in test cases to use domains.base module 2026-05-29 01:04:51 +03:00
bohd4nx 546bcb337c feat: implement logging throughout the application and enhance error handling 2026-05-29 01:04:51 +03:00
bohd4nx e9dd706fa6 refactor: add documentation link to README and pyproject.toml 2026-05-21 17:16:19 +03:00
bohd4nx f8ac7f9594 refactor: streamline rookiepy import and enhance module loading 2026-05-21 00:29:52 +03:00
bohd4nx 3f5e0bc03f chore: update version to 2026.3.0 and enhance changelog with recent changes 2026-05-21 00:23:25 +03:00
bohd4nx 92384b96f0 refactor: remove unused import of get_cookies_from_browser across multiple example scripts 2026-05-21 00:21:41 +03:00
bohd4nx 516e69f820 refactor: improve exception messages and enhance CookieResult representation 2026-05-20 23:50:03 +03:00
bohd4nx 2a6c5ef1f9 refactor: reorganize tonapi module and update imports for account management 2026-05-20 23:45:27 +03:00
bohd4nx 01a5befd87 Refactor wallet transaction handling and introduce tonapi module
- Removed wallet transaction and transfer logic from the wallet domain.
- Introduced a new tonapi module to handle transactions and balance checks.
- Updated tests to reflect the new structure and ensure functionality remains intact.
- Added functionality for sending TON and USDT transfers through the tonapi module.
- Improved error handling and validation for payment balances.
- Cleaned up and organized imports across the codebase.
2026-05-20 23:42:24 +03:00
bohd4nx ae164d4fe7 feat: Implement marketplace and purchases services
- Added MarketplaceService for searching usernames, numbers, and gifts.
- Introduced PurchasesService for purchasing stars and premium subscriptions.
- Created WalletService for wallet operations including balance checks and top-ups.
- Developed transaction handling for TON and USDT transfers.
- Added models for payments, marketplace results, and wallet information.
- Implemented error handling for various operations including wallet and transaction errors.
- Established domain structure for purchases, marketplace, and wallet functionalities.
2026-05-20 23:33:01 +03:00
bohd4nx e7dc0e051e feat: update README and pyproject.toml to clarify browser dependency installation 2026-05-20 22:55:18 +03:00
bohd4nx 363e0719ab Bump version to 2026.2.3 and refactor wallet utilities
- Updated version in pyproject.toml to 2026.2.3.
- Refactored wallet utilities:
  - Moved `clean_decode` and `process_transaction` to `transaction.py`.
  - Created `balance.py` for balance-related functions.
  - Created `info.py` for wallet information retrieval.
  - Created `transfer.py` for sending TON and USDT transfers.
- Updated tests to reflect new module structure and imports.
- Added new API utility functions for handling Fragment API requests.
2026-05-12 02:01:15 +03:00
bohd4nx b67415bcc2 chore: remove redundant reference to examples directory in README 2026-05-11 13:21:18 +03:00
bohd4nx 5df95bea29 chore: update version to 2026.2.2 and modify changelog for release 2026-05-11 13:19:56 +03:00
bohd4nx 7412448f9c feat: enhance payment processing with minimum balance checks and update documentation 2026-05-11 13:11:28 +03:00
bohd4nx 6dd9dcb5d4 feat: update wallet handling to support separate TON and USDT balances
- Refactor `get_wallet()` to return `ton_balance` and `usdt_balance` in `WalletInfo`.
- Update transaction processing to validate balances for both TON and USDT payment methods.
- Introduce `parse_required_payment_amount` utility to extract payment amounts from responses.
- Modify tests to cover new balance checks and payment method handling.
- Upgrade GitHub Actions artifact upload action to v7.
- Enhance documentation and examples to reflect changes in wallet balance handling.
2026-05-11 13:02:00 +03:00
bohd4nx 311222d478 feat: add payment method options for purchases and giveaways, enhance documentation and examples 2026-05-11 12:12:07 +03:00
bohd4nx 72918a6dd6 feat: add payment method support for premium and stars purchases, including validation and tests 2026-05-11 11:57:52 +03:00
bohd4nx c1ae5f0f28 fix: update download-artifact action version to v8 for consistency 2026-05-03 19:20:30 +03:00
bohd4nx 4873d6d6dd fix: update PyPI publish action to use the latest release version
Co-authored-by: Copilot <copilot@github.com>
2026-05-03 19:19:54 +03:00
bohd4nx d5dfed5f1a fix: update CI and publish workflows to use consistent action versions and improve error handling in API requests
Co-authored-by: Copilot <copilot@github.com>
2026-05-03 19:10:11 +03:00
bohd4nx 7923dff8b2 fix: remove unnecessary 'method' key from API request data and improve error handling for expired cookies and non-200 HTTP responses 2026-04-21 20:06:11 +03:00
bohd4nx 8c7423a6ab fix: update Python version in CI workflow and refine project metadata for clarity 2026-04-18 21:11:14 +03:00
bohd4nx 8f3e9f4bd8 chore: update action versions in CI workflow and adjust linter ignores for compatibility 2026-04-18 21:05:12 +03:00
bohd4nx fca60135a6 refactor: update type hints for better clarity and consistency across the codebase 2026-04-18 21:02:53 +03:00
bohd4nx d66602b646 fix: replace Miniconda setup with Python setup in CI workflow for consistency 2026-04-14 02:03:22 +03:00
bohd4nx 333b6f45ce fix: update Miniconda setup action version and adjust activation settings 2026-04-14 02:01:16 +03:00
bohd4nx 415d9d9a9c docs: update examples and changelog to reflect CookieResult usage and Python version changes 2026-04-14 01:57:12 +03:00
bohd4nx 6a5e007a1b refactor: update import statements and patching methods in test files for better modularity 2026-04-14 01:45:04 +03:00
bohd4nx d81b83ac91 fix: reorder import statements in conftest.py for better organization 2026-04-14 01:37:52 +03:00
bohd4nx 6b6ca37f10 fix: update Python version requirements to support 3.10 and adjust CI workflow 2026-04-14 01:34:27 +03:00
bohd4nx 028984155b fix: update Python version matrix in CI workflow to include 3.10, 3.11, and 3.12 2026-04-14 01:30:52 +03:00
bohd4nx 391d15221b feat: enhance cookie handling by introducing CookieResult and updating get_cookies_from_browser to return structured results 2026-04-14 01:25:25 +03:00
bohd4nx 269551da2f feat: Update CI workflows, add changelog extraction, and enhance README with cookie retrieval methods 2026-04-14 01:13:45 +03:00
bohd4nx ebd45d2991 Add unit tests for FragmentClient methods
- Implement tests for Stars methods: purchase_stars and giveaway_stars, including validation and success scenarios.
- Add tests for Premium methods: purchase_premium and giveaway_premium, covering validation and success cases.
- Create tests for topup_ton functionality, validating input and mocking successful operations.
- Introduce wallet tests to verify get_wallet() functionality, ensuring correct wallet info retrieval.
- Add tests for FragmentClient.call() method, validating API response handling and data merging.
- Implement tests for anonymous number methods, including login codes and session management.
- Create recharge_ads tests for self-service Telegram Ads recharge, validating input and success scenarios.
- Add username search tests for the Fragment marketplace, ensuring correct result parsing and parameter forwarding.
- Implement number search tests for the Fragment marketplace, validating result parsing and parameter handling.
- Create gift search tests for the Fragment gifts marketplace, ensuring correct result parsing and parameter forwarding.
- Add cookie extraction tests for get_cookies_from_browser(), validating successful extraction and error handling.
2026-04-14 00:22:48 +03:00
bohd4nx 733d138fcc feat: Enhance cookie handling and browser support
- Added new exceptions in `exceptions.py` for unsupported browsers and cookie read failures.
- Introduced `get_cookies_from_browser` function in `cookies.py` to extract session cookies from various browsers.
- Modified tests to cover new cookie extraction functionality, including success and failure cases.
- Refactored existing tests to use `patch.object` for mocking client calls.
- Updated dependencies in `pyproject.toml` to include `rookiepy` for cookie extraction.
2026-04-14 00:12:22 +03:00
bohd4nx 503baa9a94 ci: update test job to use matrix strategy for multiple Python versions and switch to conda setup
fix: specify type for headers parameter in execute_transaction_request
fix: specify type for transaction_data parameter in process_transaction
deps: update tonutils dependency version to 2.0.5
2026-04-07 00:52:39 +03:00
bohd4nx d4ac44f698 style: remove unnecessary blank line in test_hash.py 2026-03-25 18:35:09 +02:00
bohd4nx 3aedd619dc Merge remote-tracking branch 'origin/master' into dev 2026-03-25 18:34:30 +02:00
bohd4nx bae2e1b400 tests: add section comments to all test files 2026-03-25 18:33:47 +02:00
bohd4nx 0a71e6c508 chore: merge dev into master for v2026.1.0 2026-03-25 18:27:21 +02:00
bohd4nx fb0b82e429 chore: update version to 2026.1.0 in pyproject.toml and finalize changelog 2026-03-25 18:23:21 +02:00
bohd4nx 3c60bf5e5a feat: add unit tests for search_usernames, search_numbers, and search_gifts methods in FragmentClient 2026-03-25 18:22:25 +02:00
bohd4nx da2eb37a10 chore: update examples and README for improved clarity and consistency; streamline print statements and descriptions 2026-03-25 18:18:40 +02:00
bohd4nx 3156160778 Refactor examples: reorganize and update scripts for managing Telegram services
- Removed outdated example scripts for managing anonymous numbers, making raw API calls, fetching wallet info, and various giveaways.
- Introduced new scripts for searching gifts, numbers, and usernames in the Fragment marketplace.
- Added examples for recharging Telegram Ads accounts and managing user balances with TON.
- Updated example scripts to improve clarity and maintainability, ensuring they align with the latest API changes.
2026-03-25 18:09:41 +02:00
bohd4nx 8163ed1a9d feat: Add gift and number search functionality to FragmentClient
- Implemented `search_gifts` method for searching gifts in the Fragment marketplace.
- Implemented `search_numbers` method for searching anonymous Telegram numbers.
- Added new example scripts for searching gifts, numbers, and usernames.
- Updated HTML parsing logic to handle gift items and number listings.
- Added unit tests for the new search methods and updated existing tests for usernames and numbers.
- Removed obsolete test file for auction searches.
2026-03-25 18:01:22 +02:00
bohd4nx f37a4ab985 feat: implement search_auctions method for Fragment marketplace; add AuctionsResult type; enhance topup_ton example and tests 2026-03-25 17:40:02 +02:00
bohd4nx dfe7bb4140 feat: implement recharge_ads method for Telegram Ads account funding; add associated result type and example; enhance tests for validation and success cases 2026-03-25 00:57:31 +02:00
bohd4nx 327ca35190 feat: update README and pyproject description; enhance test docstrings for clarity and consistency 2026-03-21 17:51:19 +02:00
bohd4nx 6e4d4da740 feat: add anonymous number management methods; implement get_login_code, toggle_login_codes, and terminate_sessions with associated results and error handling 2026-03-21 17:43:58 +02:00
bohd4nx ab8d463d54 feat: add unit tests for balance checks, giveaway stars, and premium methods; validate float inputs for winners and amounts 2026-03-21 17:18:56 +02:00
bohd4nx b55cd00205 feat: update changelog and examples; unify result fields and improve error messages 2026-03-21 17:12:26 +02:00
bohd4nx 25d70a77ed feat: implement client.call() method for raw Fragment API requests; add example and unit tests 2026-03-21 16:45:52 +02:00
bohd4nx c7b6c6c933 feat: add comprehensive unit tests for FragmentClient methods; include shared test constants 2026-03-21 16:30:44 +02:00
bohd4nx 2d1a119768 feat: update purchase and giveaway methods; improve header management and add example scripts 2026-03-21 16:16:14 +02:00
bohd4nx 902b692dcb Refactor methods for Telegram Premium and Stars giveaways
- Added new methods for handling giveaways: `giveaway_premium` and `giveaway_stars`.
- Refactored existing purchase methods into separate files: `purchase_premium.py` and `purchase_stars.py`.
- Removed old premium and stars methods from the codebase.
- Updated `__init__.py` to include new giveaway methods in the public API.
- Introduced new result types for giveaways: `PremiumGiveawayResult` and `StarsGiveawayResult`.
- Updated constants for new giveaway pages.
- Enhanced error handling for configuration and user validation in giveaway methods.
- Updated tests to cover new functionality and refactored existing tests to match new method locations.
2026-03-21 16:05:38 +02:00
bohd4nx 7cf5ab4500 chore: release 2026.0.2 2026-03-20 20:58:06 +02:00
bohd4nx 6cae351607 docs: update installation instructions to include unreleased changes from the dev branch 2026-03-20 20:58:06 +02:00
bohd4nx 13f5aaf555 chore: update setup-uv action to v7.6.0 in CI and publish workflows; fix image source in README 2026-03-20 20:58:06 +02:00
bohd4nx 9660e4858f chore: streamline CI linting steps and update changelog with recent changes 2026-03-20 20:58:06 +02:00
bohd4nx 75cbe52507 fix: cast cookies to dict for type safety and improve error handling in FragmentClient 2026-03-20 20:58:06 +02:00
bohd4nx 4e03d2e2f6 refactor: harden client, clean tests, and fix timeouts
- Security & validation: tighten hash regex, add HTTP timeouts to all
  requests, pass client.timeout through get_fragment_hash and AsyncClient
- Constants: move all constants to types/constants.py; remove re-exports
  from types/__init__.py; add DEFAULT_TIMEOUT, REQUIRED_COOKIE_KEYS
- FragmentClient: add timeout param (default 30 s); async-context-manager
  support; remove WALLET_CLASSES from public API
- Exceptions: remove dead INVALID_USERNAME constant (Fragment validates
  server-side); keep full hierarchy intact
- Tests: add 006_test_methods_mock.py (6 mock tests for all 3 methods);
  DRY-refactor 004_test_balance.py (_patch_wallet context manager);
  clean up 005_test_methods.py (remove fragile network test, rename tests)
- Examples: switch all 4 examples to async-with; align error messages;
  replace %-format with f-strings
- README: rewrite usage section with single comprehensive async-with
  example covering all 3 methods and full exception hierarchy
- CI: add mypy step to lint job; add pytest-mock and mypy to dev deps;
  set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 on all jobs; fix COOKIES_JSON
  to job-level env var
2026-03-20 20:58:06 +02:00
bohd4nx 0af5ce5935 chore: release 2026.0.2 2026-03-20 20:57:37 +02:00
bohd4nx b07b4670fb docs: update installation instructions to include unreleased changes from the dev branch 2026-03-20 20:45:35 +02:00
bohd4nx 581886938b chore: update setup-uv action to v7.6.0 in CI and publish workflows; fix image source in README 2026-03-20 20:36:59 +02:00
bohd4nx 949796df17 chore: streamline CI linting steps and update changelog with recent changes 2026-03-20 20:25:35 +02:00
bohd4nx 75ee76b60e fix: cast cookies to dict for type safety and improve error handling in FragmentClient 2026-03-20 20:22:33 +02:00
bohd4nx 3e14a01c92 refactor: harden client, clean tests, and fix timeouts
- Security & validation: tighten hash regex, add HTTP timeouts to all
  requests, pass client.timeout through get_fragment_hash and AsyncClient
- Constants: move all constants to types/constants.py; remove re-exports
  from types/__init__.py; add DEFAULT_TIMEOUT, REQUIRED_COOKIE_KEYS
- FragmentClient: add timeout param (default 30 s); async-context-manager
  support; remove WALLET_CLASSES from public API
- Exceptions: remove dead INVALID_USERNAME constant (Fragment validates
  server-side); keep full hierarchy intact
- Tests: add 006_test_methods_mock.py (6 mock tests for all 3 methods);
  DRY-refactor 004_test_balance.py (_patch_wallet context manager);
  clean up 005_test_methods.py (remove fragile network test, rename tests)
- Examples: switch all 4 examples to async-with; align error messages;
  replace %-format with f-strings
- README: rewrite usage section with single comprehensive async-with
  example covering all 3 methods and full exception hierarchy
- CI: add mypy step to lint job; add pytest-mock and mypy to dev deps;
  set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 on all jobs; fix COOKIES_JSON
  to job-level env var
2026-03-20 20:16:43 +02:00
Bohdan 👾 2184dc7d98 Merge pull request #7 from bohd4nx/feat/pypi-package
Feat/pypi package
2026-03-20 01:23:15 +02:00
bohd4nx c5edfad06f feat: add context manager support to FragmentClient; update README and tests 2026-03-16 19:21:43 +02:00
bohd4nx 20b73444ab feat: rename gift methods to purchase for clarity; update examples and changelog 2026-03-16 19:13:07 +02:00
bohd4nx b726a2274c feat: update bug report template to reflect terminology change from "gift" to "purchase" for Stars 2026-03-16 19:00:55 +02:00
bohd4nx 8d58ed890d feat: add examples for purchasing Telegram Premium and Stars 2026-03-16 18:41:27 +02:00
bohd4nx f5d2e8490a feat: restructure GitHub workflows; add CI workflow, update publish process, and remove obsolete test and lint workflows 2026-03-16 18:22:58 +02:00
bohd4nx 520153dfb1 feat: update terminology from "gift" to "purchase" for Telegram Premium and Stars; enhance README and example scripts 2026-03-16 01:14:12 +02:00
bohd4nx 67f8a882c2 feat: add MIT License to the project 2026-03-15 23:05:22 +02:00
bohd4nx 041081b919 feat: add API key validation and corresponding error handling; update tests for validation 2026-03-15 23:01:37 +02:00
bohd4nx f4a96bb01a fix: restrict workflow triggers to the master branch for linting and publishing 2026-03-15 22:45:00 +02:00
bohd4nx d8f0240807 feat: add wallet management features and enhance error handling; update tests and examples 2026-03-15 22:41:52 +02:00
bohd4nx 8135a9da7e Refactor FragmentAPI to pyfragment
- Renamed the package from `fragmentapi` to `pyfragment` across all modules and tests.
- Removed the old wallet utility functions and replaced them with new implementations.
- Updated the `pyproject.toml` to reflect the new package name and repository links.
- Adjusted all import statements in tests to use the new package name.
- Implemented new methods for gifting Telegram Premium, Stars, and topping up TON balance.
- Added exception handling for various error scenarios in the API interactions.
- Created new utility functions for handling HTTP requests and decoding payloads.
- Established a clear structure for types and constants used throughout the library.
2026-03-15 22:07:01 +02:00
bohd4nx 7f269d9a87 refactor: rename error classes for consistency and clarity; update related code and tests 2026-03-15 22:03:32 +02:00
bohd4nx 9a090e3dbc feat: implement CI/CD workflows for testing, linting, and publishing to PyPI 2026-03-15 21:45:24 +02:00
bohd4nx d2d046a2b9 refactor: restructure as installable PyPI package
- Rename app/ → fragmentapi/ for proper package naming
- Add FragmentClient class with gift_premium, gift_stars, topup_ton methods
- Restructure core/ → types/ (exceptions, results, constants)
- Merge utils/hash.py into utils/client.py
- Replace _version.py with importlib.metadata
- Add input validation with min/max bounds
- Add unit tests: decode, client init/cookies
- Rename 002_test_hash → 003_test_hash
- Clean up pyproject.toml: pin deps, production classifiers
2026-03-15 21:03:03 +02:00
Bohdan 👾 116ed24db9 Merge pull request #5 from bohd4nx/refactor/tonutils-v2
Refactor/tonutils v2
2026-03-14 01:35:28 +02:00
bohd4nx 4e017cb7a1 refactor: update methods to include show_sender parameter and clean up tests 2026-03-14 01:34:51 +02:00
bohd4nx 6c869428aa refactor: streamline error messages and improve code formatting for consistency 2026-03-09 03:32:45 +02:00
Bohdan 👾 0c02062291 Merge pull request #3 from bohd4nx/refactor/tonutils-v2
Refactor/tonutils v2
2026-03-08 04:37:31 +02:00
bohd4nx c3de8a8ca3 refactor: enhance transaction logging for premium, stars, and TON topup methods 2026-03-08 04:37:07 +02:00
bohd4nx 38a4619e42 fix: pass cookies to AsyncClient instead of per-request
Fixes httpx DeprecationWarning: Setting per-request cookies is deprecated.
Cookies are now set on the AsyncClient instance and removed from all
individual post() calls and internal function signatures.
2026-03-08 04:26:45 +02:00
bohd4nx 8cf57b58b7 refactor: enhance test workflow and integration tests with cookies handling and configuration checks 2026-03-08 03:58:07 +02:00
bohd4nx 2cdc102a74 refactor: reorganize imports across multiple files for consistency and clarity 2026-03-05 06:36:27 +02:00
Bohdan 👾 da56afc75f Merge pull request #2 from bohd4nx/refactor/tonutils-v2
Refactor/tonutils v2
2026-03-05 06:27:52 +02:00
bohd4nx 7f3c3ea1c3 refactor: update API key source in .env.example and enhance README with badges and feature descriptions
feat: improve logging levels for HTTP clients and add detailed logging in premium and stars purchase methods
chore: remove unused transaction utility and adjust wallet processing logic
test: add end-to-end integration test for buying Stars
2026-03-05 06:26:49 +02:00
bohd4nx efcdda6b74 refactor: migrate to tonutils 2.0.0 and restructure codebase
- Migrate from tonutils 0.5.x to 2.0.0 API:
  - ToncenterClient → TonapiClient with NetworkGlobalID
  - tonutils.client → tonutils.clients
  - tonutils.wallet → tonutils.contracts.wallet
  - pub_key.as_hex property, wallet.balance nanotons
  - async with client context manager

- Centralize TON client init into initialize_ton_client() in wallet.py
- Move process_transaction logic into wallet.py, transaction.py re-exports
- Add WALLET_VERSION config (V4R2/V5R1, default V5R1)
- Add WALLET_CLASSES and SUPPORTED_WALLET_VERSIONS to constants.py
- Restore balance check before broadcasting
- Wait for seqno confirmation after transfer (120×2s) to prevent duplicate seqno

- Fix Fragment hash fetching: use browser navigation headers
- Remove accept-encoding from BASE_HEADERS (httpx handles decompression)
- Add cookies.example.json template
- Add cookies.json to .gitignore
2026-03-05 05:47:27 +02:00
bohd4nx b9c02646be chore: remove cookies.json from tracking, add to .gitignore 2026-03-05 05:05:24 +02:00
bohd4nx 2d7860682d fix: tonutils 2.0.0 API — as_hex property, wallet.refresh(), balance as int nanotons 2026-03-05 05:04:17 +02:00
bohd4nx 64f8058c60 fix: don't sys.exit in config at import time, support CI env vars 2026-03-05 04:23:34 +02:00
bohd4nx f57f8271c3 test: add pytest test files (were missing from previous commit) 2026-03-05 04:18:55 +02:00
bohd4nx 5dc3cddf1a refactor: replace manual base64 decoder with pytoniq_core Cell parser
- clean_decode now uses Cell.one_from_boc + load_snake_string
- no more regex/string hacks, native BOC parsing
- added full transaction_data debug log in process_transaction
2026-03-05 04:16:49 +02:00
bohd4nx d2faa27c5c ci: update actions to checkout@v6, setup-python@v6 2026-03-05 04:12:09 +02:00
bohd4nx 91d33a0972 ci: add dependabot and pytest workflow 2026-03-05 04:10:57 +02:00
bohd4nx e3706f01bf refactor: migrate to tonutils v2, pytest, pyproject.toml
- tonutils 2.0.0: TonapiClient → ToncenterV3Client, wallet.transfer()
- classes → plain async functions across all utils and methods
- core/: constants.py, cookies.py, exceptions.py extracted
- DEVICE constant in constants.py (single source of truth)
- account/device serialised with json.dumps() in all tx payloads
- tests: unittest → pytest, conftest.py fixtures, 001/002 naming
- pyproject.toml: pytest + ruff config
- min balance check: 0.056 TON
2026-03-05 04:08:35 +02:00
bohd4nx b1eae1dcb7 feat: Refactor cookie handling and hash retrieval; update environment configuration and improve logging levels 2026-01-19 19:02:28 +02:00
bohd4nx d299a1f804 feat: Refactor configuration handling and enhance logging setup; update imports and improve error handling in API methods 2026-01-18 06:41:08 +02:00
bohd4nx 76473993e2 feat: Enhance Fragment API with improved error handling and clean_decode utility; (#1) update version and dependencies 2025-11-24 19:52:22 +02:00
bohd4nx 49cf8843bc fix: Update README and SVG logo, correct import paths in utils 2025-11-03 06:01:56 +08:00
bohd4nx 8adfaf4ad2 feat: Implement ApiClient and WalletLinker classes for transaction handling 2025-11-03 06:01:53 +08:00
bohd4nx 760c853acd feat: Add Fragment API for TON topups, Telegram Premium purchases, and Stars transactions
- Implemented Fragment API with methods for TON topups, Premium gifts, and Stars purchases.
- Created configuration management using environment variables.
- Added detailed README documentation for installation, configuration, and usage.
- Developed utility classes for API client, wallet linking, and transaction processing.
- Included example usage in main.py for easy demonstration of API functionalities.
- Updated requirements.txt with necessary dependencies.
2025-11-03 05:58:34 +08:00
120 changed files with 6508 additions and 1237 deletions
-5
View File
@@ -1,5 +0,0 @@
root: ./docs
structure:
readme: README.md
summary: SUMMARY.md
+101
View File
@@ -0,0 +1,101 @@
name: Bug report
description: Report an issue or unexpected behavior in pyfragment.
labels:
- bug
body:
- type: checkboxes
attributes:
label: Checklist
options:
- label: I am sure the error is coming from pyfragment code
required: true
- label: I have searched the issue tracker for similar bug reports, including closed ones
required: true
- type: markdown
attributes:
value: |
## Context
Please provide as much detail as possible to help us reproduce and fix the issue.
- type: input
attributes:
label: Operating system
placeholder: e.g. Ubuntu 22.04 / macOS 14 / Windows 11
validations:
required: true
- type: input
attributes:
label: Python version
description: Run `python --version` inside your virtualenv
placeholder: e.g. 3.12.3
validations:
required: true
- type: input
attributes:
label: pyfragment version
description: Run `pip show pyfragment` inside your virtualenv
placeholder: e.g. 2026.1.0
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: Describe what you expected to happen.
placeholder: e.g. Stars should be purchased and StarsResult returned.
validations:
required: true
- type: textarea
attributes:
label: Current behavior
description: Describe what is actually happening.
placeholder: e.g. ParseError is raised with status 400.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Minimal steps that reproduce the issue.
placeholder: |
1. Create FragmentClient with valid credentials
2. Call purchase_stars("@username", amount=100)
3. See error
validations:
required: true
- type: textarea
attributes:
label: Code example
description: Provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) if applicable.
placeholder: |
import asyncio
from pyfragment import FragmentClient
async def main():
client = FragmentClient(...)
result = await client.purchase_stars("@username", amount=100)
asyncio.run(main())
render: python
- type: textarea
attributes:
label: Traceback / logs
description: Paste the full traceback or relevant logs.
placeholder: |
Traceback (most recent call last):
File "main.py", line 7, in main
...
pyfragment.types.ParseError: ...
render: sh
- type: textarea
attributes:
label: Additional information
description: Anything else that might help us diagnose the problem.
placeholder: e.g. Only happens with V5R1 wallet version.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Ask a question or start a discussion
url: https://github.com/bohd4nx/pyfragment/discussions
about: General questions, ideas, and community help go here — not in the issue tracker.
+51
View File
@@ -0,0 +1,51 @@
name: Feature request
description: Suggest an improvement or new feature for pyfragment.
labels:
- enhancement
body:
- type: dropdown
attributes:
label: pyfragment version
description: Which version are you running?
options:
- latest
- older
- n/a
validations:
required: true
- type: textarea
attributes:
label: Problem
description: Is your request related to a specific problem? Describe it.
placeholder: e.g. There is no way to check my current TON balance before sending.
validations:
required: true
- type: textarea
attributes:
label: Proposed solution
description: Describe what you would like to see added or changed.
placeholder: e.g. Add a get_balance() method to FragmentClient.
validations:
required: true
- type: textarea
attributes:
label: Alternatives considered
description: Any workarounds or alternative approaches you have thought of.
placeholder: e.g. I manually call the Fragment API, but it's not ergonomic.
- type: textarea
attributes:
label: Code example
description: A short example demonstrating the desired API, if applicable.
placeholder: |
balance = await client.get_balance()
print(balance.ton)
render: python
- type: textarea
attributes:
label: Additional information
description: Any other context, screenshots, or references.
+34
View File
@@ -0,0 +1,34 @@
# Description
Please include a summary of the change and which issue is fixed.
Include relevant motivation and context.
Fixes # (issue)
## Type of change
- [ ] Documentation (typos, examples, or any docs update)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] This change requires a documentation update
## How has this been tested?
Describe the tests you ran to verify the change and list any relevant details.
- [ ] Existing tests pass (`pytest`)
- [ ] New tests added for this change
**Test configuration:**
* OS:
* Python version:
* pyfragment version:
## Checklist
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have updated documentation where necessary
- [ ] I have added tests that prove my fix or feature works
- [ ] All new and existing tests pass locally
+19
View File
@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
labels:
- "dependencies"
+47
View File
@@ -0,0 +1,47 @@
name: CI
on:
push:
branches: ["**"]
pull_request:
branches: ["**"]
jobs:
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-python@v7.0.0
with:
python-version: "3.11"
cache: pip
- run: pip install ".[dev]"
- run: ruff check . && ruff format --check . && mypy pyfragment --explicit-package-bases
test:
name: Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-python@v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install package and dev dependencies
run: pip install ".[dev]"
- name: Run tests
run: pytest
-23
View File
@@ -1,23 +0,0 @@
name: Docs Branch Check
on:
push:
branches: [docs]
pull_request:
branches: [docs]
jobs:
docs-tree:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
- name: Ensure docs structure exists
run: |
test -f docs/README.md
test -f docs/SUMMARY.md
test -d docs/getting-started
test -d docs/client
test -d docs/reference
test -d docs/advanced
+107
View File
@@ -0,0 +1,107 @@
name: Publish
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [master]
jobs:
version-check:
name: Version Check
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.value }}
is-new: ${{ steps.tag.outputs.is-new }}
steps:
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
- name: Read version
id: version
run: |
value=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
echo "value=$value" >> $GITHUB_OUTPUT
- name: Check tag
id: tag
run: |
if git ls-remote --tags origin "refs/tags/v${{ steps.version.outputs.value }}" | grep -q .; then
echo "is-new=false" >> $GITHUB_OUTPUT
else
echo "is-new=true" >> $GITHUB_OUTPUT
fi
build:
name: Build
needs: version-check
if: needs.version-check.outputs.is-new == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-python@v7.0.0
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v9.0.0
- run: uv build
- uses: actions/upload-artifact@v7.0.1
with:
name: dist
path: dist/*
publish:
name: Publish to PyPI
needs: [version-check, build]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/pyfragment/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8.0.1
with:
name: dist
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
release:
name: GitHub Release
needs: [version-check, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/download-artifact@v8.0.1
with:
name: dist
path: dist
- name: Extract latest changelog entry
id: changelog
run: |
body=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
echo "body<<EOF" >> $GITHUB_OUTPUT
echo "$body" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- uses: softprops/action-gh-release@v3.0.2
with:
tag_name: v${{ needs.version-check.outputs.version }}
name: v${{ needs.version-check.outputs.version }}
files: dist/*
body: ${{ steps.changelog.outputs.body }}
make_latest: true
+42
View File
@@ -1 +1,43 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
.venv/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
*.log
#docs
*-docs/
# OS
.DS_Store
Thumbs.db
uv.lock
+283
View File
@@ -0,0 +1,283 @@
# Changelog
All notable changes to pyfragment are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MICRO`).
---
## [2026.3.2] — 2026-06-16
### Added
- Added `ApiProvider` enum with `TONAPI` (tonconsole.com, default) and `TONCENTER` (t.me/toncenter) values.
- Added `api_provider` parameter to `FragmentClient` — select the blockchain API provider at init time (`"tonapi"` or `"toncenter"`).
- Both providers accept `api_key` with the same interface; the correct `tonutils` client is selected automatically.
- New `AlreadySubscribedError` exception for Premium purchase flows when Fragment returns: `This account is already subscribed to Telegram Premium.`
- New `UserNotFoundError.NOT_A_USER` message for when Fragment returns: `Please enter a username assigned to a user.` (e.g. when the username belongs to a channel or bot).
- Added `WalletVersion.HighloadV2` and `WalletVersion.HighloadV3R1` to `WalletVersion`
### Changed
- Updated purchase and giveaway flow state nonces (`dh`) to use nonce-like dynamic values with a wider integer range.
- Stars and Premium giveaway flows now include explicit price update steps before init requests:
- `updateStarsGiveawayPrices`
- `updatePremiumGiveawayPrices`
- 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
### Added
- Python 3.13 and 3.14 are now officially supported and included in the CI test matrix and PyPI classifiers.
- `WalletVersion` is now exported from the top-level `pyfragment` package.
### Changed
- `process_transaction` (internal) refactored into focused subfunctions: `_extract_message`, `_check_payment_balances`, `_broadcast_with_retry`.
- `raw_api_call()` moved from `FragmentClient` into `pyfragment.domains.base` and exposed as a standalone helper.
- `tonapi` domain internal helpers removed from public `__init__.py` exports; only `TonapiService` is exported.
- README rewritten with badges, structured sections, and complete usage examples.
- Added `CONTRIBUTING.md` and `SECURITY.md`.
### Fixed
- CI: `mypy` now runs with `--explicit-package-bases` to avoid false-positive import errors.
- CI: `pip` dependency cache enabled to speed up workflow runs.
- CI: `warn_unused_ignores` suppressed for `pyfragment.core.cookies` to handle the optional `rookiepy` dependency correctly across environments where the package may or may not be installed.
- Publish workflow now uses `generate_release_notes: true` instead of manual changelog extraction.
### Removed
- `tonapi/transfer.py` and associated `TonTransferResult` / `UsdtTransferResult` models (internal, unused).
---
## [2026.3.0] — 2026-05-21
### Changed
- Internal architecture reorganized around explicit domain packages:
- TON account and balance helpers are now unified under `pyfragment.domains.tonapi.account`
- service wrappers and operation modules are aligned by domain (`ads`, `purchases`, `giveaways`, `anonymous_numbers`, `marketplace`, `tonapi`)
- Package exports were cleaned up for domain and model packages (`__init__.py`) to provide clearer public symbols.
- Examples and system tests were updated to follow current public import paths and project structure.
### Fixed
- `get_cookies_from_browser()` is now patch-friendly in tests (`pyfragment.core.cookies.rookiepy` can be mocked reliably).
- Anonymous number `NOT_OWNED` error message wording was adjusted for test and backward-compatibility with existing matchers.
## [2026.2.3] — 2026-05-12
### Fixed
- Fixed USDT payment flow: the USDT balance check now correctly targets the wallet linked to the Fragment account (`transaction["from"]`), not the signing seed wallet. These are two distinct addresses — the seed wallet only signs the transaction and covers TON gas fees, while USDT is withdrawn from the Fragment-linked wallet.
- Fixed `clean_decode()` incorrectly treating binary TON cell payloads (e.g. jetton transfer messages with non-zero op codes) as text comments. Only cells with op code `0x00000000` are now decoded as snake-encoded UTF-8 strings; all other op codes return the raw `Cell` as-is.
- Restored and correctly wired USDT balance validation so `WalletError` is raised before broadcasting when the Fragment-linked wallet has insufficient USDT.
### Note
- USDT (`usdt_ton`) payments require USDT to be held in the TON wallet that is linked to your Fragment account profile. The seed wallet configured in `FragmentClient` is only used to sign transactions and pay TON network fees.
---
## [2026.2.2] — 2026-05-11
### Added
- `payment_method` option (`"ton"` / `"usdt_ton"`) for:
- `purchase_stars()`
- `purchase_premium()`
- `giveaway_stars()`
- `giveaway_premium()`
### Changed
- Added runtime validation for `payment_method` via `SUPPORTED_PAYMENT_METHODS` and `ConfigurationError.INVALID_PAYMENT_METHOD`
- Updated method docstrings to explicitly document recipient/channel formats:
- `@username` / `username` / `https://t.me/username`
- `get_wallet()` now returns balances as separate fields: `ton_balance` and `usdt_balance`
- Wallet/system test output now prints TON and USDT balances on separate lines
- Balance checks are now method-aware with explicit thresholds:
- `ton`: minimum TON balance threshold via `MIN_TON_BALANCE` (based on current 50 Stars purchase amount)
- `usdt_ton`: minimum USDT balance threshold via `MIN_USDT_BALANCE` (based on current 50 Stars purchase amount)
### Tests
- Extended stars and premium test suites to cover:
- invalid payment method
- payment method propagation to `init*Request` payloads
- accepted query formats (`@`, plain username, `t.me` link)
- Extended wallet tests to verify separate TON/USDT balance values in `WalletInfo`
### Documentation
- Simplified `README` usage example
## [2026.2.1] — 2026-05-03
### Fixed
- Fragment API 429 responses are now retried automatically (up to 3 attempts) with exponential backoff and jitter in `fragment_request`
- Retry delays in TON transaction broadcasting now include jitter to reduce contention under concurrent calls
- Improved handling of non-200 HTTP responses in `get_fragment_hash`
- Removed unnecessary `method` key leaking into certain API request payloads
### Changed
- Type hints refined across the codebase for better clarity and `mypy` strict compliance
---
## [2026.2.0] — 2026-04-14
### Added
- `get_cookies_from_browser(browser)` — extract Fragment session cookies directly from an installed browser (Chrome, Firefox, Edge, Brave, Arc, Opera, Safari, and more); no browser extension or manual copy-paste required
```python
from pyfragment import get_cookies_from_browser
result = get_cookies_from_browser("chrome") # or "firefox", "edge", "brave", ...
client = FragmentClient(seed="...", api_key="...", cookies=result.cookies)
print(result.expires) # ISO 8601 expiry of stel_ssid, or None for session cookies
```
- `CookieResult` — return type of `get_cookies_from_browser()`; exposes `.cookies` (`dict[str, str]`) and `.expires` (ISO 8601 string or `None`)
### Changed
- `DEVICE` Tonkeeper fingerprint updated: `appVersion` → `26.04.0`
- `tonutils` upgraded to **2.1.0**
- Minimum Python version lowered to **3.10** (previously 3.12)
---
## [2026.1.0] — 2026-03-25
### Added
**Giveaways**
- `giveaway_stars(channel, winners, amount)` — Stars giveaway; 15 winners, 5001 000 000 stars each
- `giveaway_premium(channel, winners, months)` — Premium giveaway; 124 000 winners, 3/6/12 months each
- `StarsGiveawayResult`, `PremiumGiveawayResult` result types
**Telegram Ads**
- `recharge_ads(account, amount)` — top up a Telegram Ads account; 11 000 000 000 TON
- `AdsRechargeResult` result type
**Marketplace**
- `search_usernames(query?, sort?, filter?, offset_id?)` — search Fragment usernames; `sort`: `price_desc / price_asc / listed / ending`, `filter`: `auction / sale / sold`
- `search_numbers(query?, sort?, filter?, offset_id?)` — search Fragment anonymous numbers; same `sort` / `filter` / pagination semantics
- `search_gifts(query?, collection?, sort?, filter?, view?, attr?, offset?)` — search Fragment gifts; `attr` accepts `{"Model": ["Foosball"], "Backdrop": ["Celtic Blue"]}`
- `UsernamesResult`, `NumbersResult`, `GiftsResult` result types
**Anonymous numbers**
- `get_login_code(number)` — fetch the current pending login code
- `toggle_login_codes(number, can_receive)` — enable or disable login code delivery
- `terminate_sessions(number)` — terminate all active Telegram sessions (two-step flow handled internally)
- `LoginCodeResult`, `TerminateSessionsResult` result types; `AnonymousNumberError` exception
**Raw API**
- `FragmentClient.call(method, data, *, page_url)` — raw request to any Fragment API method
- `FRAGMENT_BASE_URL` constant — base URL shared across all page constants and headers
**Examples**
- `examples/client/` — `wallet_info.py` (wallet info), `raw_api_call.py` (raw API call)
- `examples/numbers/` — `manage_number.py` (login code fetch, session termination)
- `examples/auctions/` — `search_usernames.py`, `search_numbers.py`, `search_gifts.py` (marketplace search with pagination)
- `examples/purchase/` — `send_stars.py`, `send_premium.py`, `topup_ton_balance.py`, `run_stars_giveaway.py`, `run_premium_giveaway.py`, `recharge_ads_balance.py`
### Changed
- All result types now expose a unified `amount` field (`months` and `stars` removed)
- `__repr__` includes the unit — `3 months`, `500 stars`, etc.
- `timestamp` removed from all result dataclasses
- All page URL constants built from `FRAGMENT_BASE_URL`;
- `TransactionError` includes an SSL hint; `DUPLICATE_SEQNO` variant auto-retried up to 2 times (2 s apart)
- Error messages rewritten: "what happened → why → what to do"
---
## [2026.0.2] — 2026-03-20
### Added
- `timeout` parameter on `FragmentClient` (default `30.0` s) — passed through to every HTTP request
### Changed
- Cookie validation: narrowed type internally so no `# type: ignore` is needed in `FragmentClient.__init__`
- `WALLET_CLASSES` typed as `dict[str, Any]` so mypy resolves `from_mnemonic` correctly
- All four `examples/` files updated to `async with FragmentClient`, f-strings, and aligned error messages
- README usage section rewritten with a single comprehensive `async with` example
### Fixed
- mypy: missing return path in `process_transaction` after retry loop
- mypy: `cookies` union-attr error in `FragmentClient.__init__`
---
## [2026.0.1] — 2026-03-16
### Added
- Initial stable release of `pyfragment`
- `FragmentClient` — async client for the Fragment.com API with context manager support (`async with`)
- `purchase_premium(username, months)` — purchase Telegram Premium for any user (3, 6, or 12 months)
- `purchase_stars(username, amount)` — send Telegram Stars to any user (501,000,000)
- `topup_ton(username, amount)` — top up TON Ads balance (11,000,000,000 TON)
- `get_wallet()` — fetch wallet address and balance
- Support for TON wallet versions `V4R2` and `V5R1`
- Structured exception hierarchy (`FragmentError`, `ConfigurationError`, `CookieError`, etc.)
- `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
[2026.2.1]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.2.1
[2026.2.0]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.2.0
[2026.1.0]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.1.0
[2026.0.2]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.0.2
[2026.0.1]: https://github.com/bohd4nx/pyfragment/releases/tag/v2026.0.1
+58
View File
@@ -0,0 +1,58 @@
# Contributing to pyfragment
## Development setup
```bash
git clone https://github.com/bohd4nx/pyfragment.git
cd pyfragment
pip install -e ".[dev]"
```
## Running checks
```bash
# Lint and format
ruff check . --fix && ruff format .
# Type check
mypy . --explicit-package-bases
# Tests
pytest
```
All three must pass before opening a PR.
## Project structure
```
pyfragment/
client.py — FragmentClient (public entry point)
enums.py — ApiProvider, PaymentMethod, WalletVersion
exceptions.py — exception hierarchy
core/ — constants, validation helpers
domains/ — one package per feature domain
ads/ — recharge_ads, topup_gram
anonymous_numbers/— get_login_code, toggle_login_codes, terminate_sessions
giveaways/ — giveaway_stars, giveaway_premium
marketplace/ — search_usernames, search_numbers, search_gifts
purchases/ — purchase_stars, purchase_premium
services/ — shared infrastructure services
cookies/ — browser cookie extraction (models + service)
tonapi/ — wallet info, transaction signing (tonapi/toncenter)
tests/ — unit tests (pytest)
examples/ — runnable usage examples (excluded from CI)
```
## Conventions
- All public async methods live on `FragmentClient` and delegate to a domain service.
- Domain functions receive a `FragmentClient` instance, never raw HTTP clients.
- Patch targets in tests use the module where the name is **defined**, e.g. `pyfragment.services.tonapi.transaction._make_ton_client`.
- Versioning follows [CalVer](https://calver.org/): `YYYY.MINOR.MICRO`. Bump in `pyproject.toml`; tag as `vYYYY.MINOR.MICRO`.
## Pull requests
- Keep PRs focused — one feature or fix per PR.
- Update `CHANGELOG.md` under `[Unreleased]`.
- Add or update tests for any changed behaviour.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 bohd4nx
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+63 -4
View File
@@ -1,6 +1,65 @@
# pyfragment docs branch
<div align="center">
<img src="icon.svg" alt="pyfragment" width="96" height="96" style="border-radius: 20px;"><br><br>
This branch is dedicated to GitBook content.
# pyfragment
- Main docs source: docs/
- Navigation: docs/SUMMARY.md
[![PyPI](https://img.shields.io/pypi/v/pyfragment?style=flat&color=blue&label=PyPI)](https://pypi.org/project/pyfragment/)
[![Downloads](https://img.shields.io/pepy/dt/pyfragment?style=flat&color=blue&label=Downloads)](https://pepy.tech/projects/pyfragment)
[![Python](https://img.shields.io/pypi/pyversions/pyfragment?style=flat&color=blue&label=Python)](https://python.org)
[![Tests](https://img.shields.io/github/actions/workflow/status/bohd4nx/pyfragment/ci.yml?style=flat&label=Tests&logo=github)](https://github.com/bohd4nx/pyfragment/actions)
[![License](https://img.shields.io/github/license/bohd4nx/pyfragment?style=flat&color=blue&label=License)](LICENSE)
Async Python client for the **[Fragment.com](https://fragment.com)** marketplace API.
**[Documentation](https://bohd4nx.gitbook.io/pyfragment/)** · **[Examples](https://github.com/bohd4nx/pyfragment/tree/master/examples)**
</div>
> **Disclaimer:** This project is not affiliated with [Fragment](https://fragment.com) or [Telegram](https://telegram.org).
## Installation
```bash
pip install pyfragment
```
```bash
# Latest dev build
pip install git+https://github.com/bohd4nx/pyfragment.git@dev
```
---
## Quick Start
```python
import asyncio
from pyfragment import FragmentClient
from pyfragment.enums import PaymentMethod
async def main() -> None:
async with FragmentClient(
seed="word1 word2 ... word24",
api_key="YOUR_API_KEY",
cookies={"stel_ssid": "...", "stel_dt": "...", "stel_token": "...", "stel_ton_token": "..."},
) as client:
wallet = await client.get_wallet()
print("GRAM: %s | USDT: %s" % (wallet.gram_balance, wallet.usdt_balance))
stars = await client.purchase_stars("@username", amount=500, payment_method=PaymentMethod.USDT_GRAM)
print("Sent %s Stars to %s | tx: %s" % (stars.amount, stars.username, stars.transaction_id))
premium = await client.purchase_premium("@username", months=6, payment_method=PaymentMethod.GRAM)
print("Sent Premium %sm to %s | tx: %s" % (premium.amount, premium.username, premium.transaction_id))
asyncio.run(main())
```
<div align="center">
[Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)
</div>
+19
View File
@@ -0,0 +1,19 @@
# Security Policy
## Reporting a vulnerability
Please **do not** open a public GitHub issue for security vulnerabilities.
Report them privately via GitHub's [Security Advisory](https://github.com/bohd4nx/pyfragment/security/advisories/new) feature, or contact the maintainer directly at [@bohd4nx](https://t.me/bohd4nx) on Telegram.
Include:
- A description of the vulnerability and its potential impact.
- Steps to reproduce or a proof-of-concept.
- Affected versions.
You will receive a response within 72 hours. Once the fix is released, the advisory will be published.
## Scope
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.
-48
View File
@@ -1,48 +0,0 @@
# Overview
`pyfragment` is an async Python client for [Fragment](https://fragment.com).
If you are integrating Fragment into a bot or backend, this docs set is meant to be practical, not theoretical.
**Recommended reading order:**
1. Install the package
2. Configure `FragmentClient`
3. Set up credentials and cookies
4. Run the quick start
5. Move to feature-specific flows
## Who this is for
- Python developers integrating Fragment into bots, services, and automation.
- Teams that need predictable typed results and explicit error behavior.
**Important:** this library is not affiliated with Fragment or Telegram.
## Where to begin
1. [Installation](getting-started/installation.md)
2. [Library and Configuration](getting-started/configuration.md)
3. [Credentials and Cookies](getting-started/credentials-and-cookies.md)
4. [Quick Start](getting-started/quickstart.md)
## Feature entry points
- Stars: [Purchase](client/stars/purchase.md), [Giveaway](client/stars/giveaway.md)
- Premium: [Purchase](client/premium/purchase.md), [Giveaway](client/premium/giveaway.md)
- Marketplace: [Overview](client/marketplace/overview.md), Ads: [Overview](client/ads/overview.md)
- Numbers: [Anonymous Numbers](client/anonymous-numbers/overview.md)
- Utility operations: [Raw API Calls](client/raw-call.md)
## Additional references
- [Error Handling](reference/errors.md)
- [Result Models](reference/models.md)
- [Literal Types](reference/literals.md)
- [Troubleshooting](advanced/troubleshooting.md)
## Live examples
**Up-to-date runnable examples live in the main repository:**
- https://github.com/bohd4nx/pyfragment/tree/master/examples
-36
View File
@@ -1,36 +0,0 @@
- [Overview](README.md)
- Setup Guide
- [Installation](getting-started/installation.md)
- [Library and Configuration](getting-started/configuration.md)
- [Credentials and Cookies](getting-started/credentials-and-cookies.md)
- [Quick Start](getting-started/quickstart.md)
- [Error Handling](reference/errors.md)
- API Guides
- [Overview](client/overview.md)
- Stars
- [Purchase](client/stars/purchase.md)
- [Giveaway](client/stars/giveaway.md)
- Premium
- [Purchase](client/premium/purchase.md)
- [Giveaway](client/premium/giveaway.md)
- Marketplace
- [Overview](client/marketplace/overview.md)
- [Search Usernames](client/marketplace/search-usernames.md)
- [Search Numbers](client/marketplace/search-numbers.md)
- [Search Gifts](client/marketplace/search-gifts.md)
- Ads
- [Overview](client/ads/overview.md)
- [Top Up GRAM](client/ads/topup-gram.md)
- [Recharge Ads](client/ads/recharge-ads.md)
- Anonymous Numbers
- [Overview](client/anonymous-numbers/overview.md)
- [Get Login Code](client/anonymous-numbers/get-login-code.md)
- [Toggle Login Codes](client/anonymous-numbers/toggle-login-codes.md)
- [Terminate Sessions](client/anonymous-numbers/terminate-sessions.md)
- [Raw API Calls](client/raw-call.md)
- Reference
- [Result Models](reference/models.md)
- [Literal Types](reference/literals.md)
- Advanced
- [Cookie Extraction Details](advanced/cookies.md)
- [Troubleshooting](advanced/troubleshooting.md)
-24
View File
@@ -1,24 +0,0 @@
# Cookie Extraction Details
`get_cookies_from_browser(browser)` reads Fragment cookies from local browser storage (via `rookiepy`).
This is the fastest way to start when you do not want manual cookie export.
Supported browsers are defined in constants and include:
- chrome, firefox, edge, brave,
- arc, opera, opera_gx,
- safari, vivaldi,
- chromium variants.
Validation includes:
- required key presence,
- non-empty values,
- optional expiration check for `stel_ssid`.
**If any required cookie is empty or missing, extraction is treated as failed.**
If extraction fails, `CookieError` is raised with actionable details.
Use [Credentials and Cookies](../getting-started/credentials-and-cookies.md) for setup-first instructions.
-59
View File
@@ -1,59 +0,0 @@
# Troubleshooting
When something breaks, start here. Most issues are caused by cookies, session state, or wallet balance.
## Auth/session errors
Symptoms:
- Fragment page hash cannot be extracted,
- bad status loading Fragment pages,
- missing request IDs.
Actions:
- re-login on fragment.com,
- refresh cookies,
- ensure all `stel_*` keys are present.
- verify constructor payload in [Library and Configuration](../getting-started/configuration.md).
**Re-login + fresh cookies solves the majority of auth errors.**
## Cookie extraction errors
Symptoms:
- browser not supported,
- cannot read browser profile,
- required cookies not found.
Actions:
- install `pyfragment[browser]`,
- close locked browser profiles,
- use manual cookies if needed.
## Balance/transaction failures
Symptoms:
- low TON/USDT balance errors,
- broadcast failures,
- duplicate seqno retries.
Actions:
- keep GRAM (ex TON) reserve for fees,
- ensure USDT is on the **Fragment-linked wallet**,
- retry after short delay when seqno collisions happen.
- check operation constraints in Stars/Premium/Ads method pages.
## SSL-related broadcast failures
If you get SSL-related errors during **TON transaction broadcast** (not Fragment page loading — those use curl_cffi with bundled SSL):
```bash
pip install --upgrade certifi
```
On macOS, also run Python's `Install Certificates.command` if needed.
-16
View File
@@ -1,16 +0,0 @@
# Ads Overview
Ads flow is split into two methods:
- [Top Up GRAM](topup-gram.md)
- [Recharge Ads](recharge-ads.md)
Use the first method to send GRAM (ex TON) to a Telegram user.
Use the second method to fund your own Telegram Ads account.
## Common errors
- `ConfigurationError`
- `UserNotFoundError` (for recipient/account issues)
- `WalletError`
- `VerificationError`
-30
View File
@@ -1,30 +0,0 @@
# Recharge Ads
Use this method to add funds to your Telegram Ads account.
## Method
```python
await client.recharge_ads(
account: str,
amount: int,
) -> AdsRechargeResult
```
## Parameters
- `account`: channel or bot username linked to your ads account
- `amount`: integer from `1` to `1_000_000_000`
**Important:** `amount` must be an integer in the allowed range.
## Return
- `AdsRechargeResult(transaction_id, amount)`
## Example
```python
result: AdsRechargeResult = await client.recharge_ads("@mychannel", amount=50)
print(result.transaction_id)
```
-38
View File
@@ -1,38 +0,0 @@
# Top Up GRAM
Use this method to send GRAM (ex TON) to a user's Telegram balance.
## Method
```python
await client.topup_gram(
username: str,
amount: int,
show_sender: bool = True,
) -> AdsTopupResult
```
## Parameters
- `username`: recipient Telegram username — `@username`, `username`, or `https://t.me/username`
- `amount`: integer from `1` to `1_000_000_000`
- `show_sender`: controls sender visibility
**`amount` must be an integer in the allowed range.**
## Return
- `AdsTopupResult(transaction_id, username, amount)`
## Typical errors
- `ConfigurationError`: invalid amount
- `UserNotFoundError`: recipient not found on Fragment
- `WalletError`: insufficient GRAM (ex TON) balance
## Example
```python
result: AdsTopupResult = await client.topup_gram("@username", amount=10, show_sender=True)
print(result.transaction_id)
```
@@ -1,26 +0,0 @@
# Get Login Code
Use this method to fetch a pending login code for an anonymous number.
## Method
```python
await client.get_login_code(number: str) -> LoginCodeResult
```
## Parameters
- `number`: anonymous number (with or without leading `+`)
## Return
- `number`
- `code` (`None` if no pending code)
- `active_sessions`
## Example
```python
result: LoginCodeResult = await client.get_login_code("+1234567890")
print(result.code)
```
-16
View File
@@ -1,16 +0,0 @@
# Anonymous Numbers Overview
These methods help you manage login behavior and active sessions for anonymous numbers owned by your account.
Available methods:
- [Get Login Code](get-login-code.md)
- [Toggle Login Codes](toggle-login-codes.md)
- [Terminate Sessions](terminate-sessions.md)
## Common errors
- `AnonymousNumberError.NOT_OWNED`
- `AnonymousNumberError.TERMINATE_FAILED`
**If a number is not owned by your account, requests will fail.**
@@ -1,25 +0,0 @@
# Terminate Sessions
Use this method to terminate active sessions for an anonymous number.
## Method
```python
await client.terminate_sessions(number: str) -> TerminateSessionsResult
```
## Parameters
- `number`: anonymous number (with or without leading `+`)
## Return
- `number`
- `message`
## Example
```python
result: TerminateSessionsResult = await client.terminate_sessions("+1234567890")
print(result.message)
```
@@ -1,24 +0,0 @@
# Toggle Login Codes
Use this method to allow or block login code delivery.
## Method
```python
await client.toggle_login_codes(number: str, can_receive: bool) -> None
```
## Parameters
- `number`: anonymous number (with or without leading `+`)
- `can_receive`: `True` to allow codes, `False` to block codes
## Return
- `None`
## Example
```python
await client.toggle_login_codes("+1234567890", can_receive=False)
```
-31
View File
@@ -1,31 +0,0 @@
# Marketplace Overview
Marketplace methods are exposed directly on `FragmentClient` and via `client.marketplace` service.
If you only need one thing: pick the method by asset type (username, number, gift), then paginate until `next_offset_id` or `next_offset` becomes `None`.
Available methods:
- [Search Usernames](search-usernames.md)
- [Search Numbers](search-numbers.md)
- [Search Gifts](search-gifts.md)
## Shared behavior
- All methods are async.
- All methods call Fragment `searchAuctions` under the hood.
- `sort` and `filter` are optional passthrough strings.
**These values are passed to Fragment as-is.** If Fragment changes accepted values, behavior can change too.
Common values used by Fragment pages:
- `sort`: `price_desc`, `price_asc`, `listed`, `ending`
- `filter`: empty string, `auction`, `sale`, `sold`
## Pagination model
- Usernames and Numbers return `next_offset_id` (string)
- Gifts return `next_offset` (integer)
Use these fields to request next pages.
-87
View File
@@ -1,87 +0,0 @@
# Search Gifts
This endpoint is the most flexible marketplace search and supports collection, traits, and pagination.
## Method
```python
await client.search_gifts(
query: str = "",
collection: str | None = None,
sort: str | None = None,
filter: str | None = None,
view: str | None = None,
attr: dict[str, list[str]] | None = None,
offset: int | None = None,
) -> GiftsResult
```
## Parameters
- `query`: search text (empty string for broad listing)
- `collection`: collection slug (for example `plushpepe`, `swisswatch`)
- `sort`: optional sort key passed to Fragment
- `filter`: optional listing filter passed to Fragment
- `view`: optional UI/view mode passed to Fragment
- `attr`: optional trait filters where key is trait name and value is list of allowed values
- `offset`: page offset for next page
**`attr` is ideal for narrowing results by visual or rarity traits.**
## Sorting values
Common values accepted by Fragment:
- `price_desc`
- `price_asc`
- `listed`
- `ending`
## Filter values
Common values accepted by Fragment:
- empty string
- `auction`
- `sale`
- `sold`
## Attribute filter format
`attr` is encoded into request fields in this form:
- `attr[trait_name] = ["value1", "value2"]`
Example:
```python
attr={
"model": ["gold", "silver"],
"rarity": ["rare"],
}
```
In requests, each trait is sent as `attr[trait]` with a list of values.
## Return type
`GiftsResult` contains:
- `items: list[dict[str, Any]]`
- `next_offset: int | None`
## Pagination
If `next_offset` is not `None`, pass it back as `offset` to load the next page.
## Example
```python
result: GiftsResult = await client.search_gifts(
query="",
collection="plushpepe",
sort="price_desc",
filter="auction",
)
print(len(result.items), result.next_offset)
```
-61
View File
@@ -1,61 +0,0 @@
# Search Numbers
Use this endpoint to search anonymous Telegram number listings.
## Method
```python
await client.search_numbers(
query: str = "",
sort: str | None = None,
filter: str | None = None,
offset_id: str | None = None,
) -> NumbersResult
```
## Parameters
- `query`: digits or text to match number listings
- `sort`: optional sort key passed to Fragment
- `filter`: optional listing filter passed to Fragment
- `offset_id`: page cursor for next page
`query` can be partial digits (for example `"888"`) when you need pattern-based discovery.
## Sorting values
Common values accepted by Fragment:
- `price_desc`
- `price_asc`
- `listed`
- `ending`
## Filter values
Common values accepted by Fragment:
- empty string
- `auction`
- `sale`
- `sold`
## Return type
`NumbersResult` contains:
- `items: list[dict[str, Any]]`
- `next_offset_id: str | None`
## Pagination
If `next_offset_id` is not `None`, pass it back as `offset_id` to load the next page.
Keep requesting pages until `next_offset_id` becomes `None`.
## Example
```python
result: NumbersResult = await client.search_numbers("888", sort="price_asc", filter="sale")
print(len(result.items), result.next_offset_id)
```
@@ -1,61 +0,0 @@
# Search Usernames
Use this endpoint to discover Telegram usernames listed on Fragment.
## Method
```python
await client.search_usernames(
query: str = "",
sort: str | None = None,
filter: str | None = None,
offset_id: str | None = None,
) -> UsernamesResult
```
## Parameters
- `query`: search text (empty string means broad listing)
- `sort`: optional sort key passed to Fragment
- `filter`: optional listing filter passed to Fragment
- `offset_id`: page cursor for next page
For broad browsing, use empty `query` and set sorting only.
## Sorting values
Common values accepted by Fragment:
- `price_desc`
- `price_asc`
- `listed`
- `ending`
## Filter values
Common values accepted by Fragment:
- empty string
- `auction`
- `sale`
- `sold`
## Return type
`UsernamesResult` contains:
- `items: list[dict[str, Any]]`
- `next_offset_id: str | None`
## Pagination
If `next_offset_id` is not `None`, pass it back as `offset_id` to load the next page.
This is cursor pagination, so do not try to calculate offsets manually.
## Example
```python
result: UsernamesResult = await client.search_usernames("durov", sort="price_desc", filter="auction")
print(len(result.items), result.next_offset_id)
```
-43
View File
@@ -1,43 +0,0 @@
# Client Overview
`FragmentClient` is the main API surface.
You can call methods directly on the client or use grouped services.
Grouped service wrappers:
- `client.purchases`
- `client.giveaways`
- `client.ads`
- `client.anonymous_numbers`
- `client.marketplace`
- `client.tonapi`
Main async methods on `FragmentClient`:
- `purchase_stars(...)`
- `purchase_premium(...)`
- `giveaway_stars(...)`
- `giveaway_premium(...)`
- `topup_gram(...)`
- `recharge_ads(...)`
- `get_wallet()`
- `get_login_code(...)`
- `toggle_login_codes(...)`
- `terminate_sessions(...)`
- `search_usernames(...)`
- `search_numbers(...)`
- `search_gifts(...)`
- `call(...)`
All methods are async and should be used inside `async with FragmentClient(...) as client:`.
## Flow map
- Stars: [Purchase](stars/purchase.md), [Giveaway](stars/giveaway.md)
- Premium: [Purchase](premium/purchase.md), [Giveaway](premium/giveaway.md)
- Marketplace: [Overview](marketplace/overview.md), Ads: [Overview](ads/overview.md)
- Numbers: [Anonymous Numbers](anonymous-numbers/overview.md)
- Utility operations: [Raw API Calls](raw-call.md)
**If you are new to the library, start with Stars Purchase or Wallet read (`get_wallet`) first.**
-41
View File
@@ -1,41 +0,0 @@
# Premium Giveaway
Use this method to run a Telegram Premium giveaway for your channel.
## Method
```python
await client.giveaway_premium(
channel: str,
winners: int,
months: int = 3,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumGiveawayResult
```
## Parameters
- `channel`: accepts `@channel`, `channel`, or `https://t.me/channel`
- `winners`: integer from `1` to `24_000`
- `months`: one of `3`, `6`, `12`
- `payment_method`: `PaymentMethod.GRAM` (default), `PaymentMethod.USDT_GRAM`, or any other `PaymentMethod` value
**`winners` must be a positive integer, and large values can increase total cost significantly.**
## Return
- `PremiumGiveawayResult(transaction_id, channel, winners, amount)`
## Typical errors
- `ConfigurationError`
- `UserNotFoundError`
- `WalletError`
- `VerificationError`
## Example
```python
result: PremiumGiveawayResult = await client.giveaway_premium("@channel", winners=100, months=3)
print(result.amount)
```
-41
View File
@@ -1,41 +0,0 @@
# Premium Purchase
Use this method to gift Telegram Premium to a specific user.
## Method
```python
await client.purchase_premium(
username: str,
months: int,
show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumResult
```
## Parameters
- `username`: accepts `@username`, `username`, or `https://t.me/username`
- `months`: one of `3`, `6`, `12`
- `show_sender`: controls sender visibility on recipient side
- `payment_method`: `PaymentMethod.GRAM` (default), `PaymentMethod.USDT_GRAM`, or any other `PaymentMethod` value
**`months` only supports `3`, `6`, or `12`.**
## Return
- `PremiumResult(transaction_id, username, amount)`
## Typical errors
- `ConfigurationError`
- `UserNotFoundError`
- `WalletError`
- `VerificationError`
## Example
```python
result: PremiumResult = await client.purchase_premium("@username", months=6, payment_method=PaymentMethod.GRAM)
print(result.transaction_id)
```
-42
View File
@@ -1,42 +0,0 @@
# Raw API Calls
Use `client.call()` when you need a Fragment API method that does not yet have a dedicated wrapper.
```python
result = await client.call(
"searchPremiumGiftRecipient",
{"query": "@username", "months": 3},
page_url="https://fragment.com/premium/gift",
)
```
Signature:
```python
await client.call(
method: str,
data: dict[str, Any] | None = None,
*,
page_url: str = "https://fragment.com",
) -> dict[str, Any]
```
## Parameters
- `method`: Fragment API method name
- `data`: optional request payload as dictionary
- `page_url`: page URL used for referer/hash context (defaults to `https://fragment.com`)
## Return
- `dict[str, Any]`: raw Fragment API response
Use this carefully:
- request/response shape is Fragment-defined,
- undocumented methods can change without notice,
- you are responsible for validating returned fields.
## Recommended approach
Use dedicated wrappers first, and fallback to `call()` only for missing API surface.
-41
View File
@@ -1,41 +0,0 @@
# Stars Giveaway
Use this method to run a Stars giveaway for a channel audience.
## Method
```python
await client.giveaway_stars(
channel: str,
winners: int,
amount: int,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsGiveawayResult
```
## Parameters
- `channel`: accepts `@channel`, `channel`, or `https://t.me/channel`
- `winners`: integer from `1` to `15`
- `amount`: integer from `500` to `1_000_000` (per winner)
- `payment_method`: `PaymentMethod.GRAM` (default), `PaymentMethod.USDT_GRAM`, or any other `PaymentMethod` value
**Each winner receives the full `amount` value.**
## Return
- `StarsGiveawayResult(transaction_id, channel, winners, amount)`
## Typical errors
- `ConfigurationError`
- `UserNotFoundError`
- `WalletError`
- `VerificationError`
## Example
```python
result: StarsGiveawayResult = await client.giveaway_stars("@channel", winners=3, amount=1000)
print(result.transaction_id)
```
-41
View File
@@ -1,41 +0,0 @@
# Stars Purchase
Use this method to send Telegram Stars directly to a user.
## Method
```python
await client.purchase_stars(
username: str,
amount: int,
show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsResult
```
## Parameters
- `username`: accepts `@username`, `username`, or `https://t.me/username`
- `amount`: integer from `50` to `10_000_000`
- `show_sender`: controls sender visibility on recipient side
- `payment_method`: `PaymentMethod.GRAM` (default), `PaymentMethod.USDT_GRAM`, or any other `PaymentMethod` value
**Amount must be between `50` and `10_000_000`.**
## Return
- `StarsResult(transaction_id, username, amount)`
## Typical errors
- `ConfigurationError`: invalid amount or payment method
- `UserNotFoundError`: target user not found
- `WalletError`: insufficient balance or wallet-side issue
- `VerificationError`: verification/KYC required for operation
## Example
```python
result: StarsResult = await client.purchase_stars("@username", amount=500, payment_method=PaymentMethod.GRAM)
print(result.amount)
```
-78
View File
@@ -1,78 +0,0 @@
# Library and Configuration
Main entry point of the library is `FragmentClient`.
```python
FragmentClient(
seed: str,
api_key: str,
cookies: dict[str, Any] | str,
wallet_version: str = "V5R1",
api_provider: str = "tonapi",
timeout: float = 30.0,
)
```
## Parameters
- `seed`: wallet mnemonic (**12 or 24 words**)
- `api_key`: API key — from [tonconsole.com](https://tonconsole.com) (tonapi) or [@toncenter](https://t.me/toncenter)
- `cookies`: Fragment cookies as a dictionary or JSON string
- `wallet_version`: `"V4R2"`, `"V5R1"`, `"HighloadV2"`, or `"HighloadV3R1"`
- `api_provider`: blockchain API provider — `"tonapi"` (default) or `"toncenter"`
- `timeout`: request timeout in seconds
**If `api_key` or cookies are missing, initialization fails immediately.**
## Required cookies
- `stel_ssid`
- `stel_dt`
- `stel_token`
- `stel_ton_token`
## Minimal initialization pattern
```python
from pyfragment import FragmentClient
async with FragmentClient(
seed="word1 word2 ... word24",
api_key="YOUR_API_KEY",
cookies={
"stel_ssid": "...",
"stel_dt": "...",
"stel_token": "...",
"stel_ton_token": "...",
},
) as client:
wallet = await client.get_wallet()
```
## Switching API provider
By default, the library uses [tonconsole.com](https://tonconsole.com) (tonapi). To use [toncenter](https://t.me/toncenter) instead, pass `api_provider="toncenter"`:
```python
async with FragmentClient(
seed="...",
api_key="YOUR_TONCENTER_API_KEY",
cookies={...},
api_provider="toncenter",
) as client:
...
```
Both providers work identically — the correct `tonutils` client is selected automatically based on `api_provider`.
## Validation behavior
At initialization, library validates:
- seed format,
- cookie shape and required keys,
- supported wallet version,
- supported API provider,
- parseability of cookie JSON strings.
Constructor-level issues are raised as `ConfigurationError` or `CookieError`.
@@ -1,56 +0,0 @@
# Credentials and Cookies
This page covers the three things you need before making real requests: Tonapi key, wallet seed, and Fragment cookies.
## Tonapi key
Generate an API key at https://tonconsole.com.
## Seed phrase
Use your GRAM (ex TON) wallet mnemonic.
- **Keep it private.**
- **Never log it or commit it to git.**
## Fragment cookies
You must be logged in to Fragment.
### Option 1: automatic extraction
```python
from pyfragment import get_cookies_from_browser
cookie_result = get_cookies_from_browser("chrome")
cookies = cookie_result.cookies
```
`cookie_result` is `CookieResult`:
- `cookies`: `dict[str, str]`
- `expires`: ISO string or `None`
### Option 2: manual export
Export the four required Fragment cookies and pass them directly as dict or JSON string.
Required keys:
- `stel_ssid`
- `stel_dt`
- `stel_token`
- `stel_ton_token`
## Common auth failures
- expired session cookies,
- not logged in on fragment.com,
- missing `stel_*` keys,
- stale cookies from another browser/profile.
When this happens, re-login on fragment.com and refresh cookies first. It solves most auth issues.
## Next step
Proceed to [Quick Start](quickstart.md).
-35
View File
@@ -1,35 +0,0 @@
# Installation
You can be up and running in under a minute.
## Requirements
- Python 3.11 3.14
## Install from PyPI
```bash
pip install pyfragment
```
## Install latest dev branch
```bash
pip install git+https://github.com/bohd4nx/pyfragment.git@dev
```
## Optional browser cookie extraction support
If you want automatic cookie extraction from local browser profiles:
```bash
pip install "pyfragment[browser]"
```
This installs `rookiepy`, used by `get_cookies_from_browser()`.
**Use this extra if you do not want to copy cookies manually.**
## Next step
After installation, continue with [Library and Configuration](configuration.md).
-48
View File
@@ -1,48 +0,0 @@
# Quick Start
Use this minimal example to verify that your credentials, cookies, and wallet setup are correct.
```python
import asyncio
from pyfragment import FragmentClient
from pyfragment.enums import PaymentMethod
async def main() -> None:
async with FragmentClient(
seed="word1 word2 ... word24",
api_key="YOUR_API_KEY", # tonconsole.com (tonapi, default) or t.me/toncenter
cookies={
"stel_ssid": "...",
"stel_dt": "...",
"stel_token": "...",
"stel_ton_token": "...",
},
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
wallet = await client.get_wallet()
print("GRAM: %s | USDT: %s" % (wallet.gram_balance, wallet.usdt_balance))
recipient = "https://t.me/username" # also: @username, username
stars = await client.purchase_stars(recipient, amount=500, payment_method=PaymentMethod.USDT_GRAM)
print("Sent %s Stars to %s | tx: %s" % (stars.amount, stars.username, stars.transaction_id))
premium = await client.purchase_premium(recipient, months=6, payment_method=PaymentMethod.GRAM)
print("Sent Premium %sm to %s | tx: %s" % (premium.amount, premium.username, premium.transaction_id))
asyncio.run(main())
```
If this script returns wallet data, your setup is healthy.
Then move to feature pages:
- Stars: [Purchase](client/stars/purchase.md), [Giveaway](client/stars/giveaway.md)
- Premium: [Purchase](client/premium/purchase.md), [Giveaway](client/premium/giveaway.md)
- Marketplace: [Overview](client/marketplace/overview.md), Ads: [Overview](client/ads/overview.md)
- Numbers: [Anonymous Numbers](client/anonymous-numbers/overview.md)
- Utility operations: [Raw API Calls](client/raw-call.md)
-56
View File
@@ -1,56 +0,0 @@
# Error Handling
Good error handling is the difference between a stable integration and random production failures.
## Exception hierarchy
- `FragmentError`
- `ClientError`
- `ConfigurationError`
- `CookieError`
- `FragmentAPIError`
- `FragmentPageError`
- `UserNotFoundError`
- `AlreadySubscribedError`
- `AnonymousNumberError`
- `TransactionError`
- `ParseError`
- `VerificationError`
- `OperationError`
- `WalletError`
- `UnexpectedError`
## Recommended handling pattern
```python
from pyfragment import ConfigurationError, FragmentError, UserNotFoundError, WalletError
try:
result = await client.purchase_stars("@username", amount=500)
except UserNotFoundError:
# recipient does not exist on Fragment
...
except WalletError:
# insufficient balance or wallet-side issue
...
except ConfigurationError:
# invalid local input
...
except FragmentError:
# any other library-level failure
...
```
**Catch specific errors first, then fallback to `FragmentError`.**
## Method-to-error mapping
- Stars purchase: `ConfigurationError`, `UserNotFoundError`, `WalletError`, `VerificationError`
- Premium purchase: `ConfigurationError`, `UserNotFoundError`, `AlreadySubscribedError`, `WalletError`, `VerificationError`
- Stars/Premium giveaway: `ConfigurationError`, `UserNotFoundError`, `WalletError`, `VerificationError`
- Ads operations: `ConfigurationError`, `UserNotFoundError`, `WalletError`, `VerificationError`
- Cookies/auth setup: `CookieError`, `ConfigurationError`, `FragmentPageError`
## Canonical messages
See `pyfragment/exceptions.py` for source-of-truth message templates.
-54
View File
@@ -1,54 +0,0 @@
# Literal Types
These literals describe accepted string values for key method parameters.
## ApiProvider
```python
from pyfragment.enums import ApiProvider
ApiProvider.TONAPI # tonconsole.com — default
ApiProvider.TONCENTER # t.me/toncenter
```
Pass as string to `FragmentClient(api_provider=...)`:
```python
FragmentClient(..., api_provider="tonapi") # default
FragmentClient(..., api_provider="toncenter")
```
## PaymentMethod
```python
from pyfragment.enums import PaymentMethod
PaymentMethod.GRAM # GRAM (ex TON) — default
PaymentMethod.USDT_GRAM # USDT on GRAM (ex TON)
PaymentMethod.USDT_ETH # USDT on Ethereum
PaymentMethod.USDT_POL # USDT on Polygon
PaymentMethod.USDC_ETH # USDC on Ethereum
PaymentMethod.USDC_BASE # USDC on Base
PaymentMethod.USDC_POL # USDC on Polygon
```
## WalletVersion
```python
from pyfragment.enums import WalletVersion
WalletVersion.V5R1 # default
WalletVersion.V4R2
WalletVersion.HighloadV2
WalletVersion.HighloadV3R1
```
All enums are exported from both `pyfragment` (top-level) and `pyfragment.enums`.
## Usage notes
- Use `ApiProvider` when configuring the blockchain API provider in `FragmentClient`.
- Use `PaymentMethod` for purchase and giveaway operations.
- Use `WalletVersion` when configuring `FragmentClient`.
**Passing unsupported values raises `ConfigurationError`.**
-47
View File
@@ -1,47 +0,0 @@
# Result Models
Every high-level method returns a typed model, so you can rely on predictable fields instead of raw payload parsing.
Exported result models:
- `CookieResult(cookies, expires)`
- `StarsResult(transaction_id, username, amount)`
- `PremiumResult(transaction_id, username, amount)`
- `AdsTopupResult(transaction_id, username, amount)`
- `AdsRechargeResult(transaction_id, amount)`
- `StarsGiveawayResult(transaction_id, channel, winners, amount)`
- `PremiumGiveawayResult(transaction_id, channel, winners, amount)`
- `WalletInfo(address, state, gram_balance, usdt_balance)`
- `LoginCodeResult(number, code, active_sessions)`
- `TerminateSessionsResult(number, message)`
- `UsernamesResult(items, next_offset_id)`
- `NumbersResult(items, next_offset_id)`
- `GiftsResult(items, next_offset)`
Most high-level methods return one of these dataclasses.
## Where they are used
- `purchase_stars()`: `StarsResult`
- `purchase_premium()`: `PremiumResult`
- `giveaway_stars()`: `StarsGiveawayResult`
- `giveaway_premium()`: `PremiumGiveawayResult`
- `topup_gram()`: `AdsTopupResult`
- `recharge_ads()`: `AdsRechargeResult`
- `get_wallet()`: `WalletInfo`
- `get_login_code()`: `LoginCodeResult`
- `terminate_sessions()`: `TerminateSessionsResult`
- `search_usernames()`: `UsernamesResult`
- `search_numbers()`: `NumbersResult`
- `search_gifts()`: `GiftsResult`
## Methods without dataclass return
- `toggle_login_codes()`: returns `None`
- `call()`: returns `dict[str, Any]` (raw Fragment API response)
## Cookie helper
`CookieResult` is returned by `get_cookies_from_browser()`, not by `FragmentClient` methods.
**Use these models directly in your app layer and avoid passing raw dictionaries around.**
+53
View File
@@ -0,0 +1,53 @@
"""
Example: search the Fragment gifts marketplace.
collection filters by gift type slug (e.g. "plushpepe", "swisswatch").
sort can be "price_desc", "price_asc", "listed", or "ending".
filter can be "", "auction", "sale", or "sold".
Use next_offset for pagination.
"""
import asyncio
import json
from pyfragment import FragmentClient, GiftsResult
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
QUERY = "" # search text — or omit for all
COLLECTION = "plushpepe" # gift collection slug — or omit for all
SORT = "price_desc" # "price_desc", "price_asc", "listed", "ending" — or omit
FILTER = "" # "", "auction", "sale", "sold" — or omit
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
result: GiftsResult = await client.search_gifts(QUERY, collection=COLLECTION, sort=SORT, filter=FILTER)
print(f"Found {len(result.items)} result(s):")
print(json.dumps(result.items, indent=2))
if result.next_offset:
print(f"\nMore results available — next page offset: {result.next_offset}")
if __name__ == "__main__":
asyncio.run(main())
+51
View File
@@ -0,0 +1,51 @@
"""
Example: search the Fragment marketplace for anonymous Telegram numbers.
sort can be "price_desc", "price_asc", "listed", or "ending".
filter can be "", "auction", "sale", or "sold".
Use next_offset_id for pagination.
"""
import asyncio
import json
from pyfragment import FragmentClient, NumbersResult
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
QUERY = "888" # search term — or omit for all
SORT = "price_asc" # "price_desc", "price_asc", "listed", "ending" — or omit
FILTER = "" # "", "auction", "sale", "sold" — or omit
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
result: NumbersResult = await client.search_numbers(QUERY, sort=SORT, filter=FILTER)
print(f"Found {len(result.items)} result(s):")
print(json.dumps(result.items, indent=2))
if result.next_offset_id:
print(f"\nMore results available — next page offset: {result.next_offset_id}")
if __name__ == "__main__":
asyncio.run(main())
+51
View File
@@ -0,0 +1,51 @@
"""
Example: search the Fragment marketplace for Telegram usernames.
sort can be "price_desc", "price_asc", "listed", or "ending".
filter can be "", "auction", "sale", or "sold".
Use next_offset_id for pagination.
"""
import asyncio
import json
from pyfragment import FragmentClient, UsernamesResult
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
QUERY = "durov" # search term
SORT = "price_desc" # "price_desc", "price_asc", "listed", "ending" — or omit
FILTER = "auction" # "", "auction", "sale", "sold" — or omit
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
result: UsernamesResult = await client.search_usernames(QUERY, sort=SORT, filter=FILTER)
print(f"Found {len(result.items)} result(s):")
print(json.dumps(result.items, indent=2))
if result.next_offset_id:
print(f"\nMore results available — next page offset: {result.next_offset_id}")
if __name__ == "__main__":
asyncio.run(main())
+33
View File
@@ -0,0 +1,33 @@
"""
Example: extract Fragment cookies directly from your browser.
get_cookies_from_browser() reads the Fragment session cookies from a locally
installed browser — no manual copy-paste required.
Supported browsers: arc, brave, chrome, chromium, chromium_based, edge,
firefox, firefox_based, librewolf, opera, opera_gx,
safari, vivaldi.
The returned CookieResult.cookies dict can be passed directly to FragmentClient.
"""
from pyfragment import CookieError, get_cookies_from_browser
def main() -> None:
try:
result = get_cookies_from_browser("chrome") # or "firefox", "edge", "brave", ...
except CookieError as e:
print(f"Could not read cookies: {e}")
return
print(f"Cookies expire: {result.expires}")
print(f"Keys found: {list(result.cookies.keys())}")
# Pass the extracted cookies directly to FragmentClient
# async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=result.cookies) as client:
# ...
if __name__ == "__main__":
main()
+48
View File
@@ -0,0 +1,48 @@
"""
Example: send a raw request to any Fragment API method.
Use client.call() when you need to access a method that is not yet
wrapped by the library, or to inspect raw API responses directly.
page_url is optional — only set it when the target method belongs to a
specific Fragment page (Fragment derives the API hash per page).
Defaults to the Fragment base URL.
"""
import asyncio
from pyfragment import FragmentClient
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
METHOD = "anyFragmentMethod" # replace with the actual method name
DATA = {"key": "value"} # replace with the actual request payload
PAGE_URL = "https://fragment.com/stars/buy" # replace with the matching Fragment page (optional)
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
result = await client.call(METHOD, DATA, page_url=PAGE_URL)
print(result)
if __name__ == "__main__":
asyncio.run(main())
+44
View File
@@ -0,0 +1,44 @@
"""
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.
api_provider defaults to "tonapi" (tonconsole.com) — pass "toncenter" to use t.me/toncenter instead.
"""
import asyncio
from pyfragment import FragmentClient
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
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:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
wallet = await client.get_wallet()
print(f"Address: {wallet.address}")
print(f"State: {wallet.state}")
print(f"Balance: {wallet.gram_balance} GRAM (ex TON)")
print(f"Balance: {wallet.usdt_balance} USDT")
if __name__ == "__main__":
asyncio.run(main())
+54
View File
@@ -0,0 +1,54 @@
"""
Example: manage an anonymous Telegram number — read login code and terminate sessions.
Use get_login_code() to fetch the current pending login code for your number.
Use toggle_login_codes() to enable or disable receiving codes.
Use terminate_sessions() to forcefully end all active Telegram sessions.
"""
import asyncio
from pyfragment import AnonymousNumberError, FragmentClient
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
NUMBER = "+88888888888"
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
# Fetch the latest login code
result = await client.get_login_code(NUMBER)
if result.code:
print(f"Login code for {result.number}: {result.code} ({result.active_sessions} active session(s))")
else:
print(f"No pending login code for {result.number} ({result.active_sessions} active session(s))")
# Terminate all active sessions
try:
terminated = await client.terminate_sessions(NUMBER)
print(f"Sessions terminated for {terminated.number}" + (f": {terminated.message}" if terminated.message else ""))
except AnonymousNumberError as e:
print(f"Could not terminate sessions: {e}")
if __name__ == "__main__":
asyncio.run(main())
+62
View File
@@ -0,0 +1,62 @@
"""
Example: run a Telegram Premium giveaway for a channel.
winners must be an integer between 1 and 24 000.
months (Premium duration per winner) must be 3, 6, or 12.
Channel can be "@channel", "channel", or "https://t.me/channel".
"""
import asyncio
from pyfragment import ConfigurationError, FragmentClient, UserNotFoundError
from pyfragment.enums import PaymentMethod
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
CHANNEL = "https://t.me/channel"
WINNERS = 10 # 124 000
MONTHS = 3 # 3, 6 or 12
PAYMENT_METHOD = PaymentMethod.GRAM # GRAM, USDT_GRAM, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
try:
result = await client.giveaway_premium(
CHANNEL,
winners=WINNERS,
months=MONTHS,
payment_method=PAYMENT_METHOD,
)
except UserNotFoundError:
print(f"Channel {CHANNEL} was not found on fragment.com — check the username and try again.")
return
except ConfigurationError as e:
print(f"Invalid argument: {e}")
return
print(
f"Premium giveaway created for {result.channel}{result.winners} winner(s) × {result.amount} months each | tx: {result.transaction_id}"
)
if __name__ == "__main__":
asyncio.run(main())
+59
View File
@@ -0,0 +1,59 @@
"""
Example: purchase Telegram Premium for a user.
Supported durations: 3, 6, or 12 months.
Set show_sender=False to send anonymously.
Username can be "@username", "username", or "https://t.me/username".
"""
import asyncio
from pyfragment import ConfigurationError, FragmentClient, UserNotFoundError
from pyfragment.enums import PaymentMethod
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
USERNAME = "https://t.me/username"
MONTHS = 3 # 3, 6 or 12
PAYMENT_METHOD = PaymentMethod.GRAM # GRAM, USDT_GRAM, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
try:
result = await client.purchase_premium(
USERNAME,
months=MONTHS,
show_sender=True,
payment_method=PAYMENT_METHOD,
)
except UserNotFoundError:
print(f"User {USERNAME} was not found on fragment.com — check the username and try again.")
return
except ConfigurationError as e:
print(f"Invalid argument: {e}")
return
print(f"{result.amount} months of Premium successfully sent to {result.username} | tx: {result.transaction_id}")
if __name__ == "__main__":
asyncio.run(main())
+59
View File
@@ -0,0 +1,59 @@
"""
Example: purchase Telegram Stars for a user.
Amount must be an integer between 50 and 10 000 000.
Set show_sender=False to send anonymously.
Username can be "@username", "username", or "https://t.me/username".
"""
import asyncio
from pyfragment import ConfigurationError, FragmentClient, UserNotFoundError
from pyfragment.enums import PaymentMethod
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
USERNAME = "https://t.me/username"
AMOUNT = 500 # 5010 000 000 stars
PAYMENT_METHOD = PaymentMethod.USDT_GRAM # GRAM, USDT_GRAM, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
try:
result = await client.purchase_stars(
USERNAME,
amount=AMOUNT,
show_sender=True,
payment_method=PAYMENT_METHOD,
)
except UserNotFoundError:
print(f"User {USERNAME} was not found on fragment.com — check the username and try again.")
return
except ConfigurationError as e:
print(f"Invalid argument: {e}")
return
print(f"{result.amount} Stars successfully sent to {result.username} | tx: {result.transaction_id}")
if __name__ == "__main__":
asyncio.run(main())
+56
View File
@@ -0,0 +1,56 @@
"""
Example: recharge your own Telegram Ads account with GRAM (ex TON).
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
from pyfragment import (
AdsRechargeResult,
ConfigurationError,
FragmentClient,
WalletError,
)
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
ACCOUNT = "@mychannel" # channel or bot username linked to your Telegram Ads account
AMOUNT = 10 # 11 000 000 000 GRAM (ex TON)
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
try:
result: AdsRechargeResult = await client.recharge_ads(ACCOUNT, amount=AMOUNT)
except WalletError as e:
print(f"Wallet error — insufficient balance or misconfiguration: {e}")
return
except ConfigurationError as e:
print(f"Invalid argument: {e}")
return
print(f"{result.amount} GRAM (ex TON) recharged to Ads account {ACCOUNT} | tx: {result.transaction_id}")
if __name__ == "__main__":
asyncio.run(main())
+62
View File
@@ -0,0 +1,62 @@
"""
Example: run a Telegram Stars giveaway for a channel.
winners must be an integer between 1 and 15.
amount (stars per winner) must be an integer between 500 and 1 000 000.
Channel can be "@channel", "channel", or "https://t.me/channel".
"""
import asyncio
from pyfragment import ConfigurationError, FragmentClient, UserNotFoundError
from pyfragment.enums import PaymentMethod
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
COOKIES = {
"stel_ssid": "YOUR_STEL_SSID",
"stel_dt": "YOUR_STEL_DT",
"stel_token": "YOUR_STEL_TOKEN",
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
}
CHANNEL = "https://t.me/channel"
WINNERS = 3 # 115
AMOUNT = 1000 # 5001 000 000 stars per winner
PAYMENT_METHOD = PaymentMethod.USDT_GRAM # GRAM, USDT_GRAM, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
try:
result = await client.giveaway_stars(
CHANNEL,
winners=WINNERS,
amount=AMOUNT,
payment_method=PAYMENT_METHOD,
)
except UserNotFoundError:
print(f"Channel {CHANNEL} was not found on fragment.com — check the username and try again.")
return
except ConfigurationError as e:
print(f"Invalid argument: {e}")
return
print(
f"Stars giveaway created for {result.channel}{result.winners} winner(s) × {result.amount} stars each | tx: {result.transaction_id}"
)
if __name__ == "__main__":
asyncio.run(main())
+61
View File
@@ -0,0 +1,61 @@
"""
Example: top up GRAM (ex TON) to a recipient's Telegram balance.
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 GRAM (ex TON).
Your wallet must satisfy the current minimum GRAM (ex TON) threshold and transaction cost.
"""
import asyncio
from pyfragment import (
ConfigurationError,
FragmentClient,
UserNotFoundError,
WalletError,
)
SEED = "word1 word2 ... word24"
API_KEY = "YOUR_API_KEY" # tonconsole.com (tonapi, default) or t.me/toncenter
# Option A: extract cookies directly from your browser (no manual copy-paste needed)
# COOKIES = get_cookies_from_browser("chrome").cookies # or "firefox", "edge", "brave", ...
# Option B: provide cookies manually
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 # 11 000 000 000 GRAM (ex TON)
async def main() -> None:
async with FragmentClient(
seed=SEED,
api_key=API_KEY,
cookies=COOKIES,
wallet_version="V5R1", # or "V4R2", "HighloadV2", "HighloadV3R1"
api_provider="tonapi", # or "toncenter"
) as client:
try:
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
except WalletError as e:
print(f"Wallet error — insufficient balance or misconfiguration: {e}")
return
except ConfigurationError as e:
print(f"Invalid argument: {e}")
return
print(f"{result.amount} GRAM (ex TON) successfully topped up for {result.username} | tx: {result.transaction_id}")
if __name__ == "__main__":
asyncio.run(main())
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" width="512" height="512" style="width: 100%; height: 100%; transform: translate3d(0,0,0); content-visibility: visible;" preserveAspectRatio="xMidYMid meet"><defs><clipPath id="__lottie_element_2"><rect width="512" height="512" x="0" y="0"></rect></clipPath><clipPath id="__lottie_element_4"><path d="M0,0 L100,0 L100,100 L0,100z"></path></clipPath></defs><g clip-path="url(#__lottie_element_2)"><g clip-path="url(#__lottie_element_4)" style="display: block;" transform="matrix(5.119999885559082,0,0,5.119999885559082,0,0)" opacity="1"><g style="display: block;" transform="matrix(1.333299994468689,0,0,1.333299994468689,0,0)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill="rgb(30,40,51)" fill-opacity="1" d=" M47.31999969482422,5.619999885559082 C47.31999969482422,5.619999885559082 27.68000030517578,5.619999885559082 27.68000030517578,5.619999885559082 C15.5,5.619999885559082 5.619999885559082,15.5 5.619999885559082,27.68000030517578 C5.619999885559082,27.68000030517578 5.619999885559082,47.31999969482422 5.619999885559082,47.31999969482422 C5.619999885559082,59.5 15.5,69.37999725341797 27.68000030517578,69.37999725341797 C27.68000030517578,69.37999725341797 47.31999969482422,69.37999725341797 47.31999969482422,69.37999725341797 C59.5,69.37999725341797 69.37999725341797,59.5 69.37999725341797,47.31999969482422 C69.37999725341797,47.31999969482422 69.37999725341797,27.68000030517578 69.37999725341797,27.68000030517578 C69.37999725341797,15.5 59.5,5.619999885559082 47.31999969482422,5.619999885559082 C47.31999969482422,5.619999885559082 47.31999969482422,5.619999885559082 47.31999969482422,5.619999885559082z"></path></g><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill="rgb(255,255,255)" fill-opacity="1" d=" M36.349998474121094,32.79999923706055 C36.349998474121094,32.79999923706055 21.1299991607666,25.940000534057617 21.1299991607666,25.940000534057617 C20.020000457763672,25.450000762939453 20.389999389648438,23.790000915527344 21.600000381469727,23.790000915527344 C21.600000381469727,23.790000915527344 53.41999816894531,23.790000915527344 53.41999816894531,23.790000915527344 C54.630001068115234,23.790000915527344 54.9900016784668,25.440000534057617 53.880001068115234,25.940000534057617 C53.880001068115234,25.940000534057617 38.66999816894531,32.79999923706055 38.66999816894531,32.79999923706055 C37.939998626708984,33.130001068115234 37.09000015258789,33.130001068115234 36.36000061035156,32.79999923706055 C36.36000061035156,32.79999923706055 36.349998474121094,32.79999923706055 36.349998474121094,32.79999923706055z M56.81999969482422,30.06999969482422 C57.43000030517578,29.1200008392334 56.43000030517578,27.979999542236328 55.400001525878906,28.440000534057617 C55.400001525878906,28.440000534057617 40.72999954223633,35.13999938964844 40.72999954223633,35.13999938964844 C39.72999954223633,35.599998474121094 39.09000015258789,36.61000061035156 39.09000015258789,37.70000076293945 C39.09000015258789,37.70000076293945 39.09000015258789,53.810001373291016 39.09000015258789,53.810001373291016 C39.09000015258789,54.93000030517578 40.54999923706055,55.36000061035156 41.15999984741211,54.41999816894531 C41.15999984741211,54.41999816894531 56.810001373291016,30.06999969482422 56.810001373291016,30.06999969482422 C56.810001373291016,30.06999969482422 56.81999969482422,30.06999969482422 56.81999969482422,30.06999969482422z M19.600000381469727,28.440000534057617 C18.579999923706055,27.979999542236328 17.56999969482422,29.1200008392334 18.18000030517578,30.06999969482422 C18.18000030517578,30.06999969482422 33.84000015258789,54.43000030517578 33.84000015258789,54.43000030517578 C34.45000076293945,55.380001068115234 35.90999984741211,54.939998626708984 35.90999984741211,53.81999969482422 C35.90999984741211,53.81999969482422 35.90999984741211,37.70000076293945 35.90999984741211,37.70000076293945 C35.90999984741211,36.599998474121094 35.27000045776367,35.599998474121094 34.27000045776367,35.13999938964844 C34.27000045776367,35.13999938964844 19.59000015258789,28.450000762939453 19.59000015258789,28.450000762939453 C19.59000015258789,28.450000762939453 19.600000381469727,28.440000534057617 19.600000381469727,28.440000534057617z"></path></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+73
View File
@@ -0,0 +1,73 @@
import logging
from importlib.metadata import version
from pyfragment.client import FragmentClient
from pyfragment.domains.ads.models import AdsRechargeResult, AdsTopupResult
from pyfragment.domains.anonymous_numbers.models import LoginCodeResult, TerminateSessionsResult
from pyfragment.domains.giveaways.models import PremiumGiveawayResult, StarsGiveawayResult
from pyfragment.domains.marketplace.models import GiftsResult, NumbersResult, UsernamesResult
from pyfragment.domains.purchases.models import PremiumResult, StarsResult
from pyfragment.enums import ApiProvider, PaymentMethod, WalletVersion
from pyfragment.exceptions import (
AlreadySubscribedError,
AnonymousNumberError,
ClientError,
ConfigurationError,
CookieError,
FragmentAPIError,
FragmentError,
FragmentPageError,
OperationError,
ParseError,
TransactionError,
UnexpectedError,
UserNotFoundError,
VerificationError,
WalletError,
)
from pyfragment.services.cookies import CookieResult, get_cookies_from_browser
from pyfragment.services.tonapi.models import WalletInfo
logging.getLogger("pyfragment").addHandler(logging.NullHandler())
__version__: str = version("pyfragment")
__all__ = [
"__version__",
"FragmentClient",
# results
"StarsResult",
"StarsGiveawayResult",
"PremiumResult",
"PremiumGiveawayResult",
"WalletInfo",
"AdsTopupResult",
"AdsRechargeResult",
"CookieResult",
"GiftsResult",
"LoginCodeResult",
"NumbersResult",
"TerminateSessionsResult",
"UsernamesResult",
# exceptions
"FragmentError",
"FragmentAPIError",
"FragmentPageError",
"ConfigurationError",
"AlreadySubscribedError",
"UserNotFoundError",
"WalletError",
"VerificationError",
"TransactionError",
"AnonymousNumberError",
"ClientError",
"CookieError",
"OperationError",
"ParseError",
"UnexpectedError",
# literal types
"ApiProvider",
"PaymentMethod",
"WalletVersion",
"get_cookies_from_browser",
]
+332
View File
@@ -0,0 +1,332 @@
from __future__ import annotations
from typing import Any
from pyfragment.core.constants import BASE_HEADERS, DEFAULT_TIMEOUT, FRAGMENT_BASE_URL
from pyfragment.core.validation import (
normalize_provider,
normalize_wallet_version,
parse_cookies,
validate_cookie_keys,
validate_credentials,
)
from pyfragment.domains.ads.models import AdsRechargeResult, AdsTopupResult
from pyfragment.domains.ads.service import AdsService
from pyfragment.domains.anonymous_numbers.models import LoginCodeResult, TerminateSessionsResult
from pyfragment.domains.anonymous_numbers.service import AnonymousNumbersService
from pyfragment.domains.base import raw_api_call
from pyfragment.domains.giveaways.models import PremiumGiveawayResult, StarsGiveawayResult
from pyfragment.domains.giveaways.service import GiveawaysService
from pyfragment.domains.marketplace.models import GiftsResult, NumbersResult, UsernamesResult
from pyfragment.domains.marketplace.service import MarketplaceService
from pyfragment.domains.purchases.models import PremiumResult, StarsResult
from pyfragment.domains.purchases.service import PurchasesService
from pyfragment.enums import ApiProvider, PaymentMethod, WalletVersion
from pyfragment.services.tonapi.models import WalletInfo
from pyfragment.services.tonapi.service import TonapiService
class FragmentClient:
"""
Client for the Fragment.com API.
.. note::
This library is not affiliated with, endorsed by, or in any way officially
connected with Fragment or Telegram.
Args:
seed: 12- or 24-word mnemonic phrase for the GRAM (ex TON) wallet.
api_key: API key for the chosen provider — tonconsole.com (default) or t.me/toncenter.
cookies: Fragment session cookies as a dict or JSON string.
wallet_version: Wallet contract version — ``"V4R2"`` or ``"V5R1"`` (default).
api_provider: Blockchain API provider — ``"tonapi"`` (tonconsole.com, default)
or ``"toncenter"`` (t.me/toncenter).
timeout: HTTP request timeout in seconds. Defaults to ``30.0``.
headers: Custom HTTP request headers. If omitted, :data:`BASE_HEADERS` is used.
Raises:
ConfigurationError: If ``seed``, ``api_key``, ``wallet_version``, or ``api_provider``
are missing or invalid.
CookieError: If ``cookies`` cannot be parsed or are missing required keys.
Example::
async with FragmentClient(
seed="word1 word2 ...",
api_key="AAABBB...",
cookies={"stel_ssid": "...", "stel_dt": "...", ...},
) as client:
print(await client.get_wallet())
result = await client.purchase_premium("@username", months=6)
print(result.transaction_id)
"""
def __init__(
self,
seed: str,
api_key: str,
cookies: dict[str, Any] | str,
wallet_version: str = "V5R1",
api_provider: str = "tonapi",
timeout: float = DEFAULT_TIMEOUT,
headers: dict[str, str] | None = None,
) -> None:
validate_credentials(seed, api_key)
provider = normalize_provider(api_provider)
parsed_cookies = parse_cookies(cookies)
validate_cookie_keys(parsed_cookies)
version = normalize_wallet_version(wallet_version)
self.seed: str = seed.strip()
self.api_key: str = api_key.strip()
self.api_provider: ApiProvider = provider
self.cookies: dict[str, Any] = parsed_cookies
self.wallet_version: WalletVersion = version
self.timeout: float = timeout
self.headers: dict[str, str] = headers if headers is not None else BASE_HEADERS
self.marketplace = MarketplaceService(self)
self.purchases = PurchasesService(self)
self.giveaways = GiveawaysService(self)
self.tonapi = TonapiService(self)
self.anonymous_numbers = AnonymousNumbersService(self)
self.ads = AdsService(self)
async def __aenter__(self) -> FragmentClient:
return self
async def __aexit__(self, *_: object) -> None:
pass
def __repr__(self) -> str:
return f"FragmentClient(wallet_version='{self.wallet_version}', api_provider='{self.api_provider}', cookies={len(self.cookies)} keys)"
async def purchase_premium(
self,
username: str,
months: int,
show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumResult:
"""Gift Telegram Premium to a user.
Args:
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 — defaults to ``PaymentMethod.GRAM``.
Returns:
:class:`PremiumResult` with ``transaction_id``, ``username``, and ``amount``.
"""
return await self.purchases.purchase_premium(username, months, show_sender=show_sender, payment_method=payment_method)
async def purchase_stars(
self,
username: str,
amount: int,
show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsResult:
"""Send Telegram Stars to a user.
Args:
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 — defaults to ``PaymentMethod.GRAM``.
Returns:
:class:`StarsResult` with ``transaction_id``, ``username``, and ``amount``.
"""
return await self.purchases.purchase_stars(username, amount, show_sender=show_sender, payment_method=payment_method)
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 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_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.
Args:
account: Channel or bot username the Ads account is linked to (e.g. ``"@mychannel"``).
amount: Amount in GRAM (ex TON) — integer from ``1`` to ``1 000 000 000``.
Returns:
:class:`AdsRechargeResult` with ``transaction_id`` and ``amount``.
"""
return await self.ads.recharge_ads(account, amount)
async def get_wallet(self) -> WalletInfo:
"""Return the address, state, and balances of the wallet.
Returns:
:class:`WalletInfo` with ``address``, ``state``, ``gram_balance``, and ``usdt_balance``.
"""
return await self.tonapi.get_wallet()
async def giveaway_stars(
self,
channel: str,
winners: int,
amount: int,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsGiveawayResult:
"""Run a Telegram Stars giveaway for a channel.
Args:
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 — defaults to ``PaymentMethod.GRAM``.
Returns:
:class:`StarsGiveawayResult` with ``transaction_id``, ``channel``, ``winners``, and ``amount``.
"""
return await self.giveaways.giveaway_stars(channel, winners, amount, payment_method=payment_method)
async def giveaway_premium(
self,
channel: str,
winners: int,
months: int = 3,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumGiveawayResult:
"""Run a Telegram Premium giveaway for a channel.
Args:
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 — defaults to ``PaymentMethod.GRAM``.
Returns:
:class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``, ``winners``, and ``amount``.
"""
return await self.giveaways.giveaway_premium(channel, winners, months, payment_method=payment_method)
async def get_login_code(self, number: str) -> LoginCodeResult:
"""Fetch the current pending login code for an anonymous number.
Args:
number: Phone number with or without leading ``+``.
Returns:
:class:`LoginCodeResult` with ``number``, ``code`` (``None`` if none pending),
and ``active_sessions`` count.
"""
return await self.anonymous_numbers.get_login_code(number)
async def toggle_login_codes(self, number: str, can_receive: bool) -> None:
"""Enable or disable login code delivery for an anonymous number.
Args:
number: Phone number with or without leading ``+``.
can_receive: ``True`` to allow receiving codes, ``False`` to block them.
"""
return await self.anonymous_numbers.toggle_login_codes(number, can_receive)
async def terminate_sessions(self, number: str) -> TerminateSessionsResult:
"""Terminate all active Telegram sessions for an anonymous number.
Args:
number: Phone number with or without leading ``+``.
Returns:
:class:`TerminateSessionsResult` with ``number`` and ``message``.
Raises:
AnonymousNumberError: If the number is not owned or has no active sessions.
"""
return await self.anonymous_numbers.terminate_sessions(number)
async def search_usernames(
self,
query: str = "",
sort: str | None = None,
filter: str | None = None,
offset_id: str | None = None,
) -> UsernamesResult:
"""Search the Fragment marketplace for Telegram usernames.
Args:
query: Search text. Omit or pass ``""`` to browse all.
sort: ``"price_desc"``, ``"price_asc"``, ``"listed"``, or ``"ending"``.
filter: ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` (available).
offset_id: Pass :attr:`UsernamesResult.next_offset_id` to fetch the next page.
Returns:
:class:`UsernamesResult` with ``items`` and ``next_offset_id``.
"""
return await self.marketplace.search_usernames(query, sort=sort, filter=filter, offset_id=offset_id)
async def search_numbers(
self,
query: str = "",
sort: str | None = None,
filter: str | None = None,
offset_id: str | None = None,
) -> NumbersResult:
"""Search the Fragment marketplace for anonymous Telegram numbers.
Args:
query: Search text. Omit or pass ``""`` to browse all.
sort: ``"price_desc"``, ``"price_asc"``, ``"listed"``, or ``"ending"``.
filter: ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` (available).
offset_id: Pass :attr:`NumbersResult.next_offset_id` to fetch the next page.
Returns:
:class:`NumbersResult` with ``items`` and ``next_offset_id``.
"""
return await self.marketplace.search_numbers(query, sort=sort, filter=filter, offset_id=offset_id)
async def search_gifts(
self,
query: str = "",
collection: str | None = None,
sort: str | None = None,
filter: str | None = None,
view: str | None = None,
attr: dict[str, list[str]] | None = None,
offset: int | None = None,
) -> GiftsResult:
"""Search the Fragment gifts marketplace.
Args:
query: Search text. Omit or pass ``""`` to browse all.
collection: Gift collection slug (e.g. ``"artisanbrick"``).
sort: ``"price_desc"``, ``"price_asc"``, ``"listed"``, or ``"ending"``.
filter: ``"auction"``, ``"sale"``, ``"sold"``, or ``""`` (available).
view: Active attribute tab name (e.g. ``"Model"``).
attr: Attribute filters — e.g. ``{"Model": ["Foosball"], "Backdrop": ["Celtic Blue"]}``.
offset: Pass :attr:`GiftsResult.next_offset` to fetch the next page.
Returns:
:class:`GiftsResult` with ``items`` and ``next_offset``.
"""
return await self.marketplace.search_gifts(
query, collection=collection, sort=sort, filter=filter, view=view, attr=attr, offset=offset
)
async def call(
self, method: str, data: dict[str, Any] | None = None, *, page_url: str = FRAGMENT_BASE_URL
) -> dict[str, Any]:
"""Send a raw request to the Fragment API.
Args:
method: Fragment API method name, e.g. ``"searchPremiumGiftRecipient"``.
data: Additional form-data fields.
page_url: Fragment page URL to derive the API hash. Defaults to ``FRAGMENT_BASE_URL``.
Returns:
Raw parsed JSON response as a dict.
"""
return await raw_api_call(self.cookies, self.timeout, method, data, page_url, self.headers)
View File
+83
View File
@@ -0,0 +1,83 @@
from __future__ import annotations
from typing import Any
FRAGMENT_DOMAIN: str = "fragment.com"
FRAGMENT_BASE_URL: str = f"https://{FRAGMENT_DOMAIN}"
STARS_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/buy"
STARS_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/giveaway"
PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift"
PREMIUM_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/giveaway"
ADS_TOPUP_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup"
NUMBERS_PAGE: str = f"{FRAGMENT_BASE_URL}/numbers"
GIFTS_PAGE: str = f"{FRAGMENT_BASE_URL}/gifts"
DEFAULT_TIMEOUT: float = 30.0
# Fragment cookie keys required for authenticated API calls
REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token")
BASE_HEADERS: dict[str, str] = {
"accept": "application/json, text/javascript, */*; q=0.01",
"accept-language": "en-US,en;q=0.9,uk;q=0.8,ru;q=0.7",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"origin": FRAGMENT_BASE_URL,
"priority": "u=1, i",
"sec-ch-ua": '"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"',
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": '"Android"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": (
"Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36"
),
"x-requested-with": "XMLHttpRequest",
}
# USDT-TON jetton master contract address on GRAM (ex TON) mainnet
USDT_GRAM_MASTER_ADDRESS: str = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"
# TON Connect device info sent during wallet connection handshake
DEVICE_INFO: dict[str, Any] = {
"platform": "iphone",
"appName": "Tonkeeper",
"appVersion": "26.05.0",
"maxProtocolVersion": 2,
"features": [
"SendTransaction",
{"name": "SendTransaction", "maxMessages": 255},
{"name": "SignData", "types": ["text", "binary", "cell"]},
],
}
# Stars: direct purchase per transaction
STARS_PURCHASE_MIN: int = 50
STARS_PURCHASE_MAX: int = 10_000_000
# Stars: giveaway amount per winner
STARS_GIVEAWAY_MIN: int = 500
STARS_GIVEAWAY_MAX: int = 1_000_000
# Stars giveaway winner count
STARS_WINNERS_MIN: int = 1
STARS_WINNERS_MAX: int = 15
# Premium giveaway winner count
PREMIUM_WINNERS_MIN: int = 1
PREMIUM_WINNERS_MAX: int = 24_000
# GRAM (ex TON) topup / Ads recharge amount
GRAM_TOPUP_MIN: int = 1
GRAM_TOPUP_MAX: int = 1_000_000_000
# Minimum wallet balances required before broadcasting a transaction
MIN_GRAM_BALANCE: float = 0.33
MIN_USDT_BALANCE: float = 0.75
# Premium subscription durations (months)
PREMIUM_MONTHS_VALID: frozenset[int] = frozenset({3, 6, 12})
# Mnemonic phrase valid word counts
MNEMONIC_WORD_COUNTS_VALID: frozenset[int] = frozenset({12, 24})
+67
View File
@@ -0,0 +1,67 @@
from __future__ import annotations
import asyncio
import random
import re
from typing import Any, cast
from curl_cffi.requests import AsyncSession, Response
from pyfragment.core.constants import FRAGMENT_BASE_URL
from pyfragment.exceptions import FragmentPageError, ParseError
async def get_fragment_hash(
session: AsyncSession[Any],
headers: dict[str, str],
page_url: str,
) -> str:
# Derive the natural referer: strip the last path segment (e.g. /stars/buy → /stars)
parent_url = page_url.rsplit("/", 1)[0] or FRAGMENT_BASE_URL
page_headers = {k: v for k, v in headers.items() if k not in ("content-type", "origin")}
page_headers["referer"] = parent_url
page_headers["x-aj-referer"] = parent_url
page_headers.pop("x-aj-referer", None)
page_headers.pop("x-requested-with", None)
response = await session.get(page_url, headers=page_headers)
if response.status_code != 200:
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 FragmentPageError(FragmentPageError.NOT_FOUND.format(url=page_url))
return match.group(1)
def parse_json_response(response: Response, context: str) -> dict[str, Any]:
try:
return cast(dict[str, Any], response.json()) # type: ignore[no-untyped-call]
except Exception as exc:
raise ParseError(ParseError.UNPARSEABLE.format(context=context, exc=exc)) from exc
async def fragment_request(
session: AsyncSession[Any],
fragment_hash: str,
headers: dict[str, str],
data: dict[str, Any],
) -> dict[str, Any]:
for attempt in range(3):
resp = await session.post(
f"{FRAGMENT_BASE_URL}/api?hash={fragment_hash}",
headers=headers,
data=data,
)
if resp.status_code == 429 and attempt < 2:
await asyncio.sleep(1 + attempt + random.uniform(0, 0.5))
continue
if resp.status_code != 200:
raise FragmentPageError(
FragmentPageError.BAD_STATUS.format(status=resp.status_code, url=f"{FRAGMENT_BASE_URL}/api")
)
return parse_json_response(resp, data.get("method", "request"))
raise FragmentPageError(FragmentPageError.BAD_STATUS.format(status=429, url=f"{FRAGMENT_BASE_URL}/api"))
+58
View File
@@ -0,0 +1,58 @@
from __future__ import annotations
import json
from typing import Any, cast
from pyfragment.core.constants import MNEMONIC_WORD_COUNTS_VALID, REQUIRED_COOKIE_KEYS
from pyfragment.enums import ApiProvider, WalletVersion
from pyfragment.exceptions import ConfigurationError, CookieError
def parse_cookies(cookies: dict[str, Any] | str) -> dict[str, Any]:
if isinstance(cookies, str):
try:
cookies = json.loads(cookies)
except Exception as exc:
raise CookieError(CookieError.READ_FAILED.format(exc=exc)) from exc
return cast(dict[str, Any], cookies)
def validate_cookie_keys(cookies: dict[str, Any]) -> None:
missing = [k for k in REQUIRED_COOKIE_KEYS if not str(cookies.get(k, "")).strip()]
if missing:
raise CookieError(CookieError.MISSING_KEYS.format(keys=", ".join(missing)))
def normalize_provider(api_provider: str) -> ApiProvider:
try:
return ApiProvider(api_provider.strip().lower())
except ValueError:
raise ConfigurationError(
ConfigurationError.UNSUPPORTED_PROVIDER.format(
provider=api_provider,
supported=", ".join(sorted(p.value for p in ApiProvider)),
)
)
def normalize_wallet_version(wallet_version: str) -> WalletVersion:
version = wallet_version.strip().upper()
try:
return WalletVersion(version)
except ValueError:
raise ConfigurationError(
ConfigurationError.UNSUPPORTED_VERSION.format(
version=version,
supported=", ".join(sorted(m.value for m in WalletVersion)),
)
)
def validate_credentials(seed: str, api_key: str) -> None:
missing = [name for name, val in (("seed", seed), ("api_key", api_key)) if not val or not str(val).strip()]
if missing:
raise ConfigurationError(ConfigurationError.MISSING_VARS.format(keys=", ".join(missing)))
word_count = len(seed.split())
if word_count not in MNEMONIC_WORD_COUNTS_VALID:
raise ConfigurationError(ConfigurationError.INVALID_MNEMONIC.format(count=word_count))
+1
View File
@@ -0,0 +1 @@
"""Domain-level helpers for Fragment operations."""
+5
View File
@@ -0,0 +1,5 @@
from pyfragment.domains.ads.recharge import recharge_ads
from pyfragment.domains.ads.service import AdsService
from pyfragment.domains.ads.tonup import topup_gram
__all__ = ["AdsService", "recharge_ads", "topup_gram"]
+22
View File
@@ -0,0 +1,22 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class AdsTopupResult:
transaction_id: str
username: str
amount: int
def __repr__(self) -> str:
return f"AdsTopupResult(username='{self.username}', amount={self.amount} GRAM (ex TON), tx='{self.transaction_id}')"
@dataclass
class AdsRechargeResult:
transaction_id: str
amount: int
def __repr__(self) -> str:
return f"AdsRechargeResult(amount={self.amount} GRAM (ex TON), tx='{self.transaction_id}')"
+54
View File
@@ -0,0 +1,54 @@
from __future__ import annotations
import json
import logging
from typing import TYPE_CHECKING
from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE_INFO, GRAM_TOPUP_MAX, GRAM_TOPUP_MIN
from pyfragment.domains.ads.models import AdsRechargeResult
from pyfragment.exceptions import ConfigurationError, FragmentAPIError, FragmentError, UnexpectedError, VerificationError
from pyfragment.services.tonapi.account import get_account_info
from pyfragment.services.tonapi.transaction import process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def recharge_ads(client: FragmentClient, account: str, amount: int) -> AdsRechargeResult:
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)
result = await client.call("initAdsRechargeRequest", {"account": account, "amount": amount}, page_url=ADS_TOPUP_PAGE)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Ads recharge"))
account_info = await get_account_info(client)
transaction = await client.call(
"getAdsRechargeLink",
{
"account": json.dumps(account_info),
"device": json.dumps(DEVICE_INFO),
"transaction": 1,
"id": req_id,
},
page_url=ADS_TOPUP_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(client, transaction)
return AdsRechargeResult(transaction_id=tx_hash, amount=amount)
except FragmentError as exc:
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 GRAM (ex TON) due to an unexpected error", account, amount)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+19
View File
@@ -0,0 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pyfragment.domains.ads.models import AdsRechargeResult, AdsTopupResult
from pyfragment.domains.ads.recharge import recharge_ads
from pyfragment.domains.ads.tonup import topup_gram
from pyfragment.domains.base import BaseService
if TYPE_CHECKING:
pass
class AdsService(BaseService):
async def recharge_ads(self, account: str, amount: int) -> AdsRechargeResult:
return await recharge_ads(self._client, account, amount)
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)
+73
View File
@@ -0,0 +1,73 @@
from __future__ import annotations
import json
import logging
from typing import TYPE_CHECKING
from pyfragment.core.constants import ADS_TOPUP_PAGE, DEVICE_INFO, GRAM_TOPUP_MAX, GRAM_TOPUP_MIN
from pyfragment.domains.ads.models import AdsTopupResult
from pyfragment.domains.payments import parse_required_payment_amount
from pyfragment.exceptions import (
ConfigurationError,
FragmentAPIError,
FragmentError,
UnexpectedError,
UserNotFoundError,
VerificationError,
)
from pyfragment.services.tonapi.account import get_account_info
from pyfragment.services.tonapi.transaction import process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
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)
result = await client.call("searchAdsTopupRecipient", {"query": username}, page_url=ADS_TOPUP_PAGE)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
result = await client.call("initAdsTopupRequest", {"recipient": recipient, "amount": amount}, page_url=ADS_TOPUP_PAGE)
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="GRAM (ex TON) topup"))
account = await get_account_info(client)
transaction = await client.call(
"getAdsTopupLink",
{
"account": json.dumps(account),
"device": json.dumps(DEVICE_INFO),
"transaction": 1,
"id": req_id,
"show_sender": int(show_sender),
},
page_url=ADS_TOPUP_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(client, transaction, required_payment_amount=required_payment_amount)
return AdsTopupResult(transaction_id=tx_hash, username=username, amount=amount)
except FragmentError as exc:
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 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
@@ -0,0 +1,12 @@
from pyfragment.domains.anonymous_numbers.models import LoginCodeResult, TerminateSessionsResult
from pyfragment.domains.anonymous_numbers.number import get_login_code, terminate_sessions, toggle_login_codes
from pyfragment.domains.anonymous_numbers.service import AnonymousNumbersService
__all__ = [
"AnonymousNumbersService",
"LoginCodeResult",
"TerminateSessionsResult",
"get_login_code",
"terminate_sessions",
"toggle_login_codes",
]
@@ -0,0 +1,23 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class LoginCodeResult:
number: str
code: str | None
active_sessions: int
def __repr__(self) -> str:
code_str = f"'{self.code}'" if self.code else "None"
return f"LoginCodeResult(number='{self.number}', code={code_str}, active_sessions={self.active_sessions})"
@dataclass
class TerminateSessionsResult:
number: str
message: str | None
def __repr__(self) -> str:
return f"TerminateSessionsResult(number='{self.number}', message={self.message!r})"
@@ -0,0 +1,114 @@
from __future__ import annotations
import html
import logging
from typing import TYPE_CHECKING
from pyfragment.core.constants import NUMBERS_PAGE
from pyfragment.domains.anonymous_numbers.models import LoginCodeResult, TerminateSessionsResult
from pyfragment.domains.anonymous_numbers.parser import parse_login_code
from pyfragment.exceptions import AnonymousNumberError, FragmentAPIError, FragmentError, UnexpectedError
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
def _strip_plus(number: str) -> str:
return number.lstrip("+") if isinstance(number, str) else number
async def get_login_code(client: FragmentClient, number: str) -> LoginCodeResult:
try:
clean = _strip_plus(number)
result = await client.call(
"updateLoginCodes",
{"number": clean, "lt": "0", "from_app": "1"},
page_url=NUMBERS_PAGE,
)
if result.get("html"):
code, active_sessions = parse_login_code(result["html"])
else:
code, active_sessions = None, 0
return LoginCodeResult(number=number, code=code, active_sessions=active_sessions)
except FragmentError as exc:
logger.error("Failed to get login code for number '%s': %s", number, exc, exc_info=True)
raise
except Exception as exc:
logger.exception("Failed to get login code for number '%s' due to an unexpected error", number)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
async def toggle_login_codes(client: FragmentClient, number: str, can_receive: bool) -> None:
try:
clean = _strip_plus(number)
result = await client.call(
"toggleLoginCodes",
{"number": clean, "can_receive": 1 if can_receive else 0},
page_url=NUMBERS_PAGE,
)
if result.get("error"):
raise FragmentAPIError(html.unescape(result["error"]))
except FragmentError as exc:
logger.error(
"Failed to toggle login code delivery for number '%s' (can_receive=%s): %s",
number,
can_receive,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to toggle login code delivery for number '%s' (can_receive=%s) due to an unexpected error",
number,
can_receive,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
async def terminate_sessions(client: FragmentClient, number: str) -> TerminateSessionsResult:
try:
clean = _strip_plus(number)
confirmation = await client.call(
"terminatePhoneSessions",
{"number": clean},
page_url=NUMBERS_PAGE,
)
if confirmation.get("error"):
raise AnonymousNumberError(
AnonymousNumberError.TERMINATE_FAILED.format(number=number, error=html.unescape(confirmation["error"]))
)
terminate_hash = confirmation.get("terminate_hash")
if not terminate_hash:
raise AnonymousNumberError(AnonymousNumberError.NOT_OWNED.format(number=number))
result = await client.call(
"terminatePhoneSessions",
{"number": clean, "terminate_hash": terminate_hash},
page_url=NUMBERS_PAGE,
)
if result.get("error"):
raise AnonymousNumberError(
AnonymousNumberError.TERMINATE_FAILED.format(number=number, error=html.unescape(result["error"]))
)
return TerminateSessionsResult(number=number, message=result.get("msg"))
except FragmentError as exc:
logger.error("Failed to terminate sessions for number '%s': %s", number, exc, exc_info=True)
raise
except Exception as exc:
logger.exception("Failed to terminate sessions for number '%s' due to an unexpected error", number)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
@@ -0,0 +1,13 @@
from __future__ import annotations
import re
CODE_RE = re.compile(r'class="[^"]*table-cell-value[^"]*"[^>]*>([^<]+)<')
ROW_RE = re.compile(r"<tr[\s>]")
def parse_login_code(html: str) -> tuple[str | None, int]:
match = CODE_RE.search(html)
code = match.group(1).strip() if match else None
active_sessions = len(ROW_RE.findall(html))
return code, active_sessions
@@ -0,0 +1,21 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pyfragment.domains.anonymous_numbers.models import LoginCodeResult, TerminateSessionsResult
from pyfragment.domains.anonymous_numbers.number import get_login_code, terminate_sessions, toggle_login_codes
from pyfragment.domains.base import BaseService
if TYPE_CHECKING:
pass
class AnonymousNumbersService(BaseService):
async def get_login_code(self, number: str) -> LoginCodeResult:
return await get_login_code(self._client, number)
async def toggle_login_codes(self, number: str, can_receive: bool) -> None:
return await toggle_login_codes(self._client, number, can_receive)
async def terminate_sessions(self, number: str) -> TerminateSessionsResult:
return await terminate_sessions(self._client, number)
+42
View File
@@ -0,0 +1,42 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from curl_cffi.requests import AsyncSession
from pyfragment.core.constants import BASE_HEADERS
from pyfragment.core.transport import fragment_request, get_fragment_hash
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def raw_api_call(
cookies: dict[str, Any],
timeout: float,
method: str,
data: dict[str, Any] | None,
page_url: str,
headers: dict[str, str] | None = None,
) -> dict[str, Any]:
base = headers if headers is not None else BASE_HEADERS
payload = {"method": method, **(data or {})}
call_headers = {**base, "referer": page_url, "x-aj-referer": page_url}
logger.debug("Starting Fragment API call '%s' on %s", method, page_url)
try:
async with AsyncSession(cookies=cookies, timeout=timeout, impersonate="chrome") as session:
fragment_hash = await get_fragment_hash(session, call_headers, page_url)
response = await fragment_request(session, fragment_hash, call_headers, payload)
logger.debug("Completed Fragment API call '%s' with response keys: %s", method, sorted(response.keys()))
return response
except Exception:
logger.exception("Failed to call Fragment API method '%s' on %s", method, page_url)
raise
class BaseService:
def __init__(self, client: FragmentClient) -> None:
self._client = client
+11
View File
@@ -0,0 +1,11 @@
from pyfragment.domains.giveaways.giveaway import giveaway_premium, giveaway_stars
from pyfragment.domains.giveaways.models import PremiumGiveawayResult, StarsGiveawayResult
from pyfragment.domains.giveaways.service import GiveawaysService
__all__ = [
"GiveawaysService",
"PremiumGiveawayResult",
"StarsGiveawayResult",
"giveaway_premium",
"giveaway_stars",
]
+242
View File
@@ -0,0 +1,242 @@
from __future__ import annotations
import json
import logging
import random
from typing import TYPE_CHECKING
from pyfragment.core.constants import (
DEVICE_INFO,
PREMIUM_GIVEAWAY_PAGE,
PREMIUM_MONTHS_VALID,
PREMIUM_WINNERS_MAX,
PREMIUM_WINNERS_MIN,
STARS_GIVEAWAY_MAX,
STARS_GIVEAWAY_MIN,
STARS_GIVEAWAY_PAGE,
STARS_WINNERS_MAX,
STARS_WINNERS_MIN,
)
from pyfragment.domains.giveaways.models import PremiumGiveawayResult, StarsGiveawayResult
from pyfragment.domains.payments import parse_required_payment_amount
from pyfragment.enums import PaymentMethod
from pyfragment.exceptions import (
ConfigurationError,
FragmentAPIError,
FragmentError,
UnexpectedError,
UserNotFoundError,
VerificationError,
)
from pyfragment.services.tonapi.account import get_account_info
from pyfragment.services.tonapi.transaction import process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
def _state_nonce() -> str:
# Fragment expects a pseudo-random nonce-like dh value in giveaway state updates.
return str(random.randint(100_000_000, 2_147_483_647))
async def giveaway_stars(
client: FragmentClient,
channel: str,
winners: int,
amount: int,
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)
if not isinstance(amount, int) or not (STARS_GIVEAWAY_MIN <= amount <= STARS_GIVEAWAY_MAX):
raise ConfigurationError(ConfigurationError.INVALID_STARS_PER_WINNER)
if not any(payment_method == m for m in PaymentMethod):
raise ConfigurationError(
ConfigurationError.INVALID_PAYMENT_METHOD.format(
method=payment_method,
supported=", ".join(sorted(m.value for m in PaymentMethod)),
)
)
try:
result = await client.call("searchStarsGiveawayRecipient", {"query": channel}, page_url=STARS_GIVEAWAY_PAGE)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
await client.call(
"updateStarsGiveawayState",
{"mode": "new", "lv": "false", "dh": _state_nonce()},
page_url=STARS_GIVEAWAY_PAGE,
)
await client.call(
"updateStarsGiveawayPrices",
{"quantity": winners, "stars": amount},
page_url=STARS_GIVEAWAY_PAGE,
)
result = await client.call(
"initGiveawayStarsRequest",
{
"recipient": recipient,
"quantity": str(winners),
"stars": str(amount),
"payment_method": payment_method,
},
page_url=STARS_GIVEAWAY_PAGE,
)
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="Stars giveaway"))
account = await get_account_info(client)
transaction = await client.call(
"getGiveawayStarsLink",
{
"account": json.dumps(account),
"device": json.dumps(DEVICE_INFO),
"transaction": 1,
"id": req_id,
},
page_url=STARS_GIVEAWAY_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(
client,
transaction,
payment_method=payment_method,
required_payment_amount=required_payment_amount,
)
return StarsGiveawayResult(transaction_id=tx_hash, channel=channel, winners=winners, amount=amount)
except FragmentError as exc:
logger.error(
"Failed to run Stars giveaway for channel '%s' (winners=%s, amount=%s, payment_method='%s'): %s",
channel,
winners,
amount,
payment_method,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to run Stars giveaway for channel '%s' (winners=%s, amount=%s, payment_method='%s') due to an unexpected error",
channel,
winners,
amount,
payment_method,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
async def giveaway_premium(
client: FragmentClient,
channel: str,
winners: int,
months: int = 3,
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)
if months not in PREMIUM_MONTHS_VALID:
raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
if not any(payment_method == m for m in PaymentMethod):
raise ConfigurationError(
ConfigurationError.INVALID_PAYMENT_METHOD.format(
method=payment_method,
supported=", ".join(sorted(m.value for m in PaymentMethod)),
)
)
try:
result = await client.call(
"searchPremiumGiveawayRecipient",
{"query": channel, "quantity": winners, "months": months},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
await client.call(
"updatePremiumGiveawayState",
{
"mode": "new",
"lv": "false",
"dh": _state_nonce(),
"quantity": "",
},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
await client.call(
"updatePremiumGiveawayPrices",
{"quantity": winners},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
result = await client.call(
"initGiveawayPremiumRequest",
{
"recipient": recipient,
"quantity": str(winners),
"months": str(months),
"payment_method": payment_method,
},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
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="Premium giveaway"))
account = await get_account_info(client)
transaction = await client.call(
"getGiveawayPremiumLink",
{
"account": json.dumps(account),
"device": json.dumps(DEVICE_INFO),
"transaction": 1,
"id": req_id,
},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(
client,
transaction,
payment_method=payment_method,
required_payment_amount=required_payment_amount,
)
return PremiumGiveawayResult(transaction_id=tx_hash, channel=channel, winners=winners, amount=months)
except FragmentError as exc:
logger.error(
"Failed to run Premium giveaway for channel '%s' (winners=%s, months=%s, payment_method='%s'): %s",
channel,
winners,
months,
payment_method,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to run Premium giveaway for channel '%s' (winners=%s, months=%s, payment_method='%s') due to an unexpected error",
channel,
winners,
months,
payment_method,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class StarsGiveawayResult:
transaction_id: str
channel: str
winners: int
amount: int
def __repr__(self) -> str:
return (
f"StarsGiveawayResult(channel='{self.channel}', winners={self.winners}, "
f"amount={self.amount} stars per winner, tx='{self.transaction_id}')"
)
@dataclass
class PremiumGiveawayResult:
transaction_id: str
channel: str
winners: int
amount: int
def __repr__(self) -> str:
return (
f"PremiumGiveawayResult(channel='{self.channel}', winners={self.winners}, "
f"amount={self.amount} months per winner, tx='{self.transaction_id}')"
)
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pyfragment.domains.base import BaseService
from pyfragment.domains.giveaways.giveaway import giveaway_premium, giveaway_stars
from pyfragment.domains.giveaways.models import PremiumGiveawayResult, StarsGiveawayResult
from pyfragment.enums import PaymentMethod
if TYPE_CHECKING:
pass
class GiveawaysService(BaseService):
async def giveaway_stars(
self,
channel: str,
winners: int,
amount: int,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsGiveawayResult:
return await giveaway_stars(self._client, channel, winners, amount, payment_method=payment_method)
async def giveaway_premium(
self,
channel: str,
winners: int,
months: int = 3,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumGiveawayResult:
return await giveaway_premium(self._client, channel, winners, months, payment_method=payment_method)
@@ -0,0 +1,13 @@
from pyfragment.domains.marketplace.models import GiftsResult, NumbersResult, UsernamesResult
from pyfragment.domains.marketplace.search import search_gifts, search_numbers, search_usernames
from pyfragment.domains.marketplace.service import MarketplaceService
__all__ = [
"GiftsResult",
"MarketplaceService",
"NumbersResult",
"UsernamesResult",
"search_gifts",
"search_numbers",
"search_usernames",
]
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
@dataclass
class UsernamesResult:
items: list[dict[str, Any]]
next_offset_id: str | None
def __repr__(self) -> str:
return f"UsernamesResult(items={len(self.items)}, next_offset_id={self.next_offset_id!r})"
@dataclass
class NumbersResult:
items: list[dict[str, Any]]
next_offset_id: str | None
def __repr__(self) -> str:
return f"NumbersResult(items={len(self.items)}, next_offset_id={self.next_offset_id!r})"
@dataclass
class GiftsResult:
items: list[dict[str, Any]]
next_offset: int | None
def __repr__(self) -> str:
return f"GiftsResult(items={len(self.items)}, next_offset={self.next_offset!r})"
+95
View File
@@ -0,0 +1,95 @@
from __future__ import annotations
import re
from typing import Any
ROW_BLOCK_RE = re.compile(r'<tr\b[^>]*class="[^"]*tm-row-selectable[^"]*"[^>]*>(.*?)</tr>', re.DOTALL)
HREF_RE = re.compile(r'href="(/(?:username|number|nft)/([^"]+))"')
VALUE_RE = re.compile(r'class="[^"]*tm-value[^"]*"[^>]*>\s*([^<]+?)\s*<')
PRICE_RE = re.compile(r"icon-before\s+icon-ton[^>]*>\s*([0-9][^<]*?)\s*<")
DATETIME_RE = re.compile(r'<time[^>]+datetime="([^"]+)"[^>]*data-relative="text"[^>]*>')
DATETIME_SHORT_RE = re.compile(r'<time[^>]+datetime="([^"]+)"[^>]*data-relative="short-text"[^>]*>')
NUMERIC_RE = re.compile(r"^\+?[\d,. ]+$")
GRID_ITEM_RE = re.compile(r'<a\b[^>]*class="[^"]*tm-grid-item[^"]*"[^>]*>(.*?)</a>', re.DOTALL)
GRID_HREF_RE = re.compile(r'href="(/gift/([^?"]+))')
GRID_NAME_RE = re.compile(r'class="item-name">([^<]+)<')
GRID_NUM_RE = re.compile(r'class="item-num">[^#]*#(\w+)<')
GRID_PRICE_RE = re.compile(r'class="[^"]*tm-grid-item-value[^"]*icon-ton[^"]*"[^>]*>\s*([0-9][^<]*?)\s*<')
GRID_STATUS_RE = re.compile(r'class="[^"]*tm-grid-item-status[^"]*"[^>]*>\s*([^<]+?)\s*<')
GRID_DATETIME_RE = re.compile(r'<time[^>]+datetime="([^"]+)"')
def parse_auction_rows(html: str) -> list[dict[str, Any]]:
items: list[dict[str, Any]] = []
for row_match in ROW_BLOCK_RE.finditer(html):
row = row_match.group(1)
href_m = HREF_RE.search(row)
if not href_m:
continue
slug = href_m.group(1).lstrip("/")
values = [m.group(1).strip() for m in VALUE_RE.finditer(row)]
name = values[0] if values else slug
status: str | None = None
for v in values[1:]:
if v and v not in ("Unknown",) and not v.startswith("@") and not NUMERIC_RE.match(v):
status = v
break
price_m = PRICE_RE.search(row)
price: str | None = None
if price_m:
raw_price = price_m.group(1).strip().replace(",", "")
try:
price = f"{float(raw_price):.2f}"
except ValueError:
price = raw_price
time_m = DATETIME_RE.search(row) or DATETIME_SHORT_RE.search(row)
date: str | None = time_m.group(1) if time_m else None
items.append({"slug": slug, "name": name, "status": status, "price": price, "date": date})
return items
def parse_gift_items(html: str) -> tuple[list[dict[str, Any]], int | None]:
items: list[dict[str, Any]] = []
for item_match in GRID_ITEM_RE.finditer(html):
block = item_match.group(0)
href_m = GRID_HREF_RE.search(block)
if not href_m:
continue
slug = href_m.group(1).lstrip("/")
name_m = GRID_NAME_RE.search(block)
num_m = GRID_NUM_RE.search(block)
item_name = name_m.group(1).strip() if name_m else slug
item_num = f" #{num_m.group(1)}" if num_m else ""
name = f"{item_name}{item_num}"
status_m = GRID_STATUS_RE.search(block)
status: str | None = status_m.group(1).strip() if status_m else None
price_m = GRID_PRICE_RE.search(block)
price: str | None = None
if price_m:
raw_price = price_m.group(1).strip().replace(",", "")
try:
price = f"{float(raw_price):.2f}"
except ValueError:
price = raw_price
time_m = GRID_DATETIME_RE.search(block)
date: str | None = time_m.group(1) if time_m else None
items.append({"slug": slug, "name": name, "status": status, "price": price, "date": date})
next_offset_m = re.search(r'data-next-offset="(\d+)"', html)
next_offset = int(next_offset_m.group(1)) if next_offset_m else None
return items, next_offset
+148
View File
@@ -0,0 +1,148 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from pyfragment.core.constants import FRAGMENT_BASE_URL, GIFTS_PAGE, NUMBERS_PAGE
from pyfragment.domains.marketplace.models import GiftsResult, NumbersResult, UsernamesResult
from pyfragment.domains.marketplace.parser import parse_auction_rows, parse_gift_items
from pyfragment.exceptions import FragmentAPIError, FragmentError, UnexpectedError
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
async def search_usernames(
client: FragmentClient,
query: str = "",
sort: str | None = None,
filter: str | None = None,
offset_id: str | None = None,
) -> UsernamesResult:
data: dict[str, Any] = {"type": "usernames", "query": query}
if sort is not None:
data["sort"] = sort
if filter is not None:
data["filter"] = filter
if offset_id is not None:
data["offset_id"] = offset_id
try:
result = await client.call("searchAuctions", data, page_url=FRAGMENT_BASE_URL)
if result.get("error"):
raise FragmentAPIError(result["error"])
items = parse_auction_rows(result.get("html") or "")
raw_noi = result.get("next_offset_id")
next_offset_id = str(raw_noi) if raw_noi else None
return UsernamesResult(items=items, next_offset_id=next_offset_id)
except FragmentError as exc:
logger.error(
"Failed to search usernames (query='%s', sort='%s', filter='%s', offset_id='%s'): %s",
query,
sort,
filter,
offset_id,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception("Failed to search usernames for query '%s' due to an unexpected error", query)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
async def search_numbers(
client: FragmentClient,
query: str = "",
sort: str | None = None,
filter: str | None = None,
offset_id: str | None = None,
) -> NumbersResult:
data: dict[str, Any] = {"type": "numbers", "query": query}
if sort is not None:
data["sort"] = sort
if filter is not None:
data["filter"] = filter
if offset_id is not None:
data["offset_id"] = offset_id
try:
result = await client.call("searchAuctions", data, page_url=NUMBERS_PAGE)
if result.get("error"):
raise FragmentAPIError(result["error"])
items = parse_auction_rows(result.get("html") or "")
raw_noi = result.get("next_offset_id")
next_offset_id = str(raw_noi) if raw_noi else None
return NumbersResult(items=items, next_offset_id=next_offset_id)
except FragmentError as exc:
logger.error(
"Failed to search numbers (query='%s', sort='%s', filter='%s', offset_id='%s'): %s",
query,
sort,
filter,
offset_id,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception("Failed to search numbers for query '%s' due to an unexpected error", query)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
async def search_gifts(
client: FragmentClient,
query: str = "",
collection: str | None = None,
sort: str | None = None,
filter: str | None = None,
view: str | None = None,
attr: dict[str, list[str]] | None = None,
offset: int | None = None,
) -> GiftsResult:
data: dict[str, Any] = {"type": "gifts", "query": query}
if collection is not None:
data["collection"] = collection
if sort is not None:
data["sort"] = sort
if filter is not None:
data["filter"] = filter
if view is not None:
data["view"] = view
if attr is not None:
for trait, values in attr.items():
data[f"attr[{trait}]"] = values
if offset is not None:
data["offset"] = offset
try:
result = await client.call("searchAuctions", data, page_url=GIFTS_PAGE)
if result.get("error"):
raise FragmentAPIError(result["error"])
items, next_offset = parse_gift_items(result.get("html") or "")
return GiftsResult(items=items, next_offset=next_offset)
except FragmentError as exc:
logger.error(
"Failed to search gifts (query='%s', collection='%s', sort='%s', filter='%s', view='%s', offset='%s'): %s",
query,
collection,
sort,
filter,
view,
offset,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception("Failed to search gifts for query '%s' due to an unexpected error", query)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pyfragment.domains.base import BaseService
from pyfragment.domains.marketplace.models import GiftsResult, NumbersResult, UsernamesResult
from pyfragment.domains.marketplace.search import search_gifts, search_numbers, search_usernames
if TYPE_CHECKING:
pass
class MarketplaceService(BaseService):
async def search_usernames(
self,
query: str = "",
sort: str | None = None,
filter: str | None = None,
offset_id: str | None = None,
) -> UsernamesResult:
return await search_usernames(self._client, query, sort=sort, filter=filter, offset_id=offset_id)
async def search_numbers(
self,
query: str = "",
sort: str | None = None,
filter: str | None = None,
offset_id: str | None = None,
) -> NumbersResult:
return await search_numbers(self._client, query, sort=sort, filter=filter, offset_id=offset_id)
async def search_gifts(
self,
query: str = "",
collection: str | None = None,
sort: str | None = None,
filter: str | None = None,
view: str | None = None,
attr: dict[str, list[str]] | None = None,
offset: int | None = None,
) -> GiftsResult:
return await search_gifts(
self._client, query, collection=collection, sort=sort, filter=filter, view=view, attr=attr, offset=offset
)
+11
View File
@@ -0,0 +1,11 @@
from __future__ import annotations
from typing import Any
def parse_required_payment_amount(init_response: dict[str, Any]) -> float | None:
raw_amount = init_response.get("amount")
try:
return float(str(raw_amount))
except (TypeError, ValueError):
return None
+5
View File
@@ -0,0 +1,5 @@
from pyfragment.domains.purchases.models import PremiumResult, StarsResult
from pyfragment.domains.purchases.purchase import purchase_premium, purchase_stars
from pyfragment.domains.purchases.service import PurchasesService
__all__ = ["PremiumResult", "PurchasesService", "StarsResult", "purchase_premium", "purchase_stars"]
+23
View File
@@ -0,0 +1,23 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class PremiumResult:
transaction_id: str
username: str
amount: int
def __repr__(self) -> str:
return f"PremiumResult(username='{self.username}', amount={self.amount} months, tx='{self.transaction_id}')"
@dataclass
class StarsResult:
transaction_id: str
username: str
amount: int
def __repr__(self) -> str:
return f"StarsResult(username='{self.username}', amount={self.amount} stars, tx='{self.transaction_id}')"
+209
View File
@@ -0,0 +1,209 @@
from __future__ import annotations
import json
import logging
import random
from typing import TYPE_CHECKING
from pyfragment.core.constants import (
DEVICE_INFO,
PREMIUM_MONTHS_VALID,
PREMIUM_PAGE,
STARS_PAGE,
STARS_PURCHASE_MAX,
STARS_PURCHASE_MIN,
)
from pyfragment.domains.payments import parse_required_payment_amount
from pyfragment.domains.purchases.models import PremiumResult, StarsResult
from pyfragment.enums import PaymentMethod
from pyfragment.exceptions import (
AlreadySubscribedError,
ConfigurationError,
FragmentAPIError,
FragmentError,
UnexpectedError,
UserNotFoundError,
VerificationError,
)
from pyfragment.services.tonapi.account import get_account_info
from pyfragment.services.tonapi.transaction import process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
def _state_nonce() -> str:
# Fragment accepts a pseudo-random request nonce in state update methods.
return str(random.randint(100_000_000, 2_147_483_647))
async def purchase_stars(
client: FragmentClient,
username: str,
amount: int,
show_sender: bool = True,
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)
if not any(payment_method == m for m in PaymentMethod):
raise ConfigurationError(
ConfigurationError.INVALID_PAYMENT_METHOD.format(
method=payment_method,
supported=", ".join(sorted(m.value for m in PaymentMethod)),
)
)
try:
result = await client.call("searchStarsRecipient", {"query": username, "quantity": ""}, page_url=STARS_PAGE)
if "assigned to a user" in str(result.get("error", "")).lower():
raise UserNotFoundError(UserNotFoundError.NOT_A_USER.format(username=username))
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
await client.call(
"updateStarsBuyState",
{"mode": "new", "lv": "false", "dh": _state_nonce()},
page_url=STARS_PAGE,
)
result = await client.call(
"initBuyStarsRequest",
{"recipient": recipient, "quantity": amount, "payment_method": payment_method},
page_url=STARS_PAGE,
)
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="Stars purchase"))
account = await get_account_info(client)
transaction = await client.call(
"getBuyStarsLink",
{
"account": json.dumps(account),
"device": json.dumps(DEVICE_INFO),
"transaction": 1,
"id": req_id,
"show_sender": int(show_sender),
},
page_url=STARS_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(
client,
transaction,
payment_method=payment_method,
required_payment_amount=required_payment_amount,
)
return StarsResult(transaction_id=tx_hash, username=username, amount=amount)
except FragmentError as exc:
logger.error(
"Failed to purchase %s Stars for user '%s' using '%s': %s",
amount,
username,
payment_method,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to purchase %s Stars for user '%s' using '%s' due to an unexpected error",
amount,
username,
payment_method,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
async def purchase_premium(
client: FragmentClient,
username: str,
months: int,
show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumResult:
if months not in PREMIUM_MONTHS_VALID:
raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
if not any(payment_method == m for m in PaymentMethod):
raise ConfigurationError(
ConfigurationError.INVALID_PAYMENT_METHOD.format(
method=payment_method,
supported=", ".join(sorted(m.value for m in PaymentMethod)),
)
)
try:
result = await client.call("searchPremiumGiftRecipient", {"query": username, "months": months}, page_url=PREMIUM_PAGE)
if "assigned to a user" in str(result.get("error", "")).lower():
raise UserNotFoundError(UserNotFoundError.NOT_A_USER.format(username=username))
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
await client.call(
"updatePremiumState",
{"mode": "new", "lv": "false", "dh": _state_nonce()},
page_url=PREMIUM_PAGE,
)
result = await client.call(
"initGiftPremiumRequest",
{"recipient": recipient, "months": months, "payment_method": payment_method},
page_url=PREMIUM_PAGE,
)
error_text = str(result.get("error", "")).strip().lower()
if "already subscribed to telegram premium" in error_text:
raise AlreadySubscribedError(AlreadySubscribedError.PREMIUM_ACTIVE)
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="Premium purchase"))
account = await get_account_info(client)
transaction = await client.call(
"getGiftPremiumLink",
{
"account": json.dumps(account),
"device": json.dumps(DEVICE_INFO),
"transaction": 1,
"id": req_id,
"show_sender": int(show_sender),
},
page_url=PREMIUM_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(
client,
transaction,
payment_method=payment_method,
required_payment_amount=required_payment_amount,
)
return PremiumResult(transaction_id=tx_hash, username=username, amount=months)
except FragmentError as exc:
logger.error(
"Failed to purchase %s months of Premium for user '%s' using '%s': %s",
months,
username,
payment_method,
exc,
exc_info=True,
)
raise
except Exception as exc:
logger.exception(
"Failed to purchase %s months of Premium for user '%s' using '%s' due to an unexpected error",
months,
username,
payment_method,
)
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pyfragment.domains.base import BaseService
from pyfragment.domains.purchases.models import PremiumResult, StarsResult
from pyfragment.domains.purchases.purchase import purchase_premium, purchase_stars
from pyfragment.enums import PaymentMethod
if TYPE_CHECKING:
pass
class PurchasesService(BaseService):
async def purchase_stars(
self,
username: str,
amount: int,
show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> StarsResult:
return await purchase_stars(self._client, username, amount, show_sender=show_sender, payment_method=payment_method)
async def purchase_premium(
self,
username: str,
months: int,
show_sender: bool = True,
payment_method: PaymentMethod = PaymentMethod.GRAM,
) -> PremiumResult:
return await purchase_premium(self._client, username, months, show_sender=show_sender, payment_method=payment_method)
+54
View File
@@ -0,0 +1,54 @@
from __future__ import annotations
from enum import StrEnum
from typing import Any
from tonutils.contracts.wallet import WalletHighloadV2, WalletHighloadV3R1, WalletV4R2, WalletV5R1
class PaymentMethod(StrEnum):
GRAM = "ton"
USDT_GRAM = "usdt_ton"
# Not supported yet
USDT_ETH = "usdt_eth"
USDT_POL = "usdt_pol"
USDC_ETH = "usdc_eth"
USDC_BASE = "usdc_base"
USDC_POL = "usdc_pol"
class WalletVersion(StrEnum):
V4R2 = "V4R2"
V5R1 = "V5R1"
HighloadV2 = "HighloadV2"
HighloadV3R1 = "HighloadV3R1"
WALLET_CLASSES: dict[WalletVersion, Any] = {
WalletVersion.V4R2: WalletV4R2,
WalletVersion.V5R1: WalletV5R1,
WalletVersion.HighloadV2: WalletHighloadV2,
WalletVersion.HighloadV3R1: WalletHighloadV3R1,
}
class ApiProvider(StrEnum):
TONAPI = "tonapi" # tonconsole.com — default
TONCENTER = "toncenter" # t.me/toncenter
class SupportedBrowser(StrEnum):
ARC = "arc"
BRAVE = "brave"
CHROME = "chrome"
CHROMIUM = "chromium"
CHROMIUM_BASED = "chromium_based"
EDGE = "edge"
FIREFOX = "firefox"
FIREFOX_BASED = "firefox_based"
LIBREWOLF = "librewolf"
OPERA = "opera"
OPERA_GX = "opera_gx"
SAFARI = "safari"
VIVALDI = "vivaldi"
+179
View File
@@ -0,0 +1,179 @@
from __future__ import annotations
from pyfragment.core.constants import (
GRAM_TOPUP_MAX,
GRAM_TOPUP_MIN,
MNEMONIC_WORD_COUNTS_VALID,
PREMIUM_MONTHS_VALID,
PREMIUM_WINNERS_MAX,
PREMIUM_WINNERS_MIN,
STARS_GIVEAWAY_MAX,
STARS_GIVEAWAY_MIN,
STARS_PURCHASE_MAX,
STARS_PURCHASE_MIN,
STARS_WINNERS_MAX,
STARS_WINNERS_MIN,
)
class FragmentError(Exception):
"""Base exception for all pyfragment errors."""
class ClientError(FragmentError):
"""Raised for client configuration and setup issues."""
class ConfigurationError(ClientError):
"""Raised when required client parameters are missing or invalid."""
MISSING_VARS = "Missing required parameter(s): {keys}."
UNSUPPORTED_VERSION = "Unsupported wallet version '{version}'. Supported values: {supported}."
INVALID_MNEMONIC = f"Invalid mnemonic phrase: expected {', '.join(str(n) for n in sorted(MNEMONIC_WORD_COUNTS_VALID))} words, got {{count}}."
UNSUPPORTED_PROVIDER = "Unsupported API provider '{provider}'. Supported values: {supported}."
INVALID_MONTHS = f"Invalid Premium duration: choose {', '.join(str(m) for m in sorted(PREMIUM_MONTHS_VALID))} months."
INVALID_STARS_AMOUNT = (
f"Invalid Stars amount: must be an integer between {STARS_PURCHASE_MIN:,} and {STARS_PURCHASE_MAX:,}."
)
INVALID_GRAM_AMOUNT = f"Invalid GRAM (ex TON) amount: must be an integer between {GRAM_TOPUP_MIN:,} and {GRAM_TOPUP_MAX:,}."
INVALID_WINNERS_STARS = (
f"Invalid winners count: must be an integer between {STARS_WINNERS_MIN:,} and {STARS_WINNERS_MAX:,}."
)
INVALID_WINNERS_PREMIUM = (
f"Invalid winners count: must be an integer between {PREMIUM_WINNERS_MIN:,} and {PREMIUM_WINNERS_MAX:,}."
)
INVALID_STARS_PER_WINNER = (
f"Invalid Stars per winner: must be an integer between {STARS_GIVEAWAY_MIN:,} and {STARS_GIVEAWAY_MAX:,}."
)
INVALID_PAYMENT_METHOD = "Invalid payment method '{method}'. Supported values: {supported}."
class CookieError(ClientError):
"""Raised when cookies are unreadable or missing required fields."""
READ_FAILED = "Failed to parse cookies: expected a JSON string or a dict, got {exc}."
MISSING_KEYS = (
"Fragment cookies are missing or empty for key(s): {keys}. "
"Open fragment.com in your browser, log in, and copy fresh cookies."
)
UNSUPPORTED_BROWSER = "Unsupported browser '{browser}'. Supported values: {supported}."
BROWSER_READ_FAILED = (
"Failed to read {browser} cookies: {exc}. Make sure {browser} is installed and you are logged in to {url}."
)
MISSING_BROWSER_KEYS = (
"Fragment cookies not found in {browser}: {keys}. "
"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."
class FragmentAPIError(FragmentError):
"""Raised for errors returned by Fragment's API responses."""
NO_REQUEST_ID = "Fragment did not return a request ID for '{context}'. Your session may have expired. Refresh your cookies and try again."
class FragmentPageError(FragmentAPIError):
"""Raised when the Fragment page cannot be fetched or the API hash is not found."""
BAD_STATUS = "Fragment returned HTTP {status} when loading {url}. Your cookies may be invalid or expired. Refresh them and try again."
NOT_FOUND = "Could not extract the API hash from {url}. The page structure may have changed, or you may not be logged in. Refresh your cookies."
class UserNotFoundError(FragmentAPIError):
"""Raised when the target Telegram user is not found on Fragment."""
NOT_FOUND = (
"Telegram user '{username}' was not found on Fragment. Double-check the username and make sure the account exists."
)
NOT_A_USER = "'{username}' does not belong to a user account. Make sure the username is assigned to a personal Telegram account, not a channel or bot."
class AlreadySubscribedError(FragmentAPIError):
"""Raised when trying to gift Premium to a user who already has an active subscription."""
PREMIUM_ACTIVE = "This account is already subscribed to Telegram Premium."
class AnonymousNumberError(FragmentAPIError):
"""Raised for Fragment anonymous number API failures."""
NOT_OWNED = "Number '{number}' is not associated with your Fragment account or has no active sessions to terminate."
TERMINATE_FAILED = "Failed to terminate sessions for '{number}': {error}"
class TransactionError(FragmentAPIError):
"""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}"
BROADCAST_FAILED_SSL = (
"Transaction broadcast failed due to an SSL certificate error: {exc}\n"
"This usually means your system's CA bundle is missing or outdated.\n"
"Fix: run `pip install --upgrade certifi` and retry. "
"On macOS you may also need to run the 'Install Certificates.command' "
"located in your Python installation folder."
)
DUPLICATE_SEQNO = (
"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."
)
class ParseError(FragmentAPIError):
"""Raised when a Fragment API response or payload cannot be parsed."""
UNPARSEABLE = "Failed to parse the Fragment API response for '{context}': {exc}"
class VerificationError(FragmentAPIError):
"""Raised when Fragment requires KYC verification before proceeding."""
KYC_REQUIRED = (
"Fragment requires identity verification (KYC) before this action can be completed. "
"Complete verification at https://fragment.com/my/profile and retry."
)
class OperationError(FragmentError):
"""Raised for runtime operation failures unrelated to Fragment's API."""
class WalletError(OperationError):
"""Raised for GRAM (ex TON) wallet issues (connection, balance, account info)."""
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."
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 GRAM (ex TON) network: {exc}"
WALLET_INFO_FAILED = "Failed to retrieve wallet info from GRAM (ex TON) network: {exc}"
class UnexpectedError(OperationError):
"""Raised when an unexpected error occurs during an API call."""
UNEXPECTED = "An unexpected error occurred during the operation: {exc}"
__all__ = [
"FragmentError",
"ClientError",
"ConfigurationError",
"CookieError",
"FragmentAPIError",
"FragmentPageError",
"AnonymousNumberError",
"AlreadySubscribedError",
"UserNotFoundError",
"TransactionError",
"ParseError",
"VerificationError",
"OperationError",
"WalletError",
"UnexpectedError",
]
View File
+3
View File
@@ -0,0 +1,3 @@
from pyfragment.services.cookies import CookieResult, get_cookies_from_browser
__all__ = ["CookieResult", "get_cookies_from_browser"]
+4
View File
@@ -0,0 +1,4 @@
from pyfragment.services.cookies.models import CookieResult
from pyfragment.services.cookies.service import get_cookies_from_browser
__all__ = ["CookieResult", "get_cookies_from_browser"]
+12
View File
@@ -0,0 +1,12 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class CookieResult:
cookies: dict[str, str]
expires: str | None
def __repr__(self) -> str:
return f"CookieResult(cookies={self.cookies!r}, expires={self.expires!r})"
+60
View File
@@ -0,0 +1,60 @@
from __future__ import annotations
import importlib
from datetime import UTC, datetime
from typing import Any
from pyfragment.core.constants import FRAGMENT_BASE_URL, FRAGMENT_DOMAIN, REQUIRED_COOKIE_KEYS
from pyfragment.enums import SupportedBrowser
from pyfragment.exceptions import CookieError
from pyfragment.services.cookies.models import CookieResult
try:
import rookiepy
except Exception: # noqa: BLE001
rookiepy = None # type: ignore[assignment]
def get_cookies_from_browser(browser: str = "chrome") -> CookieResult:
global rookiepy
key = browser.lower()
if not any(key == m for m in SupportedBrowser):
supported = ", ".join(sorted(b.value for b in SupportedBrowser))
raise CookieError(CookieError.UNSUPPORTED_BROWSER.format(browser=browser, supported=supported))
try:
if rookiepy is None:
rookiepy = importlib.import_module("rookiepy")
jar: list[dict[str, Any]] = getattr(rookiepy, key)([FRAGMENT_DOMAIN])
except Exception as exc:
raise CookieError(CookieError.BROWSER_READ_FAILED.format(browser=browser, exc=exc, url=FRAGMENT_BASE_URL)) from exc
cookie_map: dict[str, str] = {c["name"]: c["value"] for c in jar if c.get("name") and c.get("value")}
missing = [k for k in REQUIRED_COOKIE_KEYS if not str(cookie_map.get(k, "")).strip()]
if missing:
raise CookieError(CookieError.MISSING_BROWSER_KEYS.format(browser=browser, keys=missing, url=FRAGMENT_BASE_URL))
expires_iso: str | None = None
for cookie in jar:
if cookie.get("name") == "stel_ssid":
raw = cookie.get("expires")
if isinstance(raw, (int, float)):
expires_iso = datetime.fromtimestamp(raw, tz=UTC).isoformat()
elif isinstance(raw, str) and raw:
for fmt in ("%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"):
try:
expires_iso = datetime.strptime(raw, fmt).replace(tzinfo=UTC).isoformat()
break
except ValueError:
continue
break
if expires_iso:
expires_dt = datetime.fromisoformat(expires_iso)
if expires_dt < datetime.now(UTC):
raise CookieError(CookieError.EXPIRED.format(expires=expires_iso))
return CookieResult(cookies={k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS}, expires=expires_iso)
+5
View File
@@ -0,0 +1,5 @@
from pyfragment.services.tonapi.service import TonapiService
__all__ = [
"TonapiService",
]
+135
View File
@@ -0,0 +1,135 @@
from __future__ import annotations
import base64
import logging
from typing import TYPE_CHECKING, Any
from ton_core import NetworkGlobalID
from tonutils.clients import TonapiClient, ToncenterClient
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_GRAM_BALANCE, MIN_USDT_BALANCE, USDT_GRAM_MASTER_ADDRESS
from pyfragment.enums import WALLET_CLASSES, ApiProvider
from pyfragment.exceptions import WalletError
from pyfragment.services.tonapi.models import WalletInfo
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
logger = logging.getLogger(__name__)
def _make_ton_client(client: FragmentClient) -> Any:
"""Return the appropriate tonutils client based on the configured api_provider."""
if client.api_provider == ApiProvider.TONCENTER:
return ToncenterClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key)
return TonapiClient(network=NetworkGlobalID.MAINNET, api_key=client.api_key)
async def get_usdt_balance(ton: Any, wallet_address: str) -> float:
"""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_GRAM_MASTER_ADDRESS,
owner_address=wallet_address,
)
wallet_data = await get_wallet_data_get_method(client=ton, address=jetton_wallet_address)
raw_balance = int(wallet_data[0]) if wallet_data else 0
return float(raw_balance) / 1_000_000.0
except ProviderResponseError as exc:
if exc.code == 404:
logger.debug("No USDT jetton wallet found for '%s'; treating balance as 0", wallet_address)
return 0.0
logger.error("Failed to load USDT balance for wallet '%s': %s", wallet_address, exc, exc_info=True)
raise WalletError(WalletError.USDT_BALANCE_CHECK_FAILED.format(exc=exc)) from exc
except Exception as exc:
logger.exception("Failed to load USDT balance for wallet '%s' due to an unexpected error", wallet_address)
raise WalletError(WalletError.USDT_BALANCE_CHECK_FAILED.format(exc=exc)) from exc
async def check_gram_payment_balance(
balance_gram: float,
amount_gram: float,
required_payment_amount: float | None,
) -> None:
"""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_gram = max(tx_price_gram, required_payment_amount)
required_gram = max(tx_price_gram, MIN_GRAM_BALANCE)
if balance_gram < required_gram:
logger.error(
"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_GRAM_BALANCE.format(balance=balance_gram, required=required_gram))
async def check_usdt_payment_balance(
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_gram < MIN_GRAM_BALANCE:
logger.error(
"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_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
if usdt_balance < required_usdt:
logger.error(
"Failed USDT balance check for wallet '%s': balance=%s USDT, required=%s USDT",
wallet_address,
round(usdt_balance, 6),
round(required_usdt, 6),
)
raise WalletError(WalletError.LOW_USDT_BALANCE.format(balance=usdt_balance, required=required_usdt))
async def get_account_info(client: FragmentClient) -> dict[str, Any]:
"""Build the wallet payload Fragment needs to prepare a transaction."""
async with _make_ton_client(client) as ton:
try:
wallet_cls = WALLET_CLASSES[client.wallet_version]
wallet, pub_key, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
boc = wallet.state_init.serialize().to_boc()
return {
"address": wallet.address.to_str(False, False),
"publicKey": pub_key.as_hex,
"chain": "-239",
"walletStateInit": base64.b64encode(boc).decode(),
}
except Exception as exc:
logger.exception("Failed to build Fragment account info from the configured wallet")
raise WalletError(WalletError.ACCOUNT_INFO_FAILED.format(exc=exc)) from exc
async def get_wallet_info(client: FragmentClient) -> WalletInfo:
"""Fetch the wallet address, chain state, and GRAM (ex TON)/USDT balances."""
async with _make_ton_client(client) as ton:
try:
wallet_cls = WALLET_CLASSES[client.wallet_version]
wallet, _, _, _ = wallet_cls.from_mnemonic(client=ton, mnemonic=client.seed)
await wallet.refresh()
wallet_address = wallet.address.to_str(False, False)
usdt_balance = await get_usdt_balance(ton, wallet_address)
return WalletInfo(
address=wallet.address.to_str(is_user_friendly=True, is_bounceable=False),
state=wallet.state.value,
gram_balance=round(wallet.balance / 1_000_000_000, 4),
usdt_balance=round(usdt_balance, 4),
)
except Exception as exc:
logger.exception("Failed to fetch wallet info from Tonapi")
raise WalletError(WalletError.WALLET_INFO_FAILED.format(exc=exc)) from exc
+17
View File
@@ -0,0 +1,17 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass
class WalletInfo:
address: str
state: str
gram_balance: float
usdt_balance: float
def __repr__(self) -> str:
return (
f"WalletInfo(address='{self.address}', state='{self.state}', "
f"gram_balance={self.gram_balance} GRAM (ex TON), usdt_balance={self.usdt_balance} USDT)"
)
+15
View File
@@ -0,0 +1,15 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pyfragment.domains.base import BaseService
from pyfragment.services.tonapi.account import get_wallet_info
from pyfragment.services.tonapi.models import WalletInfo
if TYPE_CHECKING:
pass
class TonapiService(BaseService):
async def get_wallet(self) -> WalletInfo:
return await get_wallet_info(self._client)

Some files were not shown because too many files have changed in this diff Show More