cryptopay-python-sdk 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ # Virtual environments
2
+ venv/
3
+ .venv/
4
+ env/
5
+
6
+ # Build / distribution
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+
11
+ # Python cache
12
+ __pycache__/
13
+ *.py[cod]
14
+
15
+ # Test / personal scripts
16
+ TEST.py
17
+
18
+ # Tools
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .pytest_cache/
22
+
23
+ # IDE
24
+ .idea/
25
+ .vscode/
26
+
27
+ # Misc
28
+ .env
29
+ *.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: cryptopay-python-sdk
3
+ Version: 0.1.1
4
+ Summary: Sync + Async Python client for Telegram Crypto Pay API (Crypto Bot)
5
+ Project-URL: Homepage, https://github.com/medovi40k/crypto-pay-api
6
+ Project-URL: Repository, https://github.com/medovi40k/crypto-pay-api
7
+ Project-URL: Issues, https://github.com/medovi40k/crypto-pay-api/issues
8
+ Author: medovi40k
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: api,asyncio,crypto,cryptopay,httpx,requests,telegram
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3 :: Only
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Typing :: Typed
42
+ Requires-Python: >=3.9
43
+ Requires-Dist: httpx>=0.24
44
+ Requires-Dist: requests>=2.31
45
+ Provides-Extra: dev
46
+ Requires-Dist: mypy>=1.8; extra == 'dev'
47
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
48
+ Requires-Dist: pytest>=7.4; extra == 'dev'
49
+ Requires-Dist: ruff>=0.4; extra == 'dev'
50
+ Requires-Dist: types-requests>=2.31; extra == 'dev'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # crypto-pay-api
54
+
55
+ [![PyPI](https://img.shields.io/pypi/v/crypto-pay-api)](https://pypi.org/project/crypto-pay-api/)
56
+ [![Python](https://img.shields.io/pypi/pyversions/crypto-pay-api)](https://pypi.org/project/crypto-pay-api/)
57
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
58
+
59
+ Sync + async Python client for the [Telegram Crypto Pay API](https://help.send.tg/en/articles/10279948-crypto-pay-api).
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install crypto-pay-api
65
+ ```
66
+
67
+ ## Quick start
68
+
69
+ **Async** (recommended)
70
+
71
+ ```python
72
+ import asyncio
73
+ from crypto_pay_api import CryptoPayAsync, CryptoPayConfig
74
+
75
+ async def main():
76
+ cfg = CryptoPayConfig(token="YOUR_TOKEN")
77
+ async with CryptoPayAsync(cfg) as cp:
78
+ invoice = await cp.create_invoice(asset="USDT", amount=5)
79
+ print(invoice["pay_url"])
80
+
81
+ asyncio.run(main())
82
+ ```
83
+
84
+ **Sync**
85
+
86
+ ```python
87
+ from crypto_pay_api import CryptoPay, CryptoPayConfig
88
+
89
+ cfg = CryptoPayConfig(token="YOUR_TOKEN")
90
+ with CryptoPay(cfg) as cp:
91
+ invoice = cp.create_invoice(asset="USDT", amount=5)
92
+ print(invoice["pay_url"])
93
+ ```
94
+
95
+ ## Configuration
96
+
97
+ ```python
98
+ CryptoPayConfig(
99
+ token="YOUR_TOKEN",
100
+ network="mainnet", # or "testnet"
101
+ timeout=20.0,
102
+ base_url_override=None, # override API base URL if needed
103
+ )
104
+ ```
105
+
106
+ ## Methods
107
+
108
+ | Method | Description |
109
+ |---|---|
110
+ | `get_me()` | App info |
111
+ | `create_invoice(...)` | Create a payment invoice |
112
+ | `delete_invoice(invoice_id)` | Delete invoice |
113
+ | `get_invoices(...)` | List invoices |
114
+ | `create_check(...)` | Create a check |
115
+ | `delete_check(check_id)` | Delete check |
116
+ | `get_checks(...)` | List checks |
117
+ | `transfer(...)` | Send coins to a Telegram user |
118
+ | `get_transfers(...)` | List transfers |
119
+ | `get_balance()` | App balance |
120
+ | `get_exchange_rates()` | Current exchange rates |
121
+ | `get_currencies()` | Supported currencies |
122
+ | `get_stats(...)` | App statistics |
123
+
124
+ ## Webhook verification
125
+
126
+ ```python
127
+ from crypto_pay_api import verify_webhook_signature
128
+
129
+ ok = verify_webhook_signature(
130
+ token="YOUR_TOKEN",
131
+ raw_body=request.body, # raw bytes
132
+ signature_header=request.headers["crypto-pay-api-signature"],
133
+ )
134
+ ```
135
+
136
+ ## Error handling
137
+
138
+ ```python
139
+ from crypto_pay_api import CryptoPayAPIError, CryptoPayHTTPError
140
+
141
+ try:
142
+ invoice = cp.create_invoice(asset="USDT", amount=5)
143
+ except CryptoPayAPIError as e:
144
+ print(e.error_code, e.raw) # API-level error (ok=false)
145
+ except CryptoPayHTTPError as e:
146
+ print(e) # network / HTTP error
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1,99 @@
1
+ # crypto-pay-api
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/crypto-pay-api)](https://pypi.org/project/crypto-pay-api/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/crypto-pay-api)](https://pypi.org/project/crypto-pay-api/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ Sync + async Python client for the [Telegram Crypto Pay API](https://help.send.tg/en/articles/10279948-crypto-pay-api).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install crypto-pay-api
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ **Async** (recommended)
18
+
19
+ ```python
20
+ import asyncio
21
+ from crypto_pay_api import CryptoPayAsync, CryptoPayConfig
22
+
23
+ async def main():
24
+ cfg = CryptoPayConfig(token="YOUR_TOKEN")
25
+ async with CryptoPayAsync(cfg) as cp:
26
+ invoice = await cp.create_invoice(asset="USDT", amount=5)
27
+ print(invoice["pay_url"])
28
+
29
+ asyncio.run(main())
30
+ ```
31
+
32
+ **Sync**
33
+
34
+ ```python
35
+ from crypto_pay_api import CryptoPay, CryptoPayConfig
36
+
37
+ cfg = CryptoPayConfig(token="YOUR_TOKEN")
38
+ with CryptoPay(cfg) as cp:
39
+ invoice = cp.create_invoice(asset="USDT", amount=5)
40
+ print(invoice["pay_url"])
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ ```python
46
+ CryptoPayConfig(
47
+ token="YOUR_TOKEN",
48
+ network="mainnet", # or "testnet"
49
+ timeout=20.0,
50
+ base_url_override=None, # override API base URL if needed
51
+ )
52
+ ```
53
+
54
+ ## Methods
55
+
56
+ | Method | Description |
57
+ |---|---|
58
+ | `get_me()` | App info |
59
+ | `create_invoice(...)` | Create a payment invoice |
60
+ | `delete_invoice(invoice_id)` | Delete invoice |
61
+ | `get_invoices(...)` | List invoices |
62
+ | `create_check(...)` | Create a check |
63
+ | `delete_check(check_id)` | Delete check |
64
+ | `get_checks(...)` | List checks |
65
+ | `transfer(...)` | Send coins to a Telegram user |
66
+ | `get_transfers(...)` | List transfers |
67
+ | `get_balance()` | App balance |
68
+ | `get_exchange_rates()` | Current exchange rates |
69
+ | `get_currencies()` | Supported currencies |
70
+ | `get_stats(...)` | App statistics |
71
+
72
+ ## Webhook verification
73
+
74
+ ```python
75
+ from crypto_pay_api import verify_webhook_signature
76
+
77
+ ok = verify_webhook_signature(
78
+ token="YOUR_TOKEN",
79
+ raw_body=request.body, # raw bytes
80
+ signature_header=request.headers["crypto-pay-api-signature"],
81
+ )
82
+ ```
83
+
84
+ ## Error handling
85
+
86
+ ```python
87
+ from crypto_pay_api import CryptoPayAPIError, CryptoPayHTTPError
88
+
89
+ try:
90
+ invoice = cp.create_invoice(asset="USDT", amount=5)
91
+ except CryptoPayAPIError as e:
92
+ print(e.error_code, e.raw) # API-level error (ok=false)
93
+ except CryptoPayHTTPError as e:
94
+ print(e) # network / HTTP error
95
+ ```
96
+
97
+ ## License
98
+
99
+ MIT
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.22"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cryptopay-python-sdk"
7
+ version = "0.1.1"
8
+ description = "Sync + Async Python client for Telegram Crypto Pay API (Crypto Bot)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "medovi40k" }]
13
+ keywords = ["telegram", "crypto", "cryptopay", "api", "asyncio", "httpx", "requests"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Typing :: Typed",
25
+ ]
26
+
27
+ dependencies = [
28
+ "httpx>=0.24",
29
+ "requests>=2.31",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=7.4",
35
+ "pytest-asyncio>=0.23",
36
+ "ruff>=0.4",
37
+ "mypy>=1.8",
38
+ "types-requests>=2.31",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/medovi40k/crypto-pay-api"
43
+ Repository = "https://github.com/medovi40k/crypto-pay-api"
44
+ Issues = "https://github.com/medovi40k/crypto-pay-api/issues"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/crypto_pay_api"]
48
+
49
+ [tool.ruff]
50
+ line-length = 100
51
+ target-version = "py39"
52
+
53
+ [tool.mypy]
54
+ python_version = "3.9"
55
+ warn_return_any = true
56
+ warn_unused_ignores = true
57
+ disallow_untyped_defs = false
58
+ no_implicit_optional = true
59
+ strict_optional = true
@@ -0,0 +1,16 @@
1
+ from .config import CryptoPayConfig
2
+ from .errors import CryptoPayError, CryptoPayAPIError, CryptoPayHTTPError
3
+ from .sync import CryptoPay
4
+ from .async_ import CryptoPayAsync
5
+ from .utils import verify_webhook_signature, canonical_json_bytes
6
+
7
+ __all__ = [
8
+ "CryptoPayConfig",
9
+ "CryptoPayError",
10
+ "CryptoPayAPIError",
11
+ "CryptoPayHTTPError",
12
+ "CryptoPay",
13
+ "CryptoPayAsync",
14
+ "verify_webhook_signature",
15
+ "canonical_json_bytes",
16
+ ]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .config import CryptoPayConfig
6
+ from .errors import CryptoPayAPIError, CryptoPayHTTPError
7
+
8
+
9
+ def resolve_root(cfg: CryptoPayConfig) -> str:
10
+ if cfg.base_url_override:
11
+ return cfg.base_url_override.rstrip("/")
12
+ return "https://pay.crypt.bot" if cfg.network == "mainnet" else "https://testnet-pay.crypt.bot"
13
+
14
+
15
+ def clean_params(params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
16
+ if not params:
17
+ return {}
18
+ return {k: v for k, v in params.items() if v is not None}
19
+
20
+
21
+ def parse_api_response(data: Any) -> Any:
22
+ if not isinstance(data, dict) or "ok" not in data:
23
+ raise CryptoPayHTTPError(f"Unexpected response: {data!r}")
24
+
25
+ if data.get("ok") is True:
26
+ return data.get("result")
27
+
28
+ raise CryptoPayAPIError(str(data.get("error", "UNKNOWN_ERROR")), raw=data)
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional, Union, Literal
4
+
5
+ import httpx
6
+
7
+ from .config import CryptoPayConfig
8
+ from .errors import CryptoPayHTTPError
9
+ from ._core import resolve_root, clean_params, parse_api_response
10
+
11
+
12
+ class CryptoPayAsync:
13
+ """
14
+ Async client for Crypto Pay API.
15
+ """
16
+
17
+ def __init__(self, config: CryptoPayConfig, *, client: Optional[httpx.AsyncClient] = None):
18
+ self.config = config
19
+ self._client_external = client is not None
20
+ self._client = client
21
+ self._root = resolve_root(config)
22
+
23
+ async def __aenter__(self) -> "CryptoPayAsync":
24
+ if self._client is None:
25
+ self._client = httpx.AsyncClient(
26
+ base_url=self._root,
27
+ timeout=self.config.timeout,
28
+ headers={
29
+ "Crypto-Pay-API-Token": self.config.token,
30
+ "User-Agent": self.config.user_agent,
31
+ },
32
+ )
33
+ return self
34
+
35
+ async def __aexit__(self, exc_type, exc, tb) -> None:
36
+ await self.aclose()
37
+
38
+ async def aclose(self) -> None:
39
+ if self._client and not self._client_external:
40
+ await self._client.aclose()
41
+ self._client = None
42
+
43
+ async def _request(self, method_name: str, params: Optional[Dict[str, Any]] = None) -> Any:
44
+ if self._client is None:
45
+ async with self:
46
+ return await self._request(method_name, params=params)
47
+
48
+ url = f"/api/{method_name}"
49
+ payload = clean_params(params)
50
+
51
+ try:
52
+ if payload:
53
+ resp = await self._client.post(url, json=payload)
54
+ else:
55
+ resp = await self._client.get(url)
56
+ except httpx.HTTPError as e:
57
+ raise CryptoPayHTTPError(str(e)) from e
58
+
59
+ if resp.status_code >= 400:
60
+ raise CryptoPayHTTPError(f"HTTP {resp.status_code}: {resp.text}")
61
+
62
+ return parse_api_response(resp.json())
63
+
64
+ # -------- API methods --------
65
+
66
+ async def get_me(self) -> Dict[str, Any]:
67
+ return await self._request("getMe")
68
+
69
+ async def create_invoice(
70
+ self,
71
+ *,
72
+ amount: Union[str, float],
73
+ currency_type: Optional[Literal["crypto", "fiat"]] = None,
74
+ asset: Optional[str] = None,
75
+ fiat: Optional[str] = None,
76
+ accepted_assets: Optional[str] = None,
77
+ swap_to: Optional[str] = None,
78
+ description: Optional[str] = None,
79
+ hidden_message: Optional[str] = None,
80
+ paid_btn_name: Optional[str] = None,
81
+ paid_btn_url: Optional[str] = None,
82
+ payload: Optional[str] = None,
83
+ allow_comments: Optional[bool] = None,
84
+ allow_anonymous: Optional[bool] = None,
85
+ expires_in: Optional[int] = None,
86
+ ) -> Dict[str, Any]:
87
+ return await self._request(
88
+ "createInvoice",
89
+ {
90
+ "currency_type": currency_type,
91
+ "asset": asset,
92
+ "fiat": fiat,
93
+ "accepted_assets": accepted_assets,
94
+ "amount": str(amount),
95
+ "swap_to": swap_to,
96
+ "description": description,
97
+ "hidden_message": hidden_message,
98
+ "paid_btn_name": paid_btn_name,
99
+ "paid_btn_url": paid_btn_url,
100
+ "payload": payload,
101
+ "allow_comments": allow_comments,
102
+ "allow_anonymous": allow_anonymous,
103
+ "expires_in": expires_in,
104
+ },
105
+ )
106
+
107
+ async def delete_invoice(self, invoice_id: int) -> bool:
108
+ return await self._request("deleteInvoice", {"invoice_id": invoice_id})
109
+
110
+ async def get_invoices(
111
+ self,
112
+ *,
113
+ asset: Optional[str] = None,
114
+ fiat: Optional[str] = None,
115
+ invoice_ids: Optional[Union[str, List[int]]] = None,
116
+ status: Optional[Literal["active", "paid"]] = None,
117
+ offset: Optional[int] = None,
118
+ count: Optional[int] = None,
119
+ ) -> List[Dict[str, Any]]:
120
+ if isinstance(invoice_ids, list):
121
+ invoice_ids = ",".join(str(i) for i in invoice_ids)
122
+ return await self._request(
123
+ "getInvoices",
124
+ {
125
+ "asset": asset,
126
+ "fiat": fiat,
127
+ "invoice_ids": invoice_ids,
128
+ "status": status,
129
+ "offset": offset,
130
+ "count": count,
131
+ },
132
+ )
133
+
134
+ async def create_check(
135
+ self,
136
+ *,
137
+ asset: str,
138
+ amount: Union[str, float],
139
+ pin_to_user_id: Optional[int] = None,
140
+ pin_to_username: Optional[str] = None,
141
+ ) -> Dict[str, Any]:
142
+ return await self._request(
143
+ "createCheck",
144
+ {
145
+ "asset": asset,
146
+ "amount": str(amount),
147
+ "pin_to_user_id": pin_to_user_id,
148
+ "pin_to_username": pin_to_username,
149
+ },
150
+ )
151
+
152
+ async def delete_check(self, check_id: int) -> bool:
153
+ return await self._request("deleteCheck", {"check_id": check_id})
154
+
155
+ async def get_checks(
156
+ self,
157
+ *,
158
+ asset: Optional[str] = None,
159
+ check_ids: Optional[Union[str, List[int]]] = None,
160
+ status: Optional[Literal["active", "activated"]] = None,
161
+ offset: Optional[int] = None,
162
+ count: Optional[int] = None,
163
+ ) -> List[Dict[str, Any]]:
164
+ if isinstance(check_ids, list):
165
+ check_ids = ",".join(str(i) for i in check_ids)
166
+ return await self._request(
167
+ "getChecks",
168
+ {
169
+ "asset": asset,
170
+ "check_ids": check_ids,
171
+ "status": status,
172
+ "offset": offset,
173
+ "count": count,
174
+ },
175
+ )
176
+
177
+ async def transfer(
178
+ self,
179
+ *,
180
+ user_id: int,
181
+ asset: str,
182
+ amount: Union[str, float],
183
+ spend_id: str,
184
+ comment: Optional[str] = None,
185
+ disable_send_notification: Optional[bool] = None,
186
+ ) -> Dict[str, Any]:
187
+ return await self._request(
188
+ "transfer",
189
+ {
190
+ "user_id": user_id,
191
+ "asset": asset,
192
+ "amount": str(amount),
193
+ "spend_id": spend_id,
194
+ "comment": comment,
195
+ "disable_send_notification": disable_send_notification,
196
+ },
197
+ )
198
+
199
+ async def get_transfers(
200
+ self,
201
+ *,
202
+ asset: Optional[str] = None,
203
+ transfer_ids: Optional[Union[str, List[int]]] = None,
204
+ spend_id: Optional[str] = None,
205
+ offset: Optional[int] = None,
206
+ count: Optional[int] = None,
207
+ ) -> List[Dict[str, Any]]:
208
+ if isinstance(transfer_ids, list):
209
+ transfer_ids = ",".join(str(i) for i in transfer_ids)
210
+ return await self._request(
211
+ "getTransfers",
212
+ {
213
+ "asset": asset,
214
+ "transfer_ids": transfer_ids,
215
+ "spend_id": spend_id,
216
+ "offset": offset,
217
+ "count": count,
218
+ },
219
+ )
220
+
221
+ async def get_balance(self) -> List[Dict[str, Any]]:
222
+ return await self._request("getBalance")
223
+
224
+ async def get_exchange_rates(self) -> List[Dict[str, Any]]:
225
+ return await self._request("getExchangeRates")
226
+
227
+ async def get_currencies(self) -> Dict[str, Any]:
228
+ return await self._request("getCurrencies")
229
+
230
+ async def get_stats(self, *, start_at: Optional[str] = None, end_at: Optional[str] = None) -> Dict[str, Any]:
231
+ return await self._request("getStats", {"start_at": start_at, "end_at": end_at})
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Literal, Optional
4
+
5
+
6
+ MainnetOrTestnet = Literal["mainnet", "testnet"]
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CryptoPayConfig:
11
+ token: str
12
+ network: MainnetOrTestnet = "mainnet"
13
+ timeout: float = 20.0
14
+ base_url_override: Optional[str] = None # e.g. "https://pay.crypt.bot"
15
+ user_agent: str = "crypto-pay-api/0.1.1"
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Dict, Optional
3
+
4
+
5
+ class CryptoPayError(Exception):
6
+ """Base error for Crypto Pay clients."""
7
+
8
+
9
+ class CryptoPayAPIError(CryptoPayError):
10
+ """Raised when API returns ok=false."""
11
+
12
+ def __init__(self, error_code: str, raw: Optional[Dict[str, Any]] = None):
13
+ super().__init__(error_code)
14
+ self.error_code = error_code
15
+ self.raw = raw or {}
16
+
17
+
18
+ class CryptoPayHTTPError(CryptoPayError):
19
+ """Raised on network / HTTP errors."""
File without changes
@@ -0,0 +1,226 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional, Union, Literal
4
+
5
+ import requests
6
+ from requests import Response
7
+
8
+ from .config import CryptoPayConfig
9
+ from .errors import CryptoPayHTTPError
10
+ from ._core import resolve_root, clean_params, parse_api_response
11
+
12
+
13
+ class CryptoPay:
14
+ """
15
+ Sync client for Crypto Pay API.
16
+ """
17
+
18
+ def __init__(self, config: CryptoPayConfig, *, session: Optional[requests.Session] = None):
19
+ self.config = config
20
+ self._session_external = session is not None
21
+ self._session = session or requests.Session()
22
+
23
+ self._root = resolve_root(config)
24
+ self._session.headers.update(
25
+ {
26
+ "Crypto-Pay-API-Token": self.config.token,
27
+ "User-Agent": self.config.user_agent,
28
+ }
29
+ )
30
+
31
+ def close(self) -> None:
32
+ if not self._session_external:
33
+ self._session.close()
34
+
35
+ def __enter__(self) -> "CryptoPay":
36
+ return self
37
+
38
+ def __exit__(self, exc_type, exc, tb) -> None:
39
+ self.close()
40
+
41
+ def _raise_for_http(self, resp: Response) -> None:
42
+ if resp.status_code >= 400:
43
+ raise CryptoPayHTTPError(f"HTTP {resp.status_code}: {resp.text}")
44
+
45
+ def _request(self, method_name: str, params: Optional[Dict[str, Any]] = None) -> Any:
46
+ url = f"{self._root}/api/{method_name}"
47
+ payload = clean_params(params)
48
+ try:
49
+ if payload:
50
+ resp = self._session.post(url, json=payload, timeout=self.config.timeout)
51
+ else:
52
+ resp = self._session.get(url, timeout=self.config.timeout)
53
+ except requests.RequestException as e:
54
+ raise CryptoPayHTTPError(str(e)) from e
55
+
56
+ self._raise_for_http(resp)
57
+ return parse_api_response(resp.json())
58
+
59
+ # -------- API methods --------
60
+
61
+ def get_me(self) -> Dict[str, Any]:
62
+ return self._request("getMe")
63
+
64
+ def create_invoice(
65
+ self,
66
+ *,
67
+ amount: Union[str, float],
68
+ currency_type: Optional[Literal["crypto", "fiat"]] = None,
69
+ asset: Optional[str] = None,
70
+ fiat: Optional[str] = None,
71
+ accepted_assets: Optional[str] = None,
72
+ swap_to: Optional[str] = None,
73
+ description: Optional[str] = None,
74
+ hidden_message: Optional[str] = None,
75
+ paid_btn_name: Optional[str] = None,
76
+ paid_btn_url: Optional[str] = None,
77
+ payload: Optional[str] = None,
78
+ allow_comments: Optional[bool] = None,
79
+ allow_anonymous: Optional[bool] = None,
80
+ expires_in: Optional[int] = None,
81
+ ) -> Dict[str, Any]:
82
+ return self._request(
83
+ "createInvoice",
84
+ {
85
+ "currency_type": currency_type,
86
+ "asset": asset,
87
+ "fiat": fiat,
88
+ "accepted_assets": accepted_assets,
89
+ "amount": str(amount),
90
+ "swap_to": swap_to,
91
+ "description": description,
92
+ "hidden_message": hidden_message,
93
+ "paid_btn_name": paid_btn_name,
94
+ "paid_btn_url": paid_btn_url,
95
+ "payload": payload,
96
+ "allow_comments": allow_comments,
97
+ "allow_anonymous": allow_anonymous,
98
+ "expires_in": expires_in,
99
+ },
100
+ )
101
+
102
+ def delete_invoice(self, invoice_id: int) -> bool:
103
+ return self._request("deleteInvoice", {"invoice_id": invoice_id})
104
+
105
+ def get_invoices(
106
+ self,
107
+ *,
108
+ asset: Optional[str] = None,
109
+ fiat: Optional[str] = None,
110
+ invoice_ids: Optional[Union[str, List[int]]] = None,
111
+ status: Optional[Literal["active", "paid"]] = None,
112
+ offset: Optional[int] = None,
113
+ count: Optional[int] = None,
114
+ ) -> List[Dict[str, Any]]:
115
+ if isinstance(invoice_ids, list):
116
+ invoice_ids = ",".join(str(i) for i in invoice_ids)
117
+ return self._request(
118
+ "getInvoices",
119
+ {
120
+ "asset": asset,
121
+ "fiat": fiat,
122
+ "invoice_ids": invoice_ids,
123
+ "status": status,
124
+ "offset": offset,
125
+ "count": count,
126
+ },
127
+ )
128
+
129
+ def create_check(
130
+ self,
131
+ *,
132
+ asset: str,
133
+ amount: Union[str, float],
134
+ pin_to_user_id: Optional[int] = None,
135
+ pin_to_username: Optional[str] = None,
136
+ ) -> Dict[str, Any]:
137
+ return self._request(
138
+ "createCheck",
139
+ {
140
+ "asset": asset,
141
+ "amount": str(amount),
142
+ "pin_to_user_id": pin_to_user_id,
143
+ "pin_to_username": pin_to_username,
144
+ },
145
+ )
146
+
147
+ def delete_check(self, check_id: int) -> bool:
148
+ return self._request("deleteCheck", {"check_id": check_id})
149
+
150
+ def get_checks(
151
+ self,
152
+ *,
153
+ asset: Optional[str] = None,
154
+ check_ids: Optional[Union[str, List[int]]] = None,
155
+ status: Optional[Literal["active", "activated"]] = None,
156
+ offset: Optional[int] = None,
157
+ count: Optional[int] = None,
158
+ ) -> List[Dict[str, Any]]:
159
+ if isinstance(check_ids, list):
160
+ check_ids = ",".join(str(i) for i in check_ids)
161
+ return self._request(
162
+ "getChecks",
163
+ {
164
+ "asset": asset,
165
+ "check_ids": check_ids,
166
+ "status": status,
167
+ "offset": offset,
168
+ "count": count,
169
+ },
170
+ )
171
+
172
+ def transfer(
173
+ self,
174
+ *,
175
+ user_id: int,
176
+ asset: str,
177
+ amount: Union[str, float],
178
+ spend_id: str,
179
+ comment: Optional[str] = None,
180
+ disable_send_notification: Optional[bool] = None,
181
+ ) -> Dict[str, Any]:
182
+ return self._request(
183
+ "transfer",
184
+ {
185
+ "user_id": user_id,
186
+ "asset": asset,
187
+ "amount": str(amount),
188
+ "spend_id": spend_id,
189
+ "comment": comment,
190
+ "disable_send_notification": disable_send_notification,
191
+ },
192
+ )
193
+
194
+ def get_transfers(
195
+ self,
196
+ *,
197
+ asset: Optional[str] = None,
198
+ transfer_ids: Optional[Union[str, List[int]]] = None,
199
+ spend_id: Optional[str] = None,
200
+ offset: Optional[int] = None,
201
+ count: Optional[int] = None,
202
+ ) -> List[Dict[str, Any]]:
203
+ if isinstance(transfer_ids, list):
204
+ transfer_ids = ",".join(str(i) for i in transfer_ids)
205
+ return self._request(
206
+ "getTransfers",
207
+ {
208
+ "asset": asset,
209
+ "transfer_ids": transfer_ids,
210
+ "spend_id": spend_id,
211
+ "offset": offset,
212
+ "count": count,
213
+ },
214
+ )
215
+
216
+ def get_balance(self) -> List[Dict[str, Any]]:
217
+ return self._request("getBalance")
218
+
219
+ def get_exchange_rates(self) -> List[Dict[str, Any]]:
220
+ return self._request("getExchangeRates")
221
+
222
+ def get_currencies(self) -> Dict[str, Any]:
223
+ return self._request("getCurrencies")
224
+
225
+ def get_stats(self, *, start_at: Optional[str] = None, end_at: Optional[str] = None) -> Dict[str, Any]:
226
+ return self._request("getStats", {"start_at": start_at, "end_at": end_at})
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import hmac
5
+ import json
6
+ from typing import Any, Union
7
+
8
+
9
+ def verify_webhook_signature(*, token: str, raw_body: Union[bytes, str], signature_header: str) -> bool:
10
+ """
11
+ Verify webhook update signature.
12
+
13
+ Rule (from docs): compare header `crypto-pay-api-signature` with HMAC-SHA256(body),
14
+ where key = SHA256(app_token). Body is the *entire raw request body*.
15
+ """
16
+ raw = raw_body.encode("utf-8") if isinstance(raw_body, str) else raw_body
17
+ secret = hashlib.sha256(token.encode("utf-8")).digest()
18
+ digest_hex = hmac.new(secret, raw, hashlib.sha256).hexdigest()
19
+ return hmac.compare_digest(digest_hex, signature_header)
20
+
21
+
22
+ def canonical_json_bytes(obj: Any) -> bytes:
23
+ """
24
+ If your framework lost raw body bytes, serialize deterministically:
25
+ compact JSON without spaces.
26
+ """
27
+ return json.dumps(obj, separators=(",", ":"), ensure_ascii=False).encode("utf-8")