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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,4 @@
1
+ """Package version — kept in sync with pyproject.toml by tests/test_version.py."""
2
+
3
+ VERSION = "0.1.0"
4
+ USER_AGENT = f"cryptolike-sdk-python/{VERSION}"
@@ -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()