cryptolike 0.1.0__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.
- cryptolike-0.1.0/LICENSE +21 -0
- cryptolike-0.1.0/PKG-INFO +184 -0
- cryptolike-0.1.0/README.md +152 -0
- cryptolike-0.1.0/pyproject.toml +47 -0
- cryptolike-0.1.0/setup.cfg +4 -0
- cryptolike-0.1.0/src/cryptolike/__init__.py +87 -0
- cryptolike-0.1.0/src/cryptolike/_version.py +4 -0
- cryptolike-0.1.0/src/cryptolike/auth.py +32 -0
- cryptolike-0.1.0/src/cryptolike/client.py +528 -0
- cryptolike-0.1.0/src/cryptolike/errors.py +74 -0
- cryptolike-0.1.0/src/cryptolike/py.typed +0 -0
- cryptolike-0.1.0/src/cryptolike/types.py +402 -0
- cryptolike-0.1.0/src/cryptolike/webhook.py +116 -0
- cryptolike-0.1.0/src/cryptolike.egg-info/PKG-INFO +184 -0
- cryptolike-0.1.0/src/cryptolike.egg-info/SOURCES.txt +19 -0
- cryptolike-0.1.0/src/cryptolike.egg-info/dependency_links.txt +1 -0
- cryptolike-0.1.0/src/cryptolike.egg-info/requires.txt +5 -0
- cryptolike-0.1.0/src/cryptolike.egg-info/top_level.txt +1 -0
- cryptolike-0.1.0/tests/test_client.py +429 -0
- cryptolike-0.1.0/tests/test_vectors.py +75 -0
- cryptolike-0.1.0/tests/test_version.py +11 -0
cryptolike-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CryptoLike
|
|
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,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cryptolike
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official server-side SDK for the CryptoLike merchant API: signed requests, automatic Idempotency-Key, retries with backoff, typed resources and webhook verification.
|
|
5
|
+
Author: CryptoLike team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Cryptolike-net/cryptolike-python
|
|
8
|
+
Project-URL: Documentation, https://cryptolike.net/docs/sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/Cryptolike-net/cryptolike-python
|
|
10
|
+
Project-URL: Issues, https://github.com/Cryptolike-net/cryptolike-python/issues
|
|
11
|
+
Keywords: cryptolike,crypto,cryptocurrency,crypto payments,payment gateway,payments,bitcoin,usdt,invoices,webhooks,hmac,api client,sdk
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: requests>=2.33.0
|
|
28
|
+
Requires-Dist: urllib3>=2.8.0
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# cryptolike (Python SDK)
|
|
34
|
+
|
|
35
|
+
Official **server-side** SDK for the CryptoLike merchant API (Python ≥ 3.10, `requests`; typed, `py.typed`).
|
|
36
|
+
|
|
37
|
+
- Signed requests (`X-API-Key` / `X-Timestamp` / `X-Signature`, HMAC-SHA256 — [Authentication](https://cryptolike.net/docs/auth))
|
|
38
|
+
- `Idempotency-Key` generated for every `POST` (pass your own to make retries exact)
|
|
39
|
+
- Retries with exponential backoff and jitter on `429` (honours `Retry-After`), `5xx` and transport errors
|
|
40
|
+
- Dataclasses for every object; amounts are `Decimal` — floats are refused
|
|
41
|
+
- `verify_webhook()` — signature and timestamp check of webhook deliveries ([Webhooks](https://cryptolike.net/docs/webhooks))
|
|
42
|
+
|
|
43
|
+
## The secret stays on your server
|
|
44
|
+
|
|
45
|
+
> The API secret signs requests *as you*: whoever has it can create payments, read balances and
|
|
46
|
+
> change your webhooks. Use this SDK from your backend only; never put the secret into code that
|
|
47
|
+
> ships to a browser or a mobile app. Your page needs only the invoice id / `checkout_url` your
|
|
48
|
+
> server obtained — see [Payment widget §1](https://cryptolike.net/docs/widget).
|
|
49
|
+
|
|
50
|
+
The client reads `CRYPTOLIKE_API_KEY_ID` and `CRYPTOLIKE_API_SECRET` from the environment when you
|
|
51
|
+
do not pass them, never logs them and redacts them from `repr(client)` — and therefore from
|
|
52
|
+
`print(client)`, f-strings and most log formatters. `vars(client)` still shows `_secret`.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install cryptolike
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quickstart — to the first confirmed payment
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from cryptolike import Client, ApiError
|
|
64
|
+
|
|
65
|
+
api = Client() # CRYPTOLIKE_API_KEY_ID + CRYPTOLIKE_API_SECRET from the environment
|
|
66
|
+
|
|
67
|
+
# 1. Create a payment on your server; send the customer to checkout_url.
|
|
68
|
+
invoice = api.invoices.create(
|
|
69
|
+
amount="10.50", currency="USD", order_id="A-1001",
|
|
70
|
+
return_url="https://shop.example/orders/A-1001",
|
|
71
|
+
idempotency_key="order-A-1001", # a retry never creates a second payment
|
|
72
|
+
)
|
|
73
|
+
redirect(invoice.checkout_url)
|
|
74
|
+
|
|
75
|
+
# 2. Learn the outcome from the webhook (below) — or poll:
|
|
76
|
+
detail = api.invoices.get(invoice.id)
|
|
77
|
+
if detail.status == "paid":
|
|
78
|
+
fulfil(detail.order_id)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# 3. Webhook receiver (Flask) — verify over the RAW body, dedupe by event_id, answer 2xx fast.
|
|
83
|
+
from flask import request, abort
|
|
84
|
+
from cryptolike import verify_webhook, WebhookVerificationError
|
|
85
|
+
|
|
86
|
+
@app.post("/hooks/cryptolike")
|
|
87
|
+
def hook():
|
|
88
|
+
try:
|
|
89
|
+
event = verify_webhook(request.headers, request.get_data(), os.environ["CRYPTOLIKE_WEBHOOK_SECRET"])
|
|
90
|
+
except WebhookVerificationError:
|
|
91
|
+
abort(401)
|
|
92
|
+
if not already_seen(event.event_id):
|
|
93
|
+
handle(event) # event.type == "invoice.paid" → fulfil
|
|
94
|
+
return "", 200
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Full examples: [`examples/create_payment.py`](https://github.com/Cryptolike-net/cryptolike-python/blob/main/examples/create_payment.py), [`examples/flask_webhook.py`](https://github.com/Cryptolike-net/cryptolike-python/blob/main/examples/flask_webhook.py).
|
|
98
|
+
|
|
99
|
+
## Resources
|
|
100
|
+
|
|
101
|
+
| Resource | Methods |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `api.invoices` | `create(amount, currency, *, coin_id, allowed_coin_ids, description, order_id, ttl_seconds, return_url, webhook_url, fee_paid_by, metadata, idempotency_by_order_id, idempotency_key)`, `get(id)`, `list(**filters)`, `iterate(**filters)`, `cancel(id)`, `export_csv(**filters)` |
|
|
104
|
+
| `api.addresses` | `create(coin_id, customer_id, *, label)`, `get(id, limit=, cursor=)`, `list`, `iterate`, `archive(id)`, `restore(id)` |
|
|
105
|
+
| `api` (archive) | `archive_static_address(id)`, `restore_static_address(id)`, `list_archived_static_addresses(**filters)`, `iterate_archived_static_addresses(**filters)` |
|
|
106
|
+
| `api.transactions` | `list`, `iterate`, `get`, `export_csv` |
|
|
107
|
+
| `api.balances` / `api.currencies` / `api.rates` | `list()` (`rates.list(fiat="EUR")`) |
|
|
108
|
+
| `api.withdrawals` | `estimate(coin_id, amount, *, to_address)`, `create(coin_id, amount, to_address, *, payout_id, comment)`, `get`, `list`, `iterate`, `cancel` |
|
|
109
|
+
| `api.webhooks.endpoints` | `list`, `create(url, events, *, secret)`, `get`, `update(id, *, url, events, enabled)`, `delete`, `rotate_secret` |
|
|
110
|
+
| `api.webhooks.deliveries` | `list`, `get`, `retry` |
|
|
111
|
+
| `api.embed` | `get()`, `set(allowed_origins)` — checkout widget allowlist |
|
|
112
|
+
| `api.request(method, path, query=, body=, idempotency_key=)` | any endpoint, signed |
|
|
113
|
+
|
|
114
|
+
### Static-address archive
|
|
115
|
+
|
|
116
|
+
Archiving stops offering an address and takes it out of real-time scanning; it is never deleted, and its address is never issued again. The platform checks archived addresses for incoming transfers regularly (every 24 h by default): a transfer found is credited as usual and restores the address automatically (webhook `static_address.restored`, reason `deposit`). Addresses with no incoming transfers for the platform's period (180 days by default) are archived automatically after a warning 7 days earlier (`static_address.archive_warning`, then `static_address.archived` with reason `auto`).
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
api.archive_static_address(address_id) # POST /v1/addresses/{id}/archive — a repeat is a no-op
|
|
120
|
+
page = api.list_archived_static_addresses(search="shop") # status=archived; coin_id, customer_id, limit, cursor
|
|
121
|
+
for a in api.iterate_archived_static_addresses():
|
|
122
|
+
print(a.id, a.archive_reason)
|
|
123
|
+
api.restore_static_address(address_id) # 409 conflict + details["active_address_id"]: archive the other active address first
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`event.typed_data()` of a verified `static_address.*` event returns `StaticAddressArchivedData` / `StaticAddressRestoredData` (`reason`, `deposit`, `conflict_with`) / `StaticAddressArchiveWarningData` (`archive_after`, `inactive_since`); constants `STATIC_ADDRESS_ARCHIVED|RESTORED|ARCHIVE_WARNING`. Full example: `examples/static_address_archive.py`, `examples/flask_webhook.py`.
|
|
127
|
+
|
|
128
|
+
List filters are keyword arguments named as in the API (`status="paid"`, `from_="2026-09-01T00:00:00Z"`
|
|
129
|
+
— `from_` because `from` is a keyword). `list()` returns a `Page` (`items`, `next_cursor`);
|
|
130
|
+
`iterate()` walks every page. Unknown response fields are kept in `.raw`.
|
|
131
|
+
|
|
132
|
+
## Payer chooses the coin
|
|
133
|
+
|
|
134
|
+
An invoice in a fiat currency (or a ticker on several networks) without `coin_id` lets the payer
|
|
135
|
+
choose the coin on the payment page; each option shows an estimate (≈) — the exact amount and
|
|
136
|
+
rate are fixed when the payer picks. `allowed_coin_ids` narrows the choice for one invoice; leave
|
|
137
|
+
it out to offer every coin you offer for invoices:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
ids = [c.id for c in api.currencies.list() # signed: each coin carries invoice_enabled
|
|
141
|
+
if c.invoice_enabled is not False and not c.maintenance and c.ticker == "USDT"]
|
|
142
|
+
inv = api.invoices.create(amount="49.90", currency="USD", allowed_coin_ids=ids)
|
|
143
|
+
# inv.allowed_coin_ids — the kept set; None = every coin offered for invoices.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The server keeps the ids you offer for invoices («Available for invoices» in the cabinet → Coins)
|
|
147
|
+
that match `currency`, drops the rest; none left → `ApiError` 422 `no_allowed_coins`
|
|
148
|
+
(`field == "allowed_coin_ids"`). Not combinable with `coin_id` (400). An empty list raises
|
|
149
|
+
`ValueError` before the request — a filter that found nothing never turns into "every coin".
|
|
150
|
+
|
|
151
|
+
## Errors
|
|
152
|
+
|
|
153
|
+
Every non-2xx response raises `ApiError` with `status`, `code` (stable machine code), `message`,
|
|
154
|
+
`details`, `field`, `retry_after` and `request_id`. Transport failures after all retries raise
|
|
155
|
+
`NetworkError`. Codes: [Authentication §5](https://cryptolike.net/docs/auth), [API §5](https://cryptolike.net/docs/api).
|
|
156
|
+
A payout with a key whose `withdraw` a developer requested and an owner/admin has not approved yet is
|
|
157
|
+
`403` `WITHDRAW_PENDING_APPROVAL` (`e.withdraw_pending_approval`): nothing was created, not retried —
|
|
158
|
+
approve the request in the cabinet (API keys); the key's other permissions work meanwhile.
|
|
159
|
+
|
|
160
|
+
## Retries and idempotency
|
|
161
|
+
|
|
162
|
+
`429` (after `Retry-After`, or backoff), `5xx` except `501`, and connection/timeout errors are
|
|
163
|
+
retried up to `max_retries` (default 3) with `min(retry_max, retry_base · 2^attempt)` full-jitter
|
|
164
|
+
backoff. Every `POST` carries an `Idempotency-Key` (yours, or a UUID generated once per call and
|
|
165
|
+
reused on each retry), so a retry never creates a second invoice, address or withdrawal. Each
|
|
166
|
+
attempt is signed with a fresh timestamp. A `401 auth.replay` — two identical requests signed
|
|
167
|
+
within the same second share a signature — waits for the next second and signs afresh.
|
|
168
|
+
|
|
169
|
+
## Options
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
Client(key_id=None, secret=None, *, base_url=None, session=None, timeout=30.0,
|
|
173
|
+
max_retries=3, retry_base=0.5, retry_max=8.0, retry_after_max=30.0,
|
|
174
|
+
api_version="v1", app_info="my-shop/2.3")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Webhook verification
|
|
178
|
+
|
|
179
|
+
`verify_webhook(headers, raw_body, secret, *, tolerance=300, now=None)` returns a `WebhookEvent`
|
|
180
|
+
(`event_id`, `type`, `created_at`, `data`, `raw`) or raises `WebhookVerificationError` (`reason`:
|
|
181
|
+
`missing_header` | `invalid_timestamp` | `timestamp_out_of_tolerance` | `invalid_signature` |
|
|
182
|
+
`invalid_body`). `raw_body` must be the bytes as received (Flask `request.get_data()`, Django
|
|
183
|
+
`request.body`). Answer `401`/`400` on failure, `2xx` on success, and deduplicate by `event_id` —
|
|
184
|
+
deliveries are at least once and may arrive out of order.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# cryptolike (Python SDK)
|
|
2
|
+
|
|
3
|
+
Official **server-side** SDK for the CryptoLike merchant API (Python ≥ 3.10, `requests`; typed, `py.typed`).
|
|
4
|
+
|
|
5
|
+
- Signed requests (`X-API-Key` / `X-Timestamp` / `X-Signature`, HMAC-SHA256 — [Authentication](https://cryptolike.net/docs/auth))
|
|
6
|
+
- `Idempotency-Key` generated for every `POST` (pass your own to make retries exact)
|
|
7
|
+
- Retries with exponential backoff and jitter on `429` (honours `Retry-After`), `5xx` and transport errors
|
|
8
|
+
- Dataclasses for every object; amounts are `Decimal` — floats are refused
|
|
9
|
+
- `verify_webhook()` — signature and timestamp check of webhook deliveries ([Webhooks](https://cryptolike.net/docs/webhooks))
|
|
10
|
+
|
|
11
|
+
## The secret stays on your server
|
|
12
|
+
|
|
13
|
+
> The API secret signs requests *as you*: whoever has it can create payments, read balances and
|
|
14
|
+
> change your webhooks. Use this SDK from your backend only; never put the secret into code that
|
|
15
|
+
> ships to a browser or a mobile app. Your page needs only the invoice id / `checkout_url` your
|
|
16
|
+
> server obtained — see [Payment widget §1](https://cryptolike.net/docs/widget).
|
|
17
|
+
|
|
18
|
+
The client reads `CRYPTOLIKE_API_KEY_ID` and `CRYPTOLIKE_API_SECRET` from the environment when you
|
|
19
|
+
do not pass them, never logs them and redacts them from `repr(client)` — and therefore from
|
|
20
|
+
`print(client)`, f-strings and most log formatters. `vars(client)` still shows `_secret`.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install cryptolike
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quickstart — to the first confirmed payment
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from cryptolike import Client, ApiError
|
|
32
|
+
|
|
33
|
+
api = Client() # CRYPTOLIKE_API_KEY_ID + CRYPTOLIKE_API_SECRET from the environment
|
|
34
|
+
|
|
35
|
+
# 1. Create a payment on your server; send the customer to checkout_url.
|
|
36
|
+
invoice = api.invoices.create(
|
|
37
|
+
amount="10.50", currency="USD", order_id="A-1001",
|
|
38
|
+
return_url="https://shop.example/orders/A-1001",
|
|
39
|
+
idempotency_key="order-A-1001", # a retry never creates a second payment
|
|
40
|
+
)
|
|
41
|
+
redirect(invoice.checkout_url)
|
|
42
|
+
|
|
43
|
+
# 2. Learn the outcome from the webhook (below) — or poll:
|
|
44
|
+
detail = api.invoices.get(invoice.id)
|
|
45
|
+
if detail.status == "paid":
|
|
46
|
+
fulfil(detail.order_id)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# 3. Webhook receiver (Flask) — verify over the RAW body, dedupe by event_id, answer 2xx fast.
|
|
51
|
+
from flask import request, abort
|
|
52
|
+
from cryptolike import verify_webhook, WebhookVerificationError
|
|
53
|
+
|
|
54
|
+
@app.post("/hooks/cryptolike")
|
|
55
|
+
def hook():
|
|
56
|
+
try:
|
|
57
|
+
event = verify_webhook(request.headers, request.get_data(), os.environ["CRYPTOLIKE_WEBHOOK_SECRET"])
|
|
58
|
+
except WebhookVerificationError:
|
|
59
|
+
abort(401)
|
|
60
|
+
if not already_seen(event.event_id):
|
|
61
|
+
handle(event) # event.type == "invoice.paid" → fulfil
|
|
62
|
+
return "", 200
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Full examples: [`examples/create_payment.py`](https://github.com/Cryptolike-net/cryptolike-python/blob/main/examples/create_payment.py), [`examples/flask_webhook.py`](https://github.com/Cryptolike-net/cryptolike-python/blob/main/examples/flask_webhook.py).
|
|
66
|
+
|
|
67
|
+
## Resources
|
|
68
|
+
|
|
69
|
+
| Resource | Methods |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `api.invoices` | `create(amount, currency, *, coin_id, allowed_coin_ids, description, order_id, ttl_seconds, return_url, webhook_url, fee_paid_by, metadata, idempotency_by_order_id, idempotency_key)`, `get(id)`, `list(**filters)`, `iterate(**filters)`, `cancel(id)`, `export_csv(**filters)` |
|
|
72
|
+
| `api.addresses` | `create(coin_id, customer_id, *, label)`, `get(id, limit=, cursor=)`, `list`, `iterate`, `archive(id)`, `restore(id)` |
|
|
73
|
+
| `api` (archive) | `archive_static_address(id)`, `restore_static_address(id)`, `list_archived_static_addresses(**filters)`, `iterate_archived_static_addresses(**filters)` |
|
|
74
|
+
| `api.transactions` | `list`, `iterate`, `get`, `export_csv` |
|
|
75
|
+
| `api.balances` / `api.currencies` / `api.rates` | `list()` (`rates.list(fiat="EUR")`) |
|
|
76
|
+
| `api.withdrawals` | `estimate(coin_id, amount, *, to_address)`, `create(coin_id, amount, to_address, *, payout_id, comment)`, `get`, `list`, `iterate`, `cancel` |
|
|
77
|
+
| `api.webhooks.endpoints` | `list`, `create(url, events, *, secret)`, `get`, `update(id, *, url, events, enabled)`, `delete`, `rotate_secret` |
|
|
78
|
+
| `api.webhooks.deliveries` | `list`, `get`, `retry` |
|
|
79
|
+
| `api.embed` | `get()`, `set(allowed_origins)` — checkout widget allowlist |
|
|
80
|
+
| `api.request(method, path, query=, body=, idempotency_key=)` | any endpoint, signed |
|
|
81
|
+
|
|
82
|
+
### Static-address archive
|
|
83
|
+
|
|
84
|
+
Archiving stops offering an address and takes it out of real-time scanning; it is never deleted, and its address is never issued again. The platform checks archived addresses for incoming transfers regularly (every 24 h by default): a transfer found is credited as usual and restores the address automatically (webhook `static_address.restored`, reason `deposit`). Addresses with no incoming transfers for the platform's period (180 days by default) are archived automatically after a warning 7 days earlier (`static_address.archive_warning`, then `static_address.archived` with reason `auto`).
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
api.archive_static_address(address_id) # POST /v1/addresses/{id}/archive — a repeat is a no-op
|
|
88
|
+
page = api.list_archived_static_addresses(search="shop") # status=archived; coin_id, customer_id, limit, cursor
|
|
89
|
+
for a in api.iterate_archived_static_addresses():
|
|
90
|
+
print(a.id, a.archive_reason)
|
|
91
|
+
api.restore_static_address(address_id) # 409 conflict + details["active_address_id"]: archive the other active address first
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`event.typed_data()` of a verified `static_address.*` event returns `StaticAddressArchivedData` / `StaticAddressRestoredData` (`reason`, `deposit`, `conflict_with`) / `StaticAddressArchiveWarningData` (`archive_after`, `inactive_since`); constants `STATIC_ADDRESS_ARCHIVED|RESTORED|ARCHIVE_WARNING`. Full example: `examples/static_address_archive.py`, `examples/flask_webhook.py`.
|
|
95
|
+
|
|
96
|
+
List filters are keyword arguments named as in the API (`status="paid"`, `from_="2026-09-01T00:00:00Z"`
|
|
97
|
+
— `from_` because `from` is a keyword). `list()` returns a `Page` (`items`, `next_cursor`);
|
|
98
|
+
`iterate()` walks every page. Unknown response fields are kept in `.raw`.
|
|
99
|
+
|
|
100
|
+
## Payer chooses the coin
|
|
101
|
+
|
|
102
|
+
An invoice in a fiat currency (or a ticker on several networks) without `coin_id` lets the payer
|
|
103
|
+
choose the coin on the payment page; each option shows an estimate (≈) — the exact amount and
|
|
104
|
+
rate are fixed when the payer picks. `allowed_coin_ids` narrows the choice for one invoice; leave
|
|
105
|
+
it out to offer every coin you offer for invoices:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
ids = [c.id for c in api.currencies.list() # signed: each coin carries invoice_enabled
|
|
109
|
+
if c.invoice_enabled is not False and not c.maintenance and c.ticker == "USDT"]
|
|
110
|
+
inv = api.invoices.create(amount="49.90", currency="USD", allowed_coin_ids=ids)
|
|
111
|
+
# inv.allowed_coin_ids — the kept set; None = every coin offered for invoices.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The server keeps the ids you offer for invoices («Available for invoices» in the cabinet → Coins)
|
|
115
|
+
that match `currency`, drops the rest; none left → `ApiError` 422 `no_allowed_coins`
|
|
116
|
+
(`field == "allowed_coin_ids"`). Not combinable with `coin_id` (400). An empty list raises
|
|
117
|
+
`ValueError` before the request — a filter that found nothing never turns into "every coin".
|
|
118
|
+
|
|
119
|
+
## Errors
|
|
120
|
+
|
|
121
|
+
Every non-2xx response raises `ApiError` with `status`, `code` (stable machine code), `message`,
|
|
122
|
+
`details`, `field`, `retry_after` and `request_id`. Transport failures after all retries raise
|
|
123
|
+
`NetworkError`. Codes: [Authentication §5](https://cryptolike.net/docs/auth), [API §5](https://cryptolike.net/docs/api).
|
|
124
|
+
A payout with a key whose `withdraw` a developer requested and an owner/admin has not approved yet is
|
|
125
|
+
`403` `WITHDRAW_PENDING_APPROVAL` (`e.withdraw_pending_approval`): nothing was created, not retried —
|
|
126
|
+
approve the request in the cabinet (API keys); the key's other permissions work meanwhile.
|
|
127
|
+
|
|
128
|
+
## Retries and idempotency
|
|
129
|
+
|
|
130
|
+
`429` (after `Retry-After`, or backoff), `5xx` except `501`, and connection/timeout errors are
|
|
131
|
+
retried up to `max_retries` (default 3) with `min(retry_max, retry_base · 2^attempt)` full-jitter
|
|
132
|
+
backoff. Every `POST` carries an `Idempotency-Key` (yours, or a UUID generated once per call and
|
|
133
|
+
reused on each retry), so a retry never creates a second invoice, address or withdrawal. Each
|
|
134
|
+
attempt is signed with a fresh timestamp. A `401 auth.replay` — two identical requests signed
|
|
135
|
+
within the same second share a signature — waits for the next second and signs afresh.
|
|
136
|
+
|
|
137
|
+
## Options
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
Client(key_id=None, secret=None, *, base_url=None, session=None, timeout=30.0,
|
|
141
|
+
max_retries=3, retry_base=0.5, retry_max=8.0, retry_after_max=30.0,
|
|
142
|
+
api_version="v1", app_info="my-shop/2.3")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Webhook verification
|
|
146
|
+
|
|
147
|
+
`verify_webhook(headers, raw_body, secret, *, tolerance=300, now=None)` returns a `WebhookEvent`
|
|
148
|
+
(`event_id`, `type`, `created_at`, `data`, `raw`) or raises `WebhookVerificationError` (`reason`:
|
|
149
|
+
`missing_header` | `invalid_timestamp` | `timestamp_out_of_tolerance` | `invalid_signature` |
|
|
150
|
+
`invalid_body`). `raw_body` must be the bytes as received (Flask `request.get_data()`, Django
|
|
151
|
+
`request.body`). Answer `401`/`400` on failure, `2xx` on success, and deduplicate by `event_id` —
|
|
152
|
+
deliveries are at least once and may arrive out of order.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cryptolike"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official server-side SDK for the CryptoLike merchant API: signed requests, automatic Idempotency-Key, retries with backoff, typed resources and webhook verification."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT" # SPDX (PEP 639)
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "CryptoLike team" }]
|
|
14
|
+
keywords = ["cryptolike", "crypto", "cryptocurrency", "crypto payments", "payment gateway", "payments", "bitcoin", "usdt", "invoices", "webhooks", "hmac", "api client", "sdk"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Topic :: Office/Business :: Financial",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = ["requests>=2.33.0", "urllib3>=2.8.0"]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
test = ["pytest>=7"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/Cryptolike-net/cryptolike-python"
|
|
36
|
+
Documentation = "https://cryptolike.net/docs/sdk"
|
|
37
|
+
Repository = "https://github.com/Cryptolike-net/cryptolike-python"
|
|
38
|
+
Issues = "https://github.com/Cryptolike-net/cryptolike-python/issues"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
cryptolike = ["py.typed"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""cryptolike — official server-side SDK for the CryptoLike merchant API.
|
|
2
|
+
|
|
3
|
+
from cryptolike import Client, verify_webhook
|
|
4
|
+
|
|
5
|
+
api = Client() # CRYPTOLIKE_API_KEY_ID + CRYPTOLIKE_API_SECRET from the environment
|
|
6
|
+
invoice = api.invoices.create(amount="10.50", currency="USD", order_id="A-1001")
|
|
7
|
+
|
|
8
|
+
The API secret must stay on your server (https://cryptolike.net/docs/widget §1).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from ._version import USER_AGENT, VERSION
|
|
12
|
+
from .auth import body_sha256, canonical_string, sign
|
|
13
|
+
from .client import Client
|
|
14
|
+
from .errors import WITHDRAW_PENDING_APPROVAL, ApiError, CryptoLikeError, NetworkError, WebhookVerificationError
|
|
15
|
+
from .types import (
|
|
16
|
+
Balance,
|
|
17
|
+
Currency,
|
|
18
|
+
EmbedSettings,
|
|
19
|
+
Invoice,
|
|
20
|
+
InvoiceEvent,
|
|
21
|
+
Page,
|
|
22
|
+
Rate,
|
|
23
|
+
StaticAddress,
|
|
24
|
+
StaticAddressArchivedData,
|
|
25
|
+
StaticAddressArchiveWarningData,
|
|
26
|
+
StaticAddressCard,
|
|
27
|
+
StaticAddressRestoreDeposit,
|
|
28
|
+
StaticAddressRestoredData,
|
|
29
|
+
Transaction,
|
|
30
|
+
WebhookDelivery,
|
|
31
|
+
WebhookEndpoint,
|
|
32
|
+
WebhookEndpointCreated,
|
|
33
|
+
WebhookStaticAddress,
|
|
34
|
+
Withdrawal,
|
|
35
|
+
WithdrawalEstimate,
|
|
36
|
+
)
|
|
37
|
+
from .webhook import (
|
|
38
|
+
EVENT_DATA_TYPES,
|
|
39
|
+
STATIC_ADDRESS_ARCHIVE_WARNING,
|
|
40
|
+
STATIC_ADDRESS_ARCHIVED,
|
|
41
|
+
STATIC_ADDRESS_RESTORED,
|
|
42
|
+
WebhookEvent,
|
|
43
|
+
verify_webhook,
|
|
44
|
+
webhook_signature,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__version__ = VERSION
|
|
48
|
+
__all__ = [
|
|
49
|
+
"Client",
|
|
50
|
+
"ApiError",
|
|
51
|
+
"WITHDRAW_PENDING_APPROVAL",
|
|
52
|
+
"CryptoLikeError",
|
|
53
|
+
"NetworkError",
|
|
54
|
+
"WebhookVerificationError",
|
|
55
|
+
"WebhookEvent",
|
|
56
|
+
"verify_webhook",
|
|
57
|
+
"webhook_signature",
|
|
58
|
+
"EVENT_DATA_TYPES",
|
|
59
|
+
"STATIC_ADDRESS_ARCHIVED",
|
|
60
|
+
"STATIC_ADDRESS_RESTORED",
|
|
61
|
+
"STATIC_ADDRESS_ARCHIVE_WARNING",
|
|
62
|
+
"sign",
|
|
63
|
+
"canonical_string",
|
|
64
|
+
"body_sha256",
|
|
65
|
+
"Balance",
|
|
66
|
+
"Currency",
|
|
67
|
+
"EmbedSettings",
|
|
68
|
+
"Invoice",
|
|
69
|
+
"InvoiceEvent",
|
|
70
|
+
"Page",
|
|
71
|
+
"Rate",
|
|
72
|
+
"StaticAddress",
|
|
73
|
+
"StaticAddressCard",
|
|
74
|
+
"StaticAddressArchivedData",
|
|
75
|
+
"StaticAddressRestoredData",
|
|
76
|
+
"StaticAddressArchiveWarningData",
|
|
77
|
+
"StaticAddressRestoreDeposit",
|
|
78
|
+
"WebhookStaticAddress",
|
|
79
|
+
"Transaction",
|
|
80
|
+
"WebhookDelivery",
|
|
81
|
+
"WebhookEndpoint",
|
|
82
|
+
"WebhookEndpointCreated",
|
|
83
|
+
"Withdrawal",
|
|
84
|
+
"WithdrawalEstimate",
|
|
85
|
+
"VERSION",
|
|
86
|
+
"USER_AGENT",
|
|
87
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Request signing (https://cryptolike.net/docs/auth §3). Runs on your server only — the secret never leaves it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
Bytes = Union[bytes, bytearray, memoryview]
|
|
10
|
+
|
|
11
|
+
EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _to_bytes(body: Union[str, Bytes]) -> bytes:
|
|
15
|
+
return body.encode("utf-8") if isinstance(body, str) else bytes(body)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def body_sha256(body: Union[str, Bytes] = b"") -> str:
|
|
19
|
+
"""Lower-case hex SHA-256 of the raw body bytes (a string is taken as UTF-8)."""
|
|
20
|
+
data = _to_bytes(body)
|
|
21
|
+
return EMPTY_SHA256 if not data else hashlib.sha256(data).hexdigest()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def canonical_string(timestamp: Union[int, str], method: str, path_query: str, body: Union[str, Bytes] = b"") -> str:
|
|
25
|
+
"""Four lines joined with LF, no trailing newline. ``path_query`` is the target exactly as sent."""
|
|
26
|
+
return "\n".join([str(timestamp), method.upper(), path_query, body_sha256(body)])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def sign(secret: str, timestamp: Union[int, str], method: str, path_query: str, body: Union[str, Bytes] = b"") -> str:
|
|
30
|
+
"""``X-Signature``: hex HMAC-SHA256 of the canonical string, keyed with the secret as shown in the cabinet."""
|
|
31
|
+
canonical = canonical_string(timestamp, method, path_query, body)
|
|
32
|
+
return hmac.new(secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256).hexdigest()
|