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.
- cryptopay_python_sdk-0.1.1/.gitignore +29 -0
- cryptopay_python_sdk-0.1.1/LICENSE +21 -0
- cryptopay_python_sdk-0.1.1/PKG-INFO +151 -0
- cryptopay_python_sdk-0.1.1/README.md +99 -0
- cryptopay_python_sdk-0.1.1/pyproject.toml +59 -0
- cryptopay_python_sdk-0.1.1/src/crypto_pay_api/__init__.py +16 -0
- cryptopay_python_sdk-0.1.1/src/crypto_pay_api/_core.py +28 -0
- cryptopay_python_sdk-0.1.1/src/crypto_pay_api/async_.py +231 -0
- cryptopay_python_sdk-0.1.1/src/crypto_pay_api/config.py +15 -0
- cryptopay_python_sdk-0.1.1/src/crypto_pay_api/errors.py +19 -0
- cryptopay_python_sdk-0.1.1/src/crypto_pay_api/py.typed +0 -0
- cryptopay_python_sdk-0.1.1/src/crypto_pay_api/sync.py +226 -0
- cryptopay_python_sdk-0.1.1/src/crypto_pay_api/utils.py +27 -0
|
@@ -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
|
+
[](https://pypi.org/project/crypto-pay-api/)
|
|
56
|
+
[](https://pypi.org/project/crypto-pay-api/)
|
|
57
|
+
[](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
|
+
[](https://pypi.org/project/crypto-pay-api/)
|
|
4
|
+
[](https://pypi.org/project/crypto-pay-api/)
|
|
5
|
+
[](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")
|