pyXRocketAPI 0.1.0__tar.gz → 0.2.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.
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/PKG-INFO +6 -2
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/README.md +5 -1
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI/__init__.py +2 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI/exceptions.py +16 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI/models.py +113 -113
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI/version.py +1 -1
- pyxrocketapi-0.2.0/pyXRocketAPI/webhooks.py +480 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI.egg-info/PKG-INFO +6 -2
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI.egg-info/SOURCES.txt +3 -1
- pyxrocketapi-0.2.0/tests/test_webhooks.py +105 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/LICENSE +0 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI/api.py +0 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI.egg-info/dependency_links.txt +0 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI.egg-info/requires.txt +0 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyXRocketAPI.egg-info/top_level.txt +0 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/pyproject.toml +0 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/setup.cfg +0 -0
- {pyxrocketapi-0.1.0 → pyxrocketapi-0.2.0}/tests/test_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyXRocketAPI
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python client for the xRocket Pay API
|
|
5
5
|
Author: Badiboy
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,7 +17,11 @@ License-File: LICENSE
|
|
|
17
17
|
Requires-Dist: requests>=2.28
|
|
18
18
|
Dynamic: license-file
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
[](https://pypi.python.org/pypi/pyXRocketAPI)
|
|
21
|
+
[](https://pypi.python.org/pypi/pyXRocketAPI)
|
|
22
|
+
[](https://pypi.org/project/pyXRocketAPI/)
|
|
23
|
+
|
|
24
|
+
# <p align="center">pyXRocketAPI</p>
|
|
21
25
|
|
|
22
26
|
**pyXRocketAPI** is a Python client for the [xRocket Pay API](https://docs.xrocket.exchange/api/pay/pay-api-overview). It supports application information, balances, invoices, cheques, payouts, withdrawals, currency rates, and health checks.
|
|
23
27
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
[](https://pypi.python.org/pypi/pyXRocketAPI)
|
|
2
|
+
[](https://pypi.python.org/pypi/pyXRocketAPI)
|
|
3
|
+
[](https://pypi.org/project/pyXRocketAPI/)
|
|
4
|
+
|
|
5
|
+
# <p align="center">pyXRocketAPI</p>
|
|
2
6
|
|
|
3
7
|
**pyXRocketAPI** is a Python client for the [xRocket Pay API](https://docs.xrocket.exchange/api/pay/pay-api-overview). It supports application information, balances, invoices, cheques, payouts, withdrawals, currency rates, and health checks.
|
|
4
8
|
|
|
@@ -20,3 +20,19 @@ class xRocketAPIException(Exception):
|
|
|
20
20
|
self.instance = instance
|
|
21
21
|
self.kind = kind
|
|
22
22
|
self.info = info
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class xRocketWebhookException(ValueError):
|
|
26
|
+
"""Base class for errors while validating or parsing an xRocket webhook."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class xRocketWebhookParseException(xRocketWebhookException):
|
|
30
|
+
"""Webhook body is not valid JSON or does not have the expected structure."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class xRocketWebhookSignatureException(xRocketWebhookException):
|
|
34
|
+
"""Webhook signature is absent or does not match the supplied secret."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class xRocketWebhookUnsupportedVersionException(xRocketWebhookSignatureException):
|
|
38
|
+
"""Webhook uses a signature scheme version unsupported by this library."""
|
|
@@ -68,8 +68,8 @@ class App(xRocketObject):
|
|
|
68
68
|
|
|
69
69
|
API: https://docs.xrocket.exchange/api/pay/reference/http/app-controller-get-app
|
|
70
70
|
|
|
71
|
-
:
|
|
72
|
-
:
|
|
71
|
+
:param id: No description is provided.
|
|
72
|
+
:param name: Name of current app.
|
|
73
73
|
"""
|
|
74
74
|
def __init__(self):
|
|
75
75
|
self.id = None
|
|
@@ -87,10 +87,10 @@ class Balance(xRocketObject):
|
|
|
87
87
|
|
|
88
88
|
API: https://docs.xrocket.exchange/api/pay/reference/http/app-controller-get-app-balances
|
|
89
89
|
|
|
90
|
-
:
|
|
91
|
-
:
|
|
92
|
-
:
|
|
93
|
-
:
|
|
90
|
+
:param asset: Balance asset.
|
|
91
|
+
:param balance: Asset balance.
|
|
92
|
+
:param available: Available balance.
|
|
93
|
+
:param holds: Holds balance.
|
|
94
94
|
"""
|
|
95
95
|
def __init__(self):
|
|
96
96
|
self.asset = None
|
|
@@ -110,9 +110,9 @@ class InvoiceLinks(xRocketObject):
|
|
|
110
110
|
|
|
111
111
|
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice
|
|
112
112
|
|
|
113
|
-
:
|
|
114
|
-
:
|
|
115
|
-
:
|
|
113
|
+
:param telegramBotLink: Invoice telegram bot link.
|
|
114
|
+
:param telegramMiniAppLink: Invoice telegram mini app link.
|
|
115
|
+
:param webLink: Invoice web link.
|
|
116
116
|
"""
|
|
117
117
|
def __init__(self):
|
|
118
118
|
self.telegramBotLink = None
|
|
@@ -131,21 +131,21 @@ class Invoice(xRocketObject):
|
|
|
131
131
|
|
|
132
132
|
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice
|
|
133
133
|
|
|
134
|
-
:
|
|
135
|
-
:
|
|
136
|
-
:
|
|
137
|
-
:
|
|
138
|
-
:
|
|
139
|
-
:
|
|
140
|
-
:
|
|
141
|
-
:
|
|
142
|
-
:
|
|
143
|
-
:
|
|
144
|
-
:
|
|
145
|
-
:
|
|
146
|
-
:
|
|
147
|
-
:
|
|
148
|
-
:
|
|
134
|
+
:param id: Invoice id.
|
|
135
|
+
:param priceAmount: Invoice price amount.
|
|
136
|
+
:param minPayment: Minimum payment amount (for open-amount invoices).
|
|
137
|
+
:param priceCurrency: Invoice price currency (crypto or fiat).
|
|
138
|
+
:param payCurrencies: Currencies which can be used to pay the invoice (crypto or fiat).
|
|
139
|
+
:param clientInvoiceId: Your unique identifier for this invoice to link with your internal system (e.g., order ID).
|
|
140
|
+
:param description: Description for invoice.
|
|
141
|
+
:param expiresIn: Invoice expires in milliseconds (from creation time).
|
|
142
|
+
:param createdAt: Invoice creation time.
|
|
143
|
+
:param expiresAt: Invoice expiration time (null if no expiration).
|
|
144
|
+
:param status: Invoice status. **WARNING**: This list may be extended in the future. Always use exact status comparison and handle unknown statuses gracefully.
|
|
145
|
+
:param callback: No description is provided.
|
|
146
|
+
:param url: No description is provided.
|
|
147
|
+
:param customer: No description is provided.
|
|
148
|
+
:param links: No description is provided.
|
|
149
149
|
"""
|
|
150
150
|
def __init__(self):
|
|
151
151
|
self.id = None
|
|
@@ -179,9 +179,9 @@ class ChequeLinks(xRocketObject):
|
|
|
179
179
|
|
|
180
180
|
API: https://docs.xrocket.exchange/api/pay/reference/http/cheque-controller-get-cheque
|
|
181
181
|
|
|
182
|
-
:
|
|
183
|
-
:
|
|
184
|
-
:
|
|
182
|
+
:param telegramBotLink: Cheque activation telegram bot link.
|
|
183
|
+
:param telegramMiniAppLink: Cheque activation telegram mini app link (soon).
|
|
184
|
+
:param webLink: Cheque activation web link (soon).
|
|
185
185
|
"""
|
|
186
186
|
def __init__(self):
|
|
187
187
|
self.telegramBotLink = None
|
|
@@ -200,17 +200,17 @@ class Cheque(xRocketObject):
|
|
|
200
200
|
|
|
201
201
|
API: https://docs.xrocket.exchange/api/pay/reference/http/cheque-controller-get-cheque
|
|
202
202
|
|
|
203
|
-
:
|
|
204
|
-
:
|
|
205
|
-
:
|
|
206
|
-
:
|
|
207
|
-
:
|
|
208
|
-
:
|
|
209
|
-
:
|
|
210
|
-
:
|
|
211
|
-
:
|
|
212
|
-
:
|
|
213
|
-
:
|
|
203
|
+
:param chequeId: Cheque ID.
|
|
204
|
+
:param clientChequeId: Unique cheque ID in your system to prevent double spends.
|
|
205
|
+
:param asset: Currency of transfer.
|
|
206
|
+
:param description: Description for cheque.
|
|
207
|
+
:param targetType: Target type for cheque.
|
|
208
|
+
:param target: Target for cheque.
|
|
209
|
+
:param links: Cheque activation links.
|
|
210
|
+
:param state: Cheque state.
|
|
211
|
+
:param deleted: Cheque is cancelled and the reserved funds are returned to the application balance.
|
|
212
|
+
:param callback: No description is provided.
|
|
213
|
+
:param url: No description is provided.
|
|
214
214
|
"""
|
|
215
215
|
def __init__(self):
|
|
216
216
|
self.chequeId = None
|
|
@@ -240,15 +240,15 @@ class Payout(xRocketObject):
|
|
|
240
240
|
|
|
241
241
|
API: https://docs.xrocket.exchange/api/pay/reference/http/payout-controller-get-payout
|
|
242
242
|
|
|
243
|
-
:
|
|
244
|
-
:
|
|
245
|
-
:
|
|
246
|
-
:
|
|
247
|
-
:
|
|
248
|
-
:
|
|
249
|
-
:
|
|
250
|
-
:
|
|
251
|
-
:
|
|
243
|
+
:param payoutId: Payout ID.
|
|
244
|
+
:param clientPayoutId: Unique payout ID in your system to prevent double spends.
|
|
245
|
+
:param target: Target.
|
|
246
|
+
:param targetType: Target type.
|
|
247
|
+
:param asset: Asset of payout.
|
|
248
|
+
:param amount: Payout amount.
|
|
249
|
+
:param description: Payout description.
|
|
250
|
+
:param status: Payout status.
|
|
251
|
+
:param callback: Webhook settings of this payout.
|
|
252
252
|
"""
|
|
253
253
|
def __init__(self):
|
|
254
254
|
self.payoutId = None
|
|
@@ -273,16 +273,16 @@ class Withdrawal(xRocketObject):
|
|
|
273
273
|
|
|
274
274
|
API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-controller-get-withdrawal
|
|
275
275
|
|
|
276
|
-
:
|
|
277
|
-
:
|
|
278
|
-
:
|
|
279
|
-
:
|
|
280
|
-
:
|
|
281
|
-
:
|
|
282
|
-
:
|
|
283
|
-
:
|
|
284
|
-
:
|
|
285
|
-
:
|
|
276
|
+
:param withdrawalId: Unique withdrawal ID in your system to prevent double spends.
|
|
277
|
+
:param network: Network code.
|
|
278
|
+
:param address: Withdrawal address.
|
|
279
|
+
:param asset: Asset code.
|
|
280
|
+
:param amount: Withdrawal amount. 9 decimal places, others cut off.
|
|
281
|
+
:param status: Withdrawal status.
|
|
282
|
+
:param comment: Withdrawal comment.
|
|
283
|
+
:param txHash: Withdrawal TX hash. Provided only after withdrawal.
|
|
284
|
+
:param txLink: Withdrawal TX link. Provided only after withdrawal.
|
|
285
|
+
:param callback: Webhook settings of this withdrawal.
|
|
286
286
|
"""
|
|
287
287
|
def __init__(self):
|
|
288
288
|
self.withdrawalId = None
|
|
@@ -308,7 +308,7 @@ class CurrencyNetwork(xRocketObject):
|
|
|
308
308
|
|
|
309
309
|
API: https://docs.xrocket.exchange/api/pay/reference/http/currencies-controller-get-currencies
|
|
310
310
|
|
|
311
|
-
:
|
|
311
|
+
:param code: Network code.
|
|
312
312
|
"""
|
|
313
313
|
def __init__(self):
|
|
314
314
|
self.code = None
|
|
@@ -325,10 +325,10 @@ class Currency(xRocketObject):
|
|
|
325
325
|
|
|
326
326
|
API: https://docs.xrocket.exchange/api/pay/reference/http/currencies-controller-get-currencies
|
|
327
327
|
|
|
328
|
-
:
|
|
329
|
-
:
|
|
330
|
-
:
|
|
331
|
-
:
|
|
328
|
+
:param code: No description is provided.
|
|
329
|
+
:param title: No description is provided.
|
|
330
|
+
:param kind: No description is provided.
|
|
331
|
+
:param networks: No description is provided.
|
|
332
332
|
"""
|
|
333
333
|
def __init__(self):
|
|
334
334
|
self.code = None
|
|
@@ -350,8 +350,8 @@ class Rate(xRocketObject):
|
|
|
350
350
|
|
|
351
351
|
API: https://docs.xrocket.exchange/api/pay/reference/http/rate-controller-get-rates
|
|
352
352
|
|
|
353
|
-
:
|
|
354
|
-
:
|
|
353
|
+
:param currency: No description is provided.
|
|
354
|
+
:param rate: Current rate.
|
|
355
355
|
"""
|
|
356
356
|
def __init__(self):
|
|
357
357
|
self.currency = None
|
|
@@ -369,8 +369,8 @@ class xPage(xRocketObject):
|
|
|
369
369
|
|
|
370
370
|
This is an SDK base model; the API has no standalone page endpoint.
|
|
371
371
|
|
|
372
|
-
:
|
|
373
|
-
:
|
|
372
|
+
:param items: No description is provided.
|
|
373
|
+
:param pagination: No description is provided.
|
|
374
374
|
"""
|
|
375
375
|
def __init__(self):
|
|
376
376
|
self.items = []
|
|
@@ -388,8 +388,8 @@ class CursorPagination(xRocketObject):
|
|
|
388
388
|
|
|
389
389
|
This shared response fragment has no standalone API endpoint.
|
|
390
390
|
|
|
391
|
-
:
|
|
392
|
-
:
|
|
391
|
+
:param total: Total quantity of campaigns.
|
|
392
|
+
:param next: Cursor for the next page of results.
|
|
393
393
|
"""
|
|
394
394
|
def __init__(self):
|
|
395
395
|
self.total = None
|
|
@@ -407,7 +407,7 @@ class InvoicePaymentsPagination(xRocketObject):
|
|
|
407
407
|
|
|
408
408
|
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice-payments
|
|
409
409
|
|
|
410
|
-
:
|
|
410
|
+
:param next: Cursor for the next page of results.
|
|
411
411
|
"""
|
|
412
412
|
def __init__(self):
|
|
413
413
|
self.next = None
|
|
@@ -424,14 +424,14 @@ class InvoicePayment(xRocketObject):
|
|
|
424
424
|
|
|
425
425
|
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice-payments
|
|
426
426
|
|
|
427
|
-
:
|
|
428
|
-
:
|
|
429
|
-
:
|
|
430
|
-
:
|
|
431
|
-
:
|
|
432
|
-
:
|
|
433
|
-
:
|
|
434
|
-
:
|
|
427
|
+
:param id: Unique payment id. Use it to match webhook events with the invoice payments endpoint.
|
|
428
|
+
:param status: Invoice status. **WARNING**: This list may be extended in the future. Always use exact status comparison and handle unknown statuses gracefully.
|
|
429
|
+
:param finalizedAt: When payment reached final state (null for in-progress payments).
|
|
430
|
+
:param payAmount: Gross amount payer sent in total across all transactions of this payment (before fees).
|
|
431
|
+
:param payCurrency: Currency payer used.
|
|
432
|
+
:param receiveAmount: Net amount merchant receives after fees, summed across all transactions of this payment (in invoice priceCurrency).
|
|
433
|
+
:param receiveCurrency: Currency merchant receives (= invoice priceCurrency).
|
|
434
|
+
:param transactions: No description is provided.
|
|
435
435
|
"""
|
|
436
436
|
def __init__(self):
|
|
437
437
|
self.id = None
|
|
@@ -455,8 +455,8 @@ class InvoicesList(xPage):
|
|
|
455
455
|
|
|
456
456
|
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoices
|
|
457
457
|
|
|
458
|
-
:
|
|
459
|
-
:
|
|
458
|
+
:param items: No description is provided.
|
|
459
|
+
:param pagination: No description is provided.
|
|
460
460
|
"""
|
|
461
461
|
@classmethod
|
|
462
462
|
def de_json(cls, json_dict):
|
|
@@ -474,8 +474,8 @@ class InvoicePaymentsList(xPage):
|
|
|
474
474
|
|
|
475
475
|
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-get-invoice-payments
|
|
476
476
|
|
|
477
|
-
:
|
|
478
|
-
:
|
|
477
|
+
:param items: No description is provided.
|
|
478
|
+
:param pagination: No description is provided.
|
|
479
479
|
"""
|
|
480
480
|
@classmethod
|
|
481
481
|
def de_json(cls, json_dict):
|
|
@@ -493,8 +493,8 @@ class ChequesList(xPage):
|
|
|
493
493
|
|
|
494
494
|
API: https://docs.xrocket.exchange/api/pay/reference/http/cheque-controller-get-cheques
|
|
495
495
|
|
|
496
|
-
:
|
|
497
|
-
:
|
|
496
|
+
:param items: No description is provided.
|
|
497
|
+
:param pagination: No description is provided.
|
|
498
498
|
"""
|
|
499
499
|
@classmethod
|
|
500
500
|
def de_json(cls, json_dict):
|
|
@@ -512,8 +512,8 @@ class PayoutsList(xPage):
|
|
|
512
512
|
|
|
513
513
|
API: https://docs.xrocket.exchange/api/pay/reference/http/payout-controller-get-list-payouts
|
|
514
514
|
|
|
515
|
-
:
|
|
516
|
-
:
|
|
515
|
+
:param items: No description is provided.
|
|
516
|
+
:param pagination: No description is provided.
|
|
517
517
|
"""
|
|
518
518
|
@classmethod
|
|
519
519
|
def de_json(cls, json_dict):
|
|
@@ -531,8 +531,8 @@ class WithdrawalsList(xPage):
|
|
|
531
531
|
|
|
532
532
|
API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-controller-get-withdrawals
|
|
533
533
|
|
|
534
|
-
:
|
|
535
|
-
:
|
|
534
|
+
:param items: No description is provided.
|
|
535
|
+
:param pagination: No description is provided.
|
|
536
536
|
"""
|
|
537
537
|
@classmethod
|
|
538
538
|
def de_json(cls, json_dict):
|
|
@@ -550,11 +550,11 @@ class InvoicePaymentAddress(xRocketObject):
|
|
|
550
550
|
|
|
551
551
|
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-payment-controller-create-invoice-payment-address
|
|
552
552
|
|
|
553
|
-
:
|
|
554
|
-
:
|
|
555
|
-
:
|
|
556
|
-
:
|
|
557
|
-
:
|
|
553
|
+
:param address: Deposit address.
|
|
554
|
+
:param payCurrency: Currency code.
|
|
555
|
+
:param payNetwork: Network code.
|
|
556
|
+
:param expiresAt: Payment expired at.
|
|
557
|
+
:param minAmount: Minimum deposit amount (invoice has no fixed amount).
|
|
558
558
|
"""
|
|
559
559
|
def __init__(self):
|
|
560
560
|
self.address = None
|
|
@@ -575,10 +575,10 @@ class WithdrawalQuotas(xRocketObject):
|
|
|
575
575
|
|
|
576
576
|
API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-controller-get-withdrawal-fees
|
|
577
577
|
|
|
578
|
-
:
|
|
579
|
-
:
|
|
580
|
-
:
|
|
581
|
-
:
|
|
578
|
+
:param withdrawMinSize: Minimum withdrawal amount.
|
|
579
|
+
:param withdrawFee: Withdrawal fee.
|
|
580
|
+
:param withdrawFeeAsset: Withdrawal fee asset.
|
|
581
|
+
:param precision: Floating point precision.
|
|
582
582
|
"""
|
|
583
583
|
def __init__(self):
|
|
584
584
|
self.withdrawMinSize = None
|
|
@@ -598,9 +598,9 @@ class WithdrawalLink(xRocketObject):
|
|
|
598
598
|
|
|
599
599
|
API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-links-controller-create-withdrawal-link
|
|
600
600
|
|
|
601
|
-
:
|
|
602
|
-
:
|
|
603
|
-
:
|
|
601
|
+
:param telegramBotLink: Withdrawal telegram bot link.
|
|
602
|
+
:param telegramMiniAppLink: Withdrawal telegram mini app link (soon).
|
|
603
|
+
:param webLink: Withdrawal web link (soon).
|
|
604
604
|
"""
|
|
605
605
|
def __init__(self):
|
|
606
606
|
self.telegramBotLink = None
|
|
@@ -619,12 +619,12 @@ class MassPayoutReason(xRocketObject):
|
|
|
619
619
|
|
|
620
620
|
API: https://docs.xrocket.exchange/api/pay/reference/http/mass-payouts-controller-create-mass-payouts
|
|
621
621
|
|
|
622
|
-
:
|
|
623
|
-
:
|
|
624
|
-
:
|
|
625
|
-
:
|
|
626
|
-
:
|
|
627
|
-
:
|
|
622
|
+
:param type: A URI reference that identifies the problem type.
|
|
623
|
+
:param title: A short, human-readable summary of the problem type.
|
|
624
|
+
:param status: HTTP status code.
|
|
625
|
+
:param detail: A human-readable explanation specific to this occurrence of the problem.
|
|
626
|
+
:param instance: A URI reference that identifies the specific occurrence of the problem.
|
|
627
|
+
:param kind: Problem category for easier error handling.
|
|
628
628
|
"""
|
|
629
629
|
def __init__(self):
|
|
630
630
|
self.type = None
|
|
@@ -646,12 +646,12 @@ class MassPayoutError(xRocketObject):
|
|
|
646
646
|
|
|
647
647
|
API: https://docs.xrocket.exchange/api/pay/reference/http/mass-payouts-controller-create-mass-payouts
|
|
648
648
|
|
|
649
|
-
:
|
|
650
|
-
:
|
|
651
|
-
:
|
|
652
|
-
:
|
|
653
|
-
:
|
|
654
|
-
:
|
|
649
|
+
:param target: Target.
|
|
650
|
+
:param targetType: Target type (only TelegramUserId is supported for mass payouts).
|
|
651
|
+
:param amount: Payout amount.
|
|
652
|
+
:param clientPayoutId: Unique payout ID in your system to prevent double spends.
|
|
653
|
+
:param description: Payout description.
|
|
654
|
+
:param reason: Payout error reason.
|
|
655
655
|
"""
|
|
656
656
|
def __init__(self):
|
|
657
657
|
self.target = None
|
|
@@ -676,8 +676,8 @@ class MassPayouts(xRocketObject):
|
|
|
676
676
|
|
|
677
677
|
API: https://docs.xrocket.exchange/api/pay/reference/http/mass-payouts-controller-create-mass-payouts
|
|
678
678
|
|
|
679
|
-
:
|
|
680
|
-
:
|
|
679
|
+
:param successPayouts: Successful payouts.
|
|
680
|
+
:param errorPayouts: Error payouts.
|
|
681
681
|
"""
|
|
682
682
|
def __init__(self):
|
|
683
683
|
self.successPayouts = []
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""Verification and parsing of xRocket Pay webhooks.
|
|
2
|
+
|
|
3
|
+
API: https://docs.xrocket.exchange/api/pay/pay-api-overview
|
|
4
|
+
|
|
5
|
+
This module does not receive HTTP requests and does not depend on Flask,
|
|
6
|
+
FastAPI, Django, or another web framework. Pass the request body and headers
|
|
7
|
+
from the HTTP layer that your application already uses.
|
|
8
|
+
|
|
9
|
+
Always verify a webhook before parsing or acting on it. The verifier requires
|
|
10
|
+
the original request body, preferably as ``bytes``. Do not pass a JSON object
|
|
11
|
+
that was decoded and serialized again: whitespace, field order, or escaping
|
|
12
|
+
can change and invalidate the signature.
|
|
13
|
+
|
|
14
|
+
For applications using the current xRocket Pay API, xRocket sends these
|
|
15
|
+
headers:
|
|
16
|
+
|
|
17
|
+
* ``Signature-Timestamp``: Unix timestamp in milliseconds when the webhook was sent.
|
|
18
|
+
* ``Signature-Version``: Signature scheme version. The current value is ``v1``.
|
|
19
|
+
* ``Signature``: hexadecimal HMAC-SHA-256 signature.
|
|
20
|
+
|
|
21
|
+
For version ``v1``, the signed byte sequence is::
|
|
22
|
+
{Signature-Timestamp}.{raw request body}
|
|
23
|
+
|
|
24
|
+
The application's webhook secret is the HMAC key without transformations.
|
|
25
|
+
Header names are compared case-insensitively. The legacy
|
|
26
|
+
``Rocket-Pay-Signature`` header is intentionally unsupported because it does
|
|
27
|
+
not use the current signature scheme.
|
|
28
|
+
|
|
29
|
+
Use :func:`parse_and_verify_webhook` for the usual case::
|
|
30
|
+
event = parse_and_verify_webhook(
|
|
31
|
+
raw_body=request.body,
|
|
32
|
+
headers=request.headers,
|
|
33
|
+
webhook_secret=WEBHOOK_SECRET,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
The returned object is one of :class:`InvoiceStatusChangedWebhook`,
|
|
37
|
+
:class:`PaymentStatusChangedWebhook`, :class:`ChequeWebhook`,
|
|
38
|
+
:class:`PayoutWebhook`, :class:`WithdrawalWebhook`, or
|
|
39
|
+
:class:`UnknownWebhook`.
|
|
40
|
+
|
|
41
|
+
The top-level webhook ``id`` identifies a delivery and remains stable across
|
|
42
|
+
retries. Persist and deduplicate this value in application storage before an
|
|
43
|
+
irreversible business action. Idempotency storage is deliberately outside this
|
|
44
|
+
library because its implementation depends on the application's database and
|
|
45
|
+
transaction rules.
|
|
46
|
+
|
|
47
|
+
xRocket can add new event and status values without notice. Unknown webhook
|
|
48
|
+
types or invoice event values become :class:`UnknownWebhook` instead of an
|
|
49
|
+
exception, so an application can log and safely ignore them. Invalid JSON,
|
|
50
|
+
missing required envelope fields, or an invalid signature are errors.
|
|
51
|
+
|
|
52
|
+
When a payment completes an invoice, xRocket sends both
|
|
53
|
+
``payment_status_changed`` and ``invoice_status_changed``. Delivery order is
|
|
54
|
+
not guaranteed, including after retries. Process each event independently.
|
|
55
|
+
Track successful payments only when ``event`` is
|
|
56
|
+
``payment_status_changed`` and ``event.data.payment.status`` is ``paid``;
|
|
57
|
+
track invoice completion only when ``event`` is
|
|
58
|
+
``invoice_status_changed`` and ``event.data.invoice.status`` is ``paid``.
|
|
59
|
+
Payment finality is indicated by ``event.data.payment.finalizedAt`` not being
|
|
60
|
+
``None``, rather than by relying on one fixed status value.
|
|
61
|
+
|
|
62
|
+
AI generated. Manually validated.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
import hashlib
|
|
66
|
+
import hmac
|
|
67
|
+
import json
|
|
68
|
+
from collections.abc import Mapping
|
|
69
|
+
from typing import Any
|
|
70
|
+
|
|
71
|
+
from .exceptions import (
|
|
72
|
+
xRocketWebhookParseException,
|
|
73
|
+
xRocketWebhookSignatureException,
|
|
74
|
+
xRocketWebhookUnsupportedVersionException,
|
|
75
|
+
)
|
|
76
|
+
from .models import Cheque, Invoice, InvoicePayment, Payout, Withdrawal, xRocketObject
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# noinspection method-overriding
|
|
80
|
+
class xRocketWebhook(xRocketObject):
|
|
81
|
+
"""Base model for an xRocket webhook delivery.
|
|
82
|
+
|
|
83
|
+
API: https://docs.xrocket.exchange/api/pay/pay-api-overview
|
|
84
|
+
|
|
85
|
+
:param id: Unique webhook delivery id, can be used by the receiver for
|
|
86
|
+
idempotency.
|
|
87
|
+
:param timestamp: When webhook was sent.
|
|
88
|
+
:param type: Webhook type.
|
|
89
|
+
:param data: Typed webhook payload.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self):
|
|
93
|
+
self.id = None
|
|
94
|
+
self.timestamp = None
|
|
95
|
+
self.type = None
|
|
96
|
+
self.data = None
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def de_json(cls, json_dict):
|
|
100
|
+
"""Create a webhook model from its parsed envelope dictionary.
|
|
101
|
+
|
|
102
|
+
:param json_dict: Parsed webhook envelope.
|
|
103
|
+
:return: ``xRocketWebhook``.
|
|
104
|
+
"""
|
|
105
|
+
data = cls.check_json(json_dict)
|
|
106
|
+
return super(xRocketWebhook, cls).de_json(data, process_mode=2)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# noinspection method-overriding
|
|
110
|
+
class InvoiceStatusChangedWebhookData(xRocketObject):
|
|
111
|
+
"""Payload of an ``invoice_status_changed`` webhook.
|
|
112
|
+
|
|
113
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-create-invoice
|
|
114
|
+
|
|
115
|
+
:param event: Event type. This value is ``invoice_status_changed``.
|
|
116
|
+
:param invoice: Updated invoice.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self):
|
|
120
|
+
self.event = None
|
|
121
|
+
self.invoice = None
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def de_json(cls, json_dict):
|
|
125
|
+
"""Create the payload from a parsed webhook dictionary.
|
|
126
|
+
|
|
127
|
+
:param json_dict: Parsed invoice-status-changed payload.
|
|
128
|
+
:return: ``InvoiceStatusChangedWebhookData``.
|
|
129
|
+
"""
|
|
130
|
+
data = cls.check_json(json_dict)
|
|
131
|
+
instance = super(InvoiceStatusChangedWebhookData, cls).de_json(data, process_mode=2)
|
|
132
|
+
instance.invoice = Invoice.de_json(instance.invoice)
|
|
133
|
+
return instance
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# noinspection method-overriding
|
|
137
|
+
class PaymentStatusChangedWebhookData(xRocketObject):
|
|
138
|
+
"""Payload of a ``payment_status_changed`` webhook.
|
|
139
|
+
|
|
140
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-create-invoice
|
|
141
|
+
|
|
142
|
+
:param event: Event type. This value is ``payment_status_changed``.
|
|
143
|
+
:param invoice: Updated invoice.
|
|
144
|
+
:param payment: Payment received for the invoice or whose status changed.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self):
|
|
148
|
+
self.event = None
|
|
149
|
+
self.invoice = None
|
|
150
|
+
self.payment = None
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def de_json(cls, json_dict):
|
|
154
|
+
"""Create the payload from a parsed webhook dictionary.
|
|
155
|
+
|
|
156
|
+
:param json_dict: Parsed payment-status-changed payload.
|
|
157
|
+
:return: ``PaymentStatusChangedWebhookData``.
|
|
158
|
+
"""
|
|
159
|
+
data = cls.check_json(json_dict)
|
|
160
|
+
instance = super(PaymentStatusChangedWebhookData, cls).de_json(data, process_mode=2)
|
|
161
|
+
instance.invoice = Invoice.de_json(instance.invoice)
|
|
162
|
+
instance.payment = InvoicePayment.de_json(instance.payment)
|
|
163
|
+
return instance
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# noinspection method-overriding
|
|
167
|
+
class InvoiceStatusChangedWebhook(xRocketWebhook):
|
|
168
|
+
"""Invoice webhook reporting an invoice status transition.
|
|
169
|
+
|
|
170
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-create-invoice
|
|
171
|
+
|
|
172
|
+
:param id: Unique webhook delivery id, can be used by the receiver for idempotency.
|
|
173
|
+
:param timestamp: When webhook was sent.
|
|
174
|
+
:param type: Webhook type. This value is ``invoice``.
|
|
175
|
+
:param data: ``InvoiceStatusChangedWebhookData`` payload.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def de_json(cls, json_dict):
|
|
180
|
+
"""Create the webhook from a parsed envelope dictionary.
|
|
181
|
+
|
|
182
|
+
:param json_dict: Parsed invoice webhook envelope.
|
|
183
|
+
:return: ``InvoiceStatusChangedWebhook``.
|
|
184
|
+
"""
|
|
185
|
+
data = cls.check_json(json_dict)
|
|
186
|
+
instance = super(InvoiceStatusChangedWebhook, cls).de_json(data)
|
|
187
|
+
instance.data = InvoiceStatusChangedWebhookData.de_json(instance.data)
|
|
188
|
+
return instance
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# noinspection method-overriding
|
|
192
|
+
class PaymentStatusChangedWebhook(xRocketWebhook):
|
|
193
|
+
"""Invoice webhook reporting a received payment or payment status change.
|
|
194
|
+
|
|
195
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/invoice-controller-create-invoice
|
|
196
|
+
|
|
197
|
+
:param id: Unique webhook delivery id, can be used by the receiver for idempotency.
|
|
198
|
+
:param timestamp: When webhook was sent.
|
|
199
|
+
:param type: Webhook type. This value is ``invoice``.
|
|
200
|
+
:param data: ``PaymentStatusChangedWebhookData`` payload.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def de_json(cls, json_dict):
|
|
205
|
+
"""Create the webhook from a parsed envelope dictionary.
|
|
206
|
+
|
|
207
|
+
:param json_dict: Parsed invoice webhook envelope.
|
|
208
|
+
:return: ``PaymentStatusChangedWebhook``.
|
|
209
|
+
"""
|
|
210
|
+
data = cls.check_json(json_dict)
|
|
211
|
+
instance = super(PaymentStatusChangedWebhook, cls).de_json(data)
|
|
212
|
+
instance.data = PaymentStatusChangedWebhookData.de_json(instance.data)
|
|
213
|
+
return instance
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ChequeWebhook(xRocketWebhook):
|
|
217
|
+
"""Webhook reporting an update to a cheque.
|
|
218
|
+
|
|
219
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/cheque-controller-create-cheque
|
|
220
|
+
|
|
221
|
+
:param id: Unique webhook delivery id, can be used by the receiver for idempotency.
|
|
222
|
+
:param timestamp: When webhook was sent.
|
|
223
|
+
:param type: Webhook type. This value is ``cheque``.
|
|
224
|
+
:param data: Updated ``Cheque``.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def de_json(cls, json_dict):
|
|
229
|
+
"""Create the webhook from a parsed envelope dictionary.
|
|
230
|
+
|
|
231
|
+
:param json_dict: Parsed cheque webhook envelope.
|
|
232
|
+
:return: ``ChequeWebhook``.
|
|
233
|
+
"""
|
|
234
|
+
data = cls.check_json(json_dict)
|
|
235
|
+
instance = super(ChequeWebhook, cls).de_json(data)
|
|
236
|
+
instance.data = Cheque.de_json(instance.data)
|
|
237
|
+
return instance
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class PayoutWebhook(xRocketWebhook):
|
|
241
|
+
"""Webhook reporting a payout status update.
|
|
242
|
+
|
|
243
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/payout-controller-payout
|
|
244
|
+
|
|
245
|
+
:param id: Unique webhook delivery id, can be used by the receiver for idempotency.
|
|
246
|
+
:param timestamp: When webhook was sent.
|
|
247
|
+
:param type: Webhook type. This value is ``payout``.
|
|
248
|
+
:param data: Updated ``Payout``.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def de_json(cls, json_dict):
|
|
253
|
+
"""Create the webhook from a parsed envelope dictionary.
|
|
254
|
+
|
|
255
|
+
:param json_dict: Parsed payout webhook envelope.
|
|
256
|
+
:return: ``PayoutWebhook``.
|
|
257
|
+
"""
|
|
258
|
+
data = cls.check_json(json_dict)
|
|
259
|
+
instance = super(PayoutWebhook, cls).de_json(data)
|
|
260
|
+
instance.data = Payout.de_json(instance.data)
|
|
261
|
+
return instance
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class WithdrawalWebhook(xRocketWebhook):
|
|
265
|
+
"""Webhook reporting a withdrawal status update.
|
|
266
|
+
|
|
267
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/withdrawal-controller-create-withdrawal
|
|
268
|
+
|
|
269
|
+
:param id: Unique webhook delivery id, can be used by the receiver for idempotency.
|
|
270
|
+
:param timestamp: When webhook was sent.
|
|
271
|
+
:param type: Webhook type. This value is ``withdrawal``.
|
|
272
|
+
:param data: Updated ``Withdrawal``.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def de_json(cls, json_dict):
|
|
277
|
+
"""Create the webhook from a parsed envelope dictionary.
|
|
278
|
+
|
|
279
|
+
:param json_dict: Parsed withdrawal webhook envelope.
|
|
280
|
+
:return: ``WithdrawalWebhook``.
|
|
281
|
+
"""
|
|
282
|
+
data = cls.check_json(json_dict)
|
|
283
|
+
instance = super(WithdrawalWebhook, cls).de_json(data)
|
|
284
|
+
instance.data = Withdrawal.de_json(instance.data)
|
|
285
|
+
return instance
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class UnknownWebhook(xRocketWebhook):
|
|
289
|
+
"""Webhook whose type or invoice event is not known to this library.
|
|
290
|
+
|
|
291
|
+
xRocket may extend type and event values. This model retains the original
|
|
292
|
+
envelope instead of failing, so an application can log and ignore it.
|
|
293
|
+
|
|
294
|
+
:param id: Unique webhook delivery id, can be used by the receiver for idempotency.
|
|
295
|
+
:param timestamp: When webhook was sent.
|
|
296
|
+
:param type: Unknown webhook type, or ``invoice`` with an unknown event.
|
|
297
|
+
:param data: Original untyped webhook payload.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def verify_webhook_signature(raw_body: bytes | bytearray | str, headers: Mapping[str, Any], webhook_secret: str) -> None:
|
|
302
|
+
"""Verify the current xRocket Pay ``v1`` webhook signature.
|
|
303
|
+
|
|
304
|
+
API: https://docs.xrocket.exchange/api/pay/pay-api-overview
|
|
305
|
+
|
|
306
|
+
``raw_body`` must be the original request body. ``bytes`` is preferred;
|
|
307
|
+
a string is accepted only when it is the unchanged request body text.
|
|
308
|
+
A parsed dictionary is intentionally rejected because serializing it again
|
|
309
|
+
can alter the signed representation.
|
|
310
|
+
|
|
311
|
+
:param raw_body: Original request body as bytes, bytearray, or unchanged
|
|
312
|
+
text.
|
|
313
|
+
:param headers: HTTP request headers. Header names are case-insensitive.
|
|
314
|
+
:param webhook_secret: Application webhook secret used as the HMAC key
|
|
315
|
+
without transformations.
|
|
316
|
+
:return: ``None`` when the signature is valid.
|
|
317
|
+
:raises xRocketWebhookUnsupportedVersionException: If
|
|
318
|
+
``Signature-Version`` is not ``v1``.
|
|
319
|
+
:raises xRocketWebhookSignatureException: If a required header is absent,
|
|
320
|
+
arguments are invalid, or the signature does not match.
|
|
321
|
+
"""
|
|
322
|
+
body = _get_raw_body(raw_body)
|
|
323
|
+
if not isinstance(headers, Mapping):
|
|
324
|
+
raise xRocketWebhookSignatureException("headers must be a mapping.")
|
|
325
|
+
if not isinstance(webhook_secret, str) or not webhook_secret:
|
|
326
|
+
raise xRocketWebhookSignatureException("webhook_secret must be a non-empty string.")
|
|
327
|
+
|
|
328
|
+
timestamp = _get_header(headers, "Signature-Timestamp")
|
|
329
|
+
version = _get_header(headers, "Signature-Version")
|
|
330
|
+
signature = _get_header(headers, "Signature")
|
|
331
|
+
if not timestamp or not version or not signature:
|
|
332
|
+
raise xRocketWebhookSignatureException(
|
|
333
|
+
"Webhook headers Signature-Timestamp, Signature-Version, and Signature are required."
|
|
334
|
+
)
|
|
335
|
+
if version != "v1":
|
|
336
|
+
raise xRocketWebhookUnsupportedVersionException(
|
|
337
|
+
"Unsupported xRocket webhook signature version: {}.".format(version)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
signed_body = timestamp.encode("utf-8") + b"." + body
|
|
341
|
+
expected_signature = hmac.new(
|
|
342
|
+
webhook_secret.encode("utf-8"), signed_body, hashlib.sha256
|
|
343
|
+
).hexdigest()
|
|
344
|
+
if not hmac.compare_digest(signature, expected_signature):
|
|
345
|
+
raise xRocketWebhookSignatureException("xRocket webhook signature does not match.")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def parse_webhook(payload: bytes | bytearray | str | Mapping[str, Any]) -> xRocketWebhook:
|
|
349
|
+
"""Parse an xRocket Pay webhook body into a typed model without verifying it.
|
|
350
|
+
|
|
351
|
+
API: https://docs.xrocket.exchange/api/pay/pay-api-overview
|
|
352
|
+
|
|
353
|
+
Use this only after independently calling :func:`verify_webhook_signature`.
|
|
354
|
+
In normal HTTP handling, prefer :func:`parse_and_verify_webhook`.
|
|
355
|
+
|
|
356
|
+
:param payload: Original JSON body as bytes or text, or a parsed envelope
|
|
357
|
+
mapping when signature verification has already occurred.
|
|
358
|
+
:return: A typed webhook model, or ``UnknownWebhook`` for an unknown type or invoice event.
|
|
359
|
+
:raises xRocketWebhookParseException: If the body is invalid JSON or does
|
|
360
|
+
not have the required webhook envelope structure.
|
|
361
|
+
"""
|
|
362
|
+
envelope = _get_envelope(payload)
|
|
363
|
+
_validate_envelope(envelope)
|
|
364
|
+
|
|
365
|
+
webhook_type = envelope["type"]
|
|
366
|
+
if webhook_type == "invoice":
|
|
367
|
+
return _parse_invoice_webhook(envelope)
|
|
368
|
+
if webhook_type == "cheque":
|
|
369
|
+
_require_mapping(envelope["data"], "data")
|
|
370
|
+
return ChequeWebhook.de_json(envelope)
|
|
371
|
+
if webhook_type == "payout":
|
|
372
|
+
_require_mapping(envelope["data"], "data")
|
|
373
|
+
return PayoutWebhook.de_json(envelope)
|
|
374
|
+
if webhook_type == "withdrawal":
|
|
375
|
+
_require_mapping(envelope["data"], "data")
|
|
376
|
+
return WithdrawalWebhook.de_json(envelope)
|
|
377
|
+
return UnknownWebhook.de_json(envelope)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def parse_and_verify_webhook(
|
|
381
|
+
raw_body: bytes | bytearray | str,
|
|
382
|
+
headers: Mapping[str, Any],
|
|
383
|
+
webhook_secret: str,
|
|
384
|
+
) -> xRocketWebhook:
|
|
385
|
+
"""Verify and parse an xRocket Pay webhook in the safe order.
|
|
386
|
+
|
|
387
|
+
API: https://docs.xrocket.exchange/api/pay/pay-api-overview
|
|
388
|
+
|
|
389
|
+
The signature is verified against the unchanged request body before JSON
|
|
390
|
+
parsing. The returned event must be deduplicated by its ``id`` in the
|
|
391
|
+
application's persistent storage before an irreversible business action.
|
|
392
|
+
|
|
393
|
+
:param raw_body: Original request body as bytes, bytearray, or unchanged
|
|
394
|
+
text.
|
|
395
|
+
:param headers: HTTP request headers. Header names are case-insensitive.
|
|
396
|
+
:param webhook_secret: Application webhook secret used as the HMAC key without transformations.
|
|
397
|
+
:return: A typed webhook model, or ``UnknownWebhook`` for an unknown type or invoice event.
|
|
398
|
+
:raises xRocketWebhookSignatureException: If signature validation fails.
|
|
399
|
+
:raises xRocketWebhookParseException: If the verified body is invalid JSON
|
|
400
|
+
or does not have the required webhook envelope structure.
|
|
401
|
+
"""
|
|
402
|
+
verify_webhook_signature(raw_body, headers, webhook_secret)
|
|
403
|
+
return parse_webhook(raw_body)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _get_raw_body(raw_body: bytes | bytearray | str) -> bytes:
|
|
407
|
+
"""Return the unchanged request body as bytes for signature verification."""
|
|
408
|
+
if isinstance(raw_body, bytes):
|
|
409
|
+
return raw_body
|
|
410
|
+
if isinstance(raw_body, bytearray):
|
|
411
|
+
return bytes(raw_body)
|
|
412
|
+
if isinstance(raw_body, str):
|
|
413
|
+
return raw_body.encode("utf-8")
|
|
414
|
+
raise xRocketWebhookSignatureException(
|
|
415
|
+
"raw_body must be bytes, bytearray, or unchanged request body text."
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _get_header(headers: Mapping[str, Any], expected_name: str) -> str | None:
|
|
420
|
+
"""Read one HTTP header without relying on a framework's header type."""
|
|
421
|
+
expected_name = expected_name.lower()
|
|
422
|
+
for name, value in headers.items():
|
|
423
|
+
if str(name).lower() == expected_name:
|
|
424
|
+
return str(value)
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _get_envelope(payload: bytes | bytearray | str | Mapping[str, Any]) -> dict[str, Any]:
|
|
429
|
+
"""Decode a JSON webhook body or make a shallow copy of a mapping."""
|
|
430
|
+
if isinstance(payload, Mapping):
|
|
431
|
+
return dict(payload)
|
|
432
|
+
if isinstance(payload, bytearray):
|
|
433
|
+
payload = bytes(payload)
|
|
434
|
+
if isinstance(payload, bytes):
|
|
435
|
+
try:
|
|
436
|
+
payload = payload.decode("utf-8")
|
|
437
|
+
except UnicodeDecodeError as error:
|
|
438
|
+
raise xRocketWebhookParseException("Webhook body is not UTF-8 JSON.") from error
|
|
439
|
+
if isinstance(payload, str):
|
|
440
|
+
try:
|
|
441
|
+
payload = json.loads(payload)
|
|
442
|
+
except json.JSONDecodeError as error:
|
|
443
|
+
raise xRocketWebhookParseException("Webhook body is not valid JSON.") from error
|
|
444
|
+
if not isinstance(payload, Mapping):
|
|
445
|
+
raise xRocketWebhookParseException("Webhook body must contain a JSON object.")
|
|
446
|
+
return dict(payload)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _validate_envelope(envelope: Mapping[str, Any]) -> None:
|
|
450
|
+
"""Validate the fields required by every documented webhook envelope."""
|
|
451
|
+
for field in ("id", "timestamp", "type", "data"):
|
|
452
|
+
if field not in envelope:
|
|
453
|
+
raise xRocketWebhookParseException(
|
|
454
|
+
"Webhook body is missing required field: {}.".format(field)
|
|
455
|
+
)
|
|
456
|
+
if not isinstance(envelope["type"], str):
|
|
457
|
+
raise xRocketWebhookParseException("Webhook field type must be a string.")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _parse_invoice_webhook(envelope: dict[str, Any]) -> xRocketWebhook:
|
|
461
|
+
"""Choose the typed invoice webhook model using the nested event field."""
|
|
462
|
+
data = _require_mapping(envelope["data"], "data")
|
|
463
|
+
event = data.get("event")
|
|
464
|
+
if event == "invoice_status_changed":
|
|
465
|
+
_require_mapping(data.get("invoice"), "data.invoice")
|
|
466
|
+
return InvoiceStatusChangedWebhook.de_json(envelope)
|
|
467
|
+
if event == "payment_status_changed":
|
|
468
|
+
_require_mapping(data.get("invoice"), "data.invoice")
|
|
469
|
+
_require_mapping(data.get("payment"), "data.payment")
|
|
470
|
+
return PaymentStatusChangedWebhook.de_json(envelope)
|
|
471
|
+
return UnknownWebhook.de_json(envelope)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _require_mapping(value: Any, field_name: str) -> Mapping[str, Any]:
|
|
475
|
+
"""Return a payload object or raise a precise structural parse error."""
|
|
476
|
+
if not isinstance(value, Mapping):
|
|
477
|
+
raise xRocketWebhookParseException(
|
|
478
|
+
"Webhook field {} must be a JSON object.".format(field_name)
|
|
479
|
+
)
|
|
480
|
+
return value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyXRocketAPI
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python client for the xRocket Pay API
|
|
5
5
|
Author: Badiboy
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,7 +17,11 @@ License-File: LICENSE
|
|
|
17
17
|
Requires-Dist: requests>=2.28
|
|
18
18
|
Dynamic: license-file
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
[](https://pypi.python.org/pypi/pyXRocketAPI)
|
|
21
|
+
[](https://pypi.python.org/pypi/pyXRocketAPI)
|
|
22
|
+
[](https://pypi.org/project/pyXRocketAPI/)
|
|
23
|
+
|
|
24
|
+
# <p align="center">pyXRocketAPI</p>
|
|
21
25
|
|
|
22
26
|
**pyXRocketAPI** is a Python client for the [xRocket Pay API](https://docs.xrocket.exchange/api/pay/pay-api-overview). It supports application information, balances, invoices, cheques, payouts, withdrawals, currency rates, and health checks.
|
|
23
27
|
|
|
@@ -6,9 +6,11 @@ pyXRocketAPI/api.py
|
|
|
6
6
|
pyXRocketAPI/exceptions.py
|
|
7
7
|
pyXRocketAPI/models.py
|
|
8
8
|
pyXRocketAPI/version.py
|
|
9
|
+
pyXRocketAPI/webhooks.py
|
|
9
10
|
pyXRocketAPI.egg-info/PKG-INFO
|
|
10
11
|
pyXRocketAPI.egg-info/SOURCES.txt
|
|
11
12
|
pyXRocketAPI.egg-info/dependency_links.txt
|
|
12
13
|
pyXRocketAPI.egg-info/requires.txt
|
|
13
14
|
pyXRocketAPI.egg-info/top_level.txt
|
|
14
|
-
tests/test_api.py
|
|
15
|
+
tests/test_api.py
|
|
16
|
+
tests/test_webhooks.py
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import json
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from pyXRocketAPI import (
|
|
7
|
+
Cheque,
|
|
8
|
+
ChequeWebhook,
|
|
9
|
+
Invoice,
|
|
10
|
+
Payout,
|
|
11
|
+
PayoutWebhook,
|
|
12
|
+
PaymentStatusChangedWebhook,
|
|
13
|
+
UnknownWebhook,
|
|
14
|
+
Withdrawal,
|
|
15
|
+
WithdrawalWebhook,
|
|
16
|
+
parse_and_verify_webhook,
|
|
17
|
+
parse_webhook,
|
|
18
|
+
xRocketWebhookParseException,
|
|
19
|
+
xRocketWebhookSignatureException,
|
|
20
|
+
)
|
|
21
|
+
from pyXRocketAPI.models import InvoicePayment
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
WEBHOOK_SECRET = "webhook-secret"
|
|
25
|
+
SIGNATURE_TIMESTAMP = "1735689600000"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def signed_webhook(payload):
|
|
29
|
+
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
30
|
+
signature = hmac.new(
|
|
31
|
+
WEBHOOK_SECRET.encode("utf-8"),
|
|
32
|
+
SIGNATURE_TIMESTAMP.encode("utf-8") + b"." + body,
|
|
33
|
+
hashlib.sha256,
|
|
34
|
+
).hexdigest()
|
|
35
|
+
return body, {
|
|
36
|
+
"signature-timestamp": SIGNATURE_TIMESTAMP,
|
|
37
|
+
"signature-version": "v1",
|
|
38
|
+
"signature": signature,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class XRocketWebhookTests(unittest.TestCase):
|
|
43
|
+
def test_parses_and_verifies_payment_status_changed_webhook(self):
|
|
44
|
+
body, headers = signed_webhook({
|
|
45
|
+
"id": "delivery-1",
|
|
46
|
+
"timestamp": "2025-01-01T00:00:00.000Z",
|
|
47
|
+
"type": "invoice",
|
|
48
|
+
"data": {
|
|
49
|
+
"event": "payment_status_changed",
|
|
50
|
+
"invoice": {"id": "invoice-1", "status": "paid"},
|
|
51
|
+
"payment": {"id": "payment-1", "status": "paid", "finalizedAt": "2025-01-01T00:00:00.000Z"},
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
event = parse_and_verify_webhook(body, headers, WEBHOOK_SECRET)
|
|
56
|
+
|
|
57
|
+
self.assertIsInstance(event, PaymentStatusChangedWebhook)
|
|
58
|
+
self.assertEqual(event.id, "delivery-1")
|
|
59
|
+
self.assertIsInstance(event.data.invoice, Invoice)
|
|
60
|
+
self.assertIsInstance(event.data.payment, InvoicePayment)
|
|
61
|
+
self.assertEqual(event.data.payment.status, "paid")
|
|
62
|
+
|
|
63
|
+
def test_rejects_invalid_signature_before_parsing(self):
|
|
64
|
+
body, headers = signed_webhook({"id": "delivery-1", "timestamp": "now", "type": "payout", "data": {}})
|
|
65
|
+
|
|
66
|
+
with self.assertRaises(xRocketWebhookSignatureException):
|
|
67
|
+
parse_and_verify_webhook(body + b" ", headers, WEBHOOK_SECRET)
|
|
68
|
+
|
|
69
|
+
def test_returns_unknown_webhook_for_future_type_and_invoice_event(self):
|
|
70
|
+
future_type = parse_webhook({
|
|
71
|
+
"id": "delivery-1", "timestamp": "now", "type": "future_type", "data": {"value": 1},
|
|
72
|
+
})
|
|
73
|
+
future_invoice_event = parse_webhook({
|
|
74
|
+
"id": "delivery-2", "timestamp": "now", "type": "invoice", "data": {"event": "future_event"},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
self.assertIsInstance(future_type, UnknownWebhook)
|
|
78
|
+
self.assertEqual(future_type.data, {"value": 1})
|
|
79
|
+
self.assertIsInstance(future_invoice_event, UnknownWebhook)
|
|
80
|
+
self.assertEqual(future_invoice_event.data["event"], "future_event")
|
|
81
|
+
|
|
82
|
+
def test_parses_other_documented_webhook_types(self):
|
|
83
|
+
cases = (
|
|
84
|
+
("cheque", {"chequeId": "cheque-1"}, ChequeWebhook, Cheque),
|
|
85
|
+
("payout", {"payoutId": "payout-1"}, PayoutWebhook, Payout),
|
|
86
|
+
("withdrawal", {"withdrawalId": "withdrawal-1"}, WithdrawalWebhook, Withdrawal),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for webhook_type, data, webhook_class, data_class in cases:
|
|
90
|
+
with self.subTest(webhook_type=webhook_type):
|
|
91
|
+
event = parse_webhook({
|
|
92
|
+
"id": "delivery-1", "timestamp": "now", "type": webhook_type, "data": data,
|
|
93
|
+
})
|
|
94
|
+
self.assertIsInstance(event, webhook_class)
|
|
95
|
+
self.assertIsInstance(event.data, data_class)
|
|
96
|
+
|
|
97
|
+
def test_rejects_malformed_webhook_envelope(self):
|
|
98
|
+
with self.assertRaises(xRocketWebhookParseException):
|
|
99
|
+
parse_webhook(b"not json")
|
|
100
|
+
with self.assertRaises(xRocketWebhookParseException):
|
|
101
|
+
parse_webhook({"id": "delivery-1", "timestamp": "now", "type": "payout"})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|