pyXRocketAPI 0.3.0__tar.gz → 1.0.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.3.0 → pyxrocketapi-1.0.0}/PKG-INFO +3 -4
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI/api.py +118 -4
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI/models.py +155 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI/version.py +1 -1
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI/webhooks.py +64 -2
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI.egg-info/PKG-INFO +3 -4
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyproject.toml +2 -3
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/tests/test_api.py +104 -2
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/tests/test_webhooks.py +32 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/LICENSE +0 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/README.md +0 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI/__init__.py +0 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI/exceptions.py +0 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI.egg-info/SOURCES.txt +0 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI.egg-info/dependency_links.txt +0 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI.egg-info/requires.txt +0 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/pyXRocketAPI.egg-info/top_level.txt +0 -0
- {pyxrocketapi-0.3.0 → pyxrocketapi-1.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyXRocketAPI
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Python client for the xRocket Pay API
|
|
5
5
|
Author: Badiboy
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,10 +8,9 @@ Project-URL: Homepage, https://github.com/Badiboy/pyXRocketAPI
|
|
|
8
8
|
Project-URL: Documentation, https://docs.xrocket.exchange/api/pay/pay-api-overview
|
|
9
9
|
Project-URL: Repository, https://github.com/Badiboy/pyXRocketAPI
|
|
10
10
|
Keywords: xrocket,pay,crypto,payments,api
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
|
|
14
|
-
Requires-Python: >=3.10
|
|
13
|
+
Requires-Python: >=3.11
|
|
15
14
|
Description-Content-Type: text/markdown
|
|
16
15
|
License-File: LICENSE
|
|
17
16
|
Requires-Dist: requests>=2.28
|
|
@@ -4,7 +4,7 @@ from typing import Any
|
|
|
4
4
|
import requests
|
|
5
5
|
|
|
6
6
|
from .exceptions import xRocketAPIException
|
|
7
|
-
from .models import App, Balance, Cheque, ChequeCallback, ChequeUrl, ChequesList, Currency, Health, Invoice, InvoiceCallback, InvoiceCustomer, InvoicePaymentAddress, InvoicePaymentsList, InvoiceUrl, InvoicesList, MassPayout, MassPayouts, Payout, PayoutCallback, PayoutsList, Rate, Withdrawal, WithdrawalCallback, WithdrawalLink, WithdrawalQuotas, WithdrawalsList, xPage, xRocketObject
|
|
7
|
+
from .models import App, Balance, Cheque, ChequeCallback, ChequeUrl, ChequesList, Currency, Health, Invoice, InvoiceCallback, InvoiceCustomer, InvoicePaymentAddress, InvoicePaymentsList, InvoiceUrl, InvoicesList, MassPayout, MassPayouts, Payout, PayoutCallback, PayoutsList, Rate, Swap, SwapAsset, SwapEstimate, SwapRoute, SwapsList, Withdrawal, WithdrawalCallback, WithdrawalLink, WithdrawalQuotas, WithdrawalsList, xPage, xRocketObject
|
|
8
8
|
|
|
9
9
|
PRODUCTION_API_URL = "https://pay.api.xrocket.exchange"
|
|
10
10
|
TESTNET_API_URL = "https://pay.api.testnet.xrocket.exchange"
|
|
@@ -57,6 +57,18 @@ class xRocketPayAPI:
|
|
|
57
57
|
raise TypeError("{} must be a string or number, not bool.".format(parameter_name))
|
|
58
58
|
return str(value)
|
|
59
59
|
|
|
60
|
+
@staticmethod
|
|
61
|
+
def __swap_amount_mode(
|
|
62
|
+
amount: str | int | float | None, receive_amount: str | int | float | None
|
|
63
|
+
) -> dict[str, str]:
|
|
64
|
+
"""Validate and serialize one exact-in or exact-out swap amount."""
|
|
65
|
+
if (amount is None) == (receive_amount is None):
|
|
66
|
+
raise ValueError("Specify exactly one of amount or receive_amount.")
|
|
67
|
+
return xRocketPayAPI.__without_none(
|
|
68
|
+
amount=xRocketPayAPI.__number_as_string(amount, "amount"),
|
|
69
|
+
receiveAmount=xRocketPayAPI.__number_as_string(receive_amount, "receive_amount"),
|
|
70
|
+
)
|
|
71
|
+
|
|
60
72
|
def __init__(
|
|
61
73
|
self,
|
|
62
74
|
token: str | None = None,
|
|
@@ -190,6 +202,110 @@ class xRocketPayAPI:
|
|
|
190
202
|
)
|
|
191
203
|
return [Rate.de_json(item) for item in data]
|
|
192
204
|
|
|
205
|
+
def create_swap(
|
|
206
|
+
self, from_asset: str, to_asset: str, client_order_id: str,
|
|
207
|
+
amount: str | int | float | None = None, receive_amount: str | int | float | None = None,
|
|
208
|
+
) -> Swap:
|
|
209
|
+
"""Create swap.
|
|
210
|
+
|
|
211
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-create-swap
|
|
212
|
+
|
|
213
|
+
:param from_asset: Asset to swap from.
|
|
214
|
+
:param to_asset: Asset to swap to.
|
|
215
|
+
:param client_order_id: Merchant-provided idempotency key. Re-using a
|
|
216
|
+
value returns a conflict.
|
|
217
|
+
:param amount: Exact-in amount: how much of ``from_asset`` to spend.
|
|
218
|
+
Provide exactly one of ``amount`` or ``receive_amount``. A numeric
|
|
219
|
+
value is encoded as an API string.
|
|
220
|
+
:param receive_amount: Exact-out amount: how much of ``to_asset`` to
|
|
221
|
+
receive. Provide exactly one of ``amount`` or ``receive_amount``.
|
|
222
|
+
A numeric value is encoded as an API string.
|
|
223
|
+
:return: ``Swap``.
|
|
224
|
+
"""
|
|
225
|
+
body = {"fromAsset": from_asset, "toAsset": to_asset, "clientOrderId": client_order_id}
|
|
226
|
+
body.update(self.__swap_amount_mode(amount, receive_amount))
|
|
227
|
+
return Swap.de_json(self._request("POST", "/api/v1/swap", body=body))
|
|
228
|
+
|
|
229
|
+
def estimate_swap(
|
|
230
|
+
self, from_asset: str, to_asset: str,
|
|
231
|
+
amount: str | int | float | None = None, receive_amount: str | int | float | None = None,
|
|
232
|
+
) -> SwapEstimate:
|
|
233
|
+
"""Estimate swap amounts and rate without creating it.
|
|
234
|
+
|
|
235
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-estimate
|
|
236
|
+
|
|
237
|
+
:param from_asset: Asset to swap from.
|
|
238
|
+
:param to_asset: Asset to swap to.
|
|
239
|
+
:param amount: Exact-in amount: how much of ``from_asset`` to spend.
|
|
240
|
+
Provide exactly one of ``amount`` or ``receive_amount``. A numeric
|
|
241
|
+
value is encoded as an API string.
|
|
242
|
+
:param receive_amount: Exact-out amount: how much of ``to_asset`` to
|
|
243
|
+
receive. Provide exactly one of ``amount`` or ``receive_amount``.
|
|
244
|
+
A numeric value is encoded as an API string.
|
|
245
|
+
:return: ``SwapEstimate``.
|
|
246
|
+
"""
|
|
247
|
+
body = {"fromAsset": from_asset, "toAsset": to_asset}
|
|
248
|
+
body.update(self.__swap_amount_mode(amount, receive_amount))
|
|
249
|
+
return SwapEstimate.de_json(self._request("POST", "/api/v1/swap/estimate", body=body))
|
|
250
|
+
|
|
251
|
+
def get_swaps_list(self, cursor: str | None = None, limit: int | None = None) -> SwapsList:
|
|
252
|
+
"""Get application swaps.
|
|
253
|
+
|
|
254
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-get-swaps
|
|
255
|
+
|
|
256
|
+
:param cursor: Cursor for pagination.
|
|
257
|
+
:param limit: No description is provided.
|
|
258
|
+
:return: ``SwapsList``.
|
|
259
|
+
"""
|
|
260
|
+
return SwapsList.de_json(self._request(
|
|
261
|
+
"GET", "/api/v1/swaps", params=self.__without_none(cursor=cursor, limit=limit)
|
|
262
|
+
))
|
|
263
|
+
|
|
264
|
+
def get_swap_info(self, swap_id: str) -> Swap:
|
|
265
|
+
"""Get application swap info by id.
|
|
266
|
+
|
|
267
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-get-swap
|
|
268
|
+
|
|
269
|
+
:param swap_id: Swap id (track id).
|
|
270
|
+
:return: ``Swap``.
|
|
271
|
+
"""
|
|
272
|
+
return Swap.de_json(self._request("GET", "/api/v1/swap/{}".format(swap_id)))
|
|
273
|
+
|
|
274
|
+
def get_swap_input_assets(self, asset_out: str | None = None) -> list[SwapAsset]:
|
|
275
|
+
"""Get available input assets, optionally filtered by a target asset.
|
|
276
|
+
|
|
277
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-get-assets-in
|
|
278
|
+
|
|
279
|
+
:param asset_out: Target asset used to filter the result.
|
|
280
|
+
:return: ``list[SwapAsset]``.
|
|
281
|
+
"""
|
|
282
|
+
data = self._request("GET", "/api/v1/swap/assets-in", params=self.__without_none(assetOut=asset_out))
|
|
283
|
+
return [SwapAsset.de_json(item) for item in data.get("items", [])]
|
|
284
|
+
|
|
285
|
+
def get_swap_output_assets(self, asset_in: str | None = None) -> list[SwapAsset]:
|
|
286
|
+
"""Get available output assets, optionally filtered by a source asset.
|
|
287
|
+
|
|
288
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-get-assets-out
|
|
289
|
+
|
|
290
|
+
:param asset_in: Source asset used to filter the result.
|
|
291
|
+
:return: ``list[SwapAsset]``.
|
|
292
|
+
"""
|
|
293
|
+
data = self._request("GET", "/api/v1/swap/assets-out", params=self.__without_none(assetIn=asset_in))
|
|
294
|
+
return [SwapAsset.de_json(item) for item in data.get("items", [])]
|
|
295
|
+
|
|
296
|
+
def get_swap_route_params(self, asset_in: str, asset_out: str) -> SwapRoute:
|
|
297
|
+
"""Get swap route params (min/max amounts) for an asset pair.
|
|
298
|
+
|
|
299
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-get-params
|
|
300
|
+
|
|
301
|
+
:param asset_in: Source asset.
|
|
302
|
+
:param asset_out: Target asset.
|
|
303
|
+
:return: ``SwapRoute``.
|
|
304
|
+
"""
|
|
305
|
+
return SwapRoute.de_json(self._request(
|
|
306
|
+
"GET", "/api/v1/swap/params", params={"assetIn": asset_in, "assetOut": asset_out}
|
|
307
|
+
))
|
|
308
|
+
|
|
193
309
|
def create_invoice(
|
|
194
310
|
self,
|
|
195
311
|
price_currency: str,
|
|
@@ -204,7 +320,6 @@ class xRocketPayAPI:
|
|
|
204
320
|
callback: InvoiceCallback | None = None,
|
|
205
321
|
url: InvoiceUrl | None = None,
|
|
206
322
|
customer: InvoiceCustomer | None = None,
|
|
207
|
-
is_fee_paid_by_user: bool | None = None,
|
|
208
323
|
data: Mapping[str, Any] | None = None,
|
|
209
324
|
platform_id: str | None = None,
|
|
210
325
|
) -> Invoice:
|
|
@@ -224,7 +339,6 @@ class xRocketPayAPI:
|
|
|
224
339
|
:param callback: Callback data as an ``InvoiceCallback`` instance.
|
|
225
340
|
:param url: User redirect urls as an ``InvoiceUrl`` instance.
|
|
226
341
|
:param customer: Customer info as an ``InvoiceCustomer`` instance.
|
|
227
|
-
:param is_fee_paid_by_user: If true, the user pays a commission.
|
|
228
342
|
:param data: Custom user data passed through and returned in callbacks/webhooks (max size 4KB).
|
|
229
343
|
:param platform_id: Platform identifier.
|
|
230
344
|
:return: ``Invoice``.
|
|
@@ -238,7 +352,7 @@ class xRocketPayAPI:
|
|
|
238
352
|
callback=self.__request_model(callback, InvoiceCallback, "callback"),
|
|
239
353
|
url=self.__request_model(url, InvoiceUrl, "url"),
|
|
240
354
|
customer=self.__request_model(customer, InvoiceCustomer, "customer"),
|
|
241
|
-
|
|
355
|
+
data=data, platformId=platform_id,
|
|
242
356
|
)
|
|
243
357
|
return Invoice.de_json(self._request("POST", "/api/v1/invoices", body=body))
|
|
244
358
|
|
|
@@ -267,6 +267,7 @@ class InvoiceCustomer(Dictionaryable, xRocketObject):
|
|
|
267
267
|
:param telegramUsername: Customer telegram username (without @).
|
|
268
268
|
"""
|
|
269
269
|
|
|
270
|
+
# noinspection shadowing-builtins
|
|
270
271
|
def __init__(self, id=None, email=None, telegramId=None, telegramUsername=None):
|
|
271
272
|
self.id = id
|
|
272
273
|
self.email = email
|
|
@@ -648,6 +649,141 @@ class Rate(xRocketObject):
|
|
|
648
649
|
return instance
|
|
649
650
|
|
|
650
651
|
|
|
652
|
+
# noinspection method-overriding
|
|
653
|
+
class Swap(xRocketObject):
|
|
654
|
+
"""A currency exchange performed from the application balance.
|
|
655
|
+
|
|
656
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-create-swap
|
|
657
|
+
|
|
658
|
+
:param id: Swap id (track id).
|
|
659
|
+
:param status: Swap status. ``completed`` and ``cancelled`` are terminal;
|
|
660
|
+
``pending``, ``failed`` and ``paused`` are non-terminal.
|
|
661
|
+
:param fromAsset: Asset swapped from.
|
|
662
|
+
:param toAsset: Asset swapped to.
|
|
663
|
+
:param fromAmount: Amount in ``fromAsset``.
|
|
664
|
+
:param toAmount: Guaranteed amount received in ``toAsset``. It is settled
|
|
665
|
+
on completion and reserved while pending.
|
|
666
|
+
:param rate: Applied rate.
|
|
667
|
+
:param clientOrderId: Merchant-provided idempotency key.
|
|
668
|
+
"""
|
|
669
|
+
def __init__(self):
|
|
670
|
+
self.id = None
|
|
671
|
+
self.status = None
|
|
672
|
+
self.fromAsset = None
|
|
673
|
+
self.toAsset = None
|
|
674
|
+
self.fromAmount = None
|
|
675
|
+
self.toAmount = None
|
|
676
|
+
self.rate = None
|
|
677
|
+
self.clientOrderId = None
|
|
678
|
+
|
|
679
|
+
@classmethod
|
|
680
|
+
def de_json(cls, json_dict):
|
|
681
|
+
data = cls.check_json(json_dict)
|
|
682
|
+
instance = super(Swap, cls).de_json(data, process_mode=2)
|
|
683
|
+
for field in ("fromAmount", "toAmount", "rate"):
|
|
684
|
+
if getattr(instance, field) is not None:
|
|
685
|
+
setattr(instance, field, float(getattr(instance, field)))
|
|
686
|
+
return instance
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# noinspection method-overriding
|
|
690
|
+
class SwapEstimate(xRocketObject):
|
|
691
|
+
"""Estimated amounts and rate for a prospective currency exchange.
|
|
692
|
+
|
|
693
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-estimate
|
|
694
|
+
|
|
695
|
+
:param fromAsset: Asset to swap from.
|
|
696
|
+
:param toAsset: Asset to swap to.
|
|
697
|
+
:param fromAmount: Amount in ``fromAsset``.
|
|
698
|
+
:param toAmount: Estimated amount in ``toAsset``.
|
|
699
|
+
:param rate: Estimated rate.
|
|
700
|
+
"""
|
|
701
|
+
def __init__(self):
|
|
702
|
+
self.fromAsset = None
|
|
703
|
+
self.toAsset = None
|
|
704
|
+
self.fromAmount = None
|
|
705
|
+
self.toAmount = None
|
|
706
|
+
self.rate = None
|
|
707
|
+
|
|
708
|
+
@classmethod
|
|
709
|
+
def de_json(cls, json_dict):
|
|
710
|
+
data = cls.check_json(json_dict)
|
|
711
|
+
instance = super(SwapEstimate, cls).de_json(data, process_mode=2)
|
|
712
|
+
for field in ("fromAmount", "toAmount", "rate"):
|
|
713
|
+
if getattr(instance, field) is not None:
|
|
714
|
+
setattr(instance, field, float(getattr(instance, field)))
|
|
715
|
+
return instance
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
# noinspection method-overriding
|
|
719
|
+
class SwapAsset(xRocketObject):
|
|
720
|
+
"""An asset available as one side of a swap route.
|
|
721
|
+
|
|
722
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-get-assets-in
|
|
723
|
+
|
|
724
|
+
:param code: Asset code.
|
|
725
|
+
"""
|
|
726
|
+
def __init__(self):
|
|
727
|
+
self.code = None
|
|
728
|
+
|
|
729
|
+
@classmethod
|
|
730
|
+
def de_json(cls, json_dict):
|
|
731
|
+
data = cls.check_json(json_dict)
|
|
732
|
+
return super(SwapAsset, cls).de_json(data, process_mode=2)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
# noinspection method-overriding
|
|
736
|
+
class SwapRouteParams(xRocketObject):
|
|
737
|
+
"""Amount limits for a swap route.
|
|
738
|
+
|
|
739
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-get-params
|
|
740
|
+
|
|
741
|
+
:param minAmount: Minimum amount in ``assetIn``.
|
|
742
|
+
:param maxAmount: Maximum amount in ``assetIn``.
|
|
743
|
+
"""
|
|
744
|
+
def __init__(self):
|
|
745
|
+
self.minAmount = None
|
|
746
|
+
self.maxAmount = None
|
|
747
|
+
|
|
748
|
+
@classmethod
|
|
749
|
+
def de_json(cls, json_dict):
|
|
750
|
+
data = cls.check_json(json_dict)
|
|
751
|
+
instance = super(SwapRouteParams, cls).de_json(data, process_mode=2)
|
|
752
|
+
if instance.minAmount is not None:
|
|
753
|
+
instance.minAmount = float(instance.minAmount)
|
|
754
|
+
if instance.maxAmount is not None:
|
|
755
|
+
instance.maxAmount = float(instance.maxAmount)
|
|
756
|
+
return instance
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
# noinspection method-overriding
|
|
760
|
+
class SwapRoute(xRocketObject):
|
|
761
|
+
"""Available input and output assets with limits for their swap route.
|
|
762
|
+
|
|
763
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-get-params
|
|
764
|
+
|
|
765
|
+
:param assetIn: Route asset in.
|
|
766
|
+
:param assetOut: Route asset out.
|
|
767
|
+
:param params: Route parameters.
|
|
768
|
+
"""
|
|
769
|
+
def __init__(self):
|
|
770
|
+
self.assetIn = None
|
|
771
|
+
self.assetOut = None
|
|
772
|
+
self.params = None
|
|
773
|
+
|
|
774
|
+
@classmethod
|
|
775
|
+
def de_json(cls, json_dict):
|
|
776
|
+
data = cls.check_json(json_dict)
|
|
777
|
+
instance = super(SwapRoute, cls).de_json(data, process_mode=2)
|
|
778
|
+
if instance.assetIn is not None:
|
|
779
|
+
instance.assetIn = SwapAsset.de_json(instance.assetIn)
|
|
780
|
+
if instance.assetOut is not None:
|
|
781
|
+
instance.assetOut = SwapAsset.de_json(instance.assetOut)
|
|
782
|
+
if instance.params is not None:
|
|
783
|
+
instance.params = SwapRouteParams.de_json(instance.params)
|
|
784
|
+
return instance
|
|
785
|
+
|
|
786
|
+
|
|
651
787
|
# noinspection method-overriding
|
|
652
788
|
class xPage(xRocketObject):
|
|
653
789
|
"""Base class for paginated xRocket Pay responses.
|
|
@@ -988,6 +1124,25 @@ class WithdrawalsList(xPage):
|
|
|
988
1124
|
return instance
|
|
989
1125
|
|
|
990
1126
|
|
|
1127
|
+
# noinspection method-overriding
|
|
1128
|
+
class SwapsList(xPage):
|
|
1129
|
+
"""Paginated list of application swaps.
|
|
1130
|
+
|
|
1131
|
+
API: https://docs.xrocket.exchange/api/pay/reference/http/swap-controller-get-swaps
|
|
1132
|
+
|
|
1133
|
+
:param items: Swaps.
|
|
1134
|
+
:param pagination: Cursor pagination information.
|
|
1135
|
+
"""
|
|
1136
|
+
@classmethod
|
|
1137
|
+
def de_json(cls, json_dict):
|
|
1138
|
+
data = cls.check_json(json_dict)
|
|
1139
|
+
instance = super(SwapsList, cls).de_json(data)
|
|
1140
|
+
instance.items = [Swap.de_json(item) for item in instance.items]
|
|
1141
|
+
if instance.pagination is not None:
|
|
1142
|
+
instance.pagination = CursorPagination.de_json(instance.pagination)
|
|
1143
|
+
return instance
|
|
1144
|
+
|
|
1145
|
+
|
|
991
1146
|
# noinspection method-overriding
|
|
992
1147
|
class InvoicePaymentAddress(xRocketObject):
|
|
993
1148
|
"""Deposit address created for an invoice payment.
|
|
@@ -35,7 +35,7 @@ Use :func:`parse_and_verify_webhook` for the usual case::
|
|
|
35
35
|
|
|
36
36
|
The returned object is one of :class:`InvoiceStatusChangedWebhook`,
|
|
37
37
|
:class:`PaymentStatusChangedWebhook`, :class:`ChequeWebhook`,
|
|
38
|
-
:class:`PayoutWebhook`, :class:`WithdrawalWebhook`, or
|
|
38
|
+
:class:`PayoutWebhook`, :class:`WithdrawalWebhook`, :class:`SwapWebhook`, or
|
|
39
39
|
:class:`UnknownWebhook`.
|
|
40
40
|
|
|
41
41
|
The top-level webhook ``id`` identifies a delivery and remains stable across
|
|
@@ -71,7 +71,7 @@ from .exceptions import (
|
|
|
71
71
|
xRocketWebhookSignatureException,
|
|
72
72
|
xRocketWebhookUnsupportedVersionException,
|
|
73
73
|
)
|
|
74
|
-
from .models import Cheque, Invoice, InvoicePayment, Payout, Withdrawal, xRocketObject
|
|
74
|
+
from .models import Cheque, Invoice, InvoicePayment, Payout, Swap, Withdrawal, xRocketObject
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
# noinspection method-overriding
|
|
@@ -283,6 +283,57 @@ class WithdrawalWebhook(xRocketWebhook):
|
|
|
283
283
|
return instance
|
|
284
284
|
|
|
285
285
|
|
|
286
|
+
# noinspection method-overriding
|
|
287
|
+
class SwapStatusChangedWebhookData(xRocketObject):
|
|
288
|
+
"""Payload of a ``swap_status_changed`` webhook.
|
|
289
|
+
|
|
290
|
+
API: https://docs.xrocket.exchange/api/pay/webhooks
|
|
291
|
+
|
|
292
|
+
:param event: Event type. This value is ``swap_status_changed``.
|
|
293
|
+
:param swap: Updated swap.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def __init__(self):
|
|
297
|
+
self.event = None
|
|
298
|
+
self.swap = None
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def de_json(cls, json_dict):
|
|
302
|
+
"""Create the payload from a parsed webhook dictionary.
|
|
303
|
+
|
|
304
|
+
:param json_dict: Parsed swap-status-changed payload.
|
|
305
|
+
:return: ``SwapStatusChangedWebhookData``.
|
|
306
|
+
"""
|
|
307
|
+
data = cls.check_json(json_dict)
|
|
308
|
+
instance = super(SwapStatusChangedWebhookData, cls).de_json(data, process_mode=2)
|
|
309
|
+
instance.swap = Swap.de_json(instance.swap)
|
|
310
|
+
return instance
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class SwapWebhook(xRocketWebhook):
|
|
314
|
+
"""Webhook reporting a swap status update.
|
|
315
|
+
|
|
316
|
+
API: https://docs.xrocket.exchange/api/pay/webhooks
|
|
317
|
+
|
|
318
|
+
:param id: Unique webhook delivery id, can be used by the receiver for idempotency.
|
|
319
|
+
:param timestamp: When webhook was sent.
|
|
320
|
+
:param type: Webhook type. This value is ``swap``.
|
|
321
|
+
:param data: ``SwapStatusChangedWebhookData`` payload.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def de_json(cls, json_dict):
|
|
326
|
+
"""Create the webhook from a parsed envelope dictionary.
|
|
327
|
+
|
|
328
|
+
:param json_dict: Parsed swap webhook envelope.
|
|
329
|
+
:return: ``SwapWebhook``.
|
|
330
|
+
"""
|
|
331
|
+
data = cls.check_json(json_dict)
|
|
332
|
+
instance = super(SwapWebhook, cls).de_json(data)
|
|
333
|
+
instance.data = SwapStatusChangedWebhookData.de_json(instance.data)
|
|
334
|
+
return instance
|
|
335
|
+
|
|
336
|
+
|
|
286
337
|
class UnknownWebhook(xRocketWebhook):
|
|
287
338
|
"""Webhook whose type or invoice event is not known to this library.
|
|
288
339
|
|
|
@@ -372,6 +423,8 @@ def parse_webhook(payload: bytes | bytearray | str | Mapping[str, Any]) -> xRock
|
|
|
372
423
|
if webhook_type == "withdrawal":
|
|
373
424
|
_require_mapping(envelope["data"], "data")
|
|
374
425
|
return WithdrawalWebhook.de_json(envelope)
|
|
426
|
+
if webhook_type == "swap":
|
|
427
|
+
return _parse_swap_webhook(envelope)
|
|
375
428
|
return UnknownWebhook.de_json(envelope)
|
|
376
429
|
|
|
377
430
|
|
|
@@ -469,6 +522,15 @@ def _parse_invoice_webhook(envelope: dict[str, Any]) -> xRocketWebhook:
|
|
|
469
522
|
return UnknownWebhook.de_json(envelope)
|
|
470
523
|
|
|
471
524
|
|
|
525
|
+
def _parse_swap_webhook(envelope: dict[str, Any]) -> xRocketWebhook:
|
|
526
|
+
"""Choose the typed swap webhook model using the nested event field."""
|
|
527
|
+
data = _require_mapping(envelope["data"], "data")
|
|
528
|
+
if data.get("event") == "swap_status_changed":
|
|
529
|
+
_require_mapping(data.get("swap"), "data.swap")
|
|
530
|
+
return SwapWebhook.de_json(envelope)
|
|
531
|
+
return UnknownWebhook.de_json(envelope)
|
|
532
|
+
|
|
533
|
+
|
|
472
534
|
def _require_mapping(value: Any, field_name: str) -> Mapping[str, Any]:
|
|
473
535
|
"""Return a payload object or raise a precise structural parse error."""
|
|
474
536
|
if not isinstance(value, Mapping):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyXRocketAPI
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Python client for the xRocket Pay API
|
|
5
5
|
Author: Badiboy
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,10 +8,9 @@ Project-URL: Homepage, https://github.com/Badiboy/pyXRocketAPI
|
|
|
8
8
|
Project-URL: Documentation, https://docs.xrocket.exchange/api/pay/pay-api-overview
|
|
9
9
|
Project-URL: Repository, https://github.com/Badiboy/pyXRocketAPI
|
|
10
10
|
Keywords: xrocket,pay,crypto,payments,api
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
|
|
14
|
-
Requires-Python: >=3.10
|
|
13
|
+
Requires-Python: >=3.11
|
|
15
14
|
Description-Content-Type: text/markdown
|
|
16
15
|
License-File: LICENSE
|
|
17
16
|
Requires-Dist: requests>=2.28
|
|
@@ -7,14 +7,13 @@ name = "pyXRocketAPI"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "Python client for the xRocket Pay API"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
authors = [{ name = "Badiboy" }]
|
|
13
13
|
keywords = ["xrocket", "pay", "crypto", "payments", "api"]
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
16
|
"Programming Language :: Python :: 3",
|
|
17
|
-
"Programming Language :: Python :: 3 :: Only",
|
|
18
17
|
]
|
|
19
18
|
dependencies = ["requests>=2.28"]
|
|
20
19
|
|
|
@@ -3,10 +3,12 @@ from unittest.mock import patch
|
|
|
3
3
|
|
|
4
4
|
from pyXRocketAPI import Cheque, ChequesList, Health, xPage, xRocketAPIException, xRocketPayAPI
|
|
5
5
|
from pyXRocketAPI import api as api_module
|
|
6
|
+
from pyXRocketAPI.version import __version__
|
|
6
7
|
from pyXRocketAPI.models import (
|
|
7
8
|
Balance,
|
|
8
9
|
ChequeCallback,
|
|
9
10
|
ChequeUrl,
|
|
11
|
+
CursorPagination,
|
|
10
12
|
Invoice,
|
|
11
13
|
InvoiceBlockchainTransaction,
|
|
12
14
|
InvoiceCallback,
|
|
@@ -24,6 +26,12 @@ from pyXRocketAPI.models import (
|
|
|
24
26
|
Payout,
|
|
25
27
|
PayoutCallback,
|
|
26
28
|
Rate,
|
|
29
|
+
Swap,
|
|
30
|
+
SwapAsset,
|
|
31
|
+
SwapEstimate,
|
|
32
|
+
SwapRoute,
|
|
33
|
+
SwapRouteParams,
|
|
34
|
+
SwapsList,
|
|
27
35
|
Withdrawal,
|
|
28
36
|
WithdrawalCallback,
|
|
29
37
|
WithdrawalQuotas,
|
|
@@ -54,6 +62,9 @@ class XRocketPayAPITests(unittest.TestCase):
|
|
|
54
62
|
self.request.return_value = response
|
|
55
63
|
return xRocketPayAPI(token=token, timeout=5)
|
|
56
64
|
|
|
65
|
+
def test_package_version_is_1_0_0(self):
|
|
66
|
+
self.assertEqual(__version__, "1.0.0")
|
|
67
|
+
|
|
57
68
|
def test_public_health_requires_no_token(self):
|
|
58
69
|
client = self.client(Response(data={
|
|
59
70
|
"status": "ok",
|
|
@@ -81,15 +92,17 @@ class XRocketPayAPITests(unittest.TestCase):
|
|
|
81
92
|
def test_invoice_body_preserves_false_and_uses_camel_case(self):
|
|
82
93
|
client = self.client(Response(201, {"id": "invoice-1", "links": {"webLink": "https://pay.example"}}))
|
|
83
94
|
invoice = client.create_invoice(
|
|
84
|
-
price_currency="USDT", price_amount="10.00", client_invoice_id="order-1"
|
|
95
|
+
price_currency="USDT", price_amount="10.00", client_invoice_id="order-1"
|
|
85
96
|
)
|
|
86
97
|
self.assertEqual(invoice.links.webLink, "https://pay.example")
|
|
87
98
|
method, url = self.request.call_args.args[:2]
|
|
88
99
|
kwargs = self.request.call_args.kwargs
|
|
89
100
|
self.assertEqual((method, url), ("POST", "https://pay.api.xrocket.exchange/api/v1/invoices"))
|
|
90
101
|
self.assertEqual(kwargs["json"], {
|
|
91
|
-
"priceCurrency": "USDT", "priceAmount": "10.00", "clientInvoiceId": "order-1",
|
|
102
|
+
"priceCurrency": "USDT", "priceAmount": "10.00", "clientInvoiceId": "order-1",
|
|
92
103
|
})
|
|
104
|
+
with self.assertRaises(TypeError):
|
|
105
|
+
client.create_invoice(price_currency="USDT", is_fee_paid_by_user=False)
|
|
93
106
|
|
|
94
107
|
def test_invoice_deserializes_all_nested_response_schemas(self):
|
|
95
108
|
client = self.client(Response(data={
|
|
@@ -208,6 +221,95 @@ class XRocketPayAPITests(unittest.TestCase):
|
|
|
208
221
|
self.assertEqual((payout.amount, withdrawal.amount, rate.rate), (5.5, 6.25, 291.87))
|
|
209
222
|
self.assertEqual((address.minAmount, quotas.withdrawMinSize, quotas.withdrawFee, quotas.precision, error.amount), (0.01, 0.001, 0.003, 6, 7.5))
|
|
210
223
|
|
|
224
|
+
def test_swap_models_decode_numeric_values_and_list_pagination(self):
|
|
225
|
+
swap_source = {
|
|
226
|
+
"id": "swap-1", "status": "pending", "fromAsset": "USDT", "toAsset": "BTC",
|
|
227
|
+
"fromAmount": "100.5", "toAmount": "0.001", "rate": "0.00001", "clientOrderId": "order-1",
|
|
228
|
+
}
|
|
229
|
+
swap = Swap.de_json(swap_source)
|
|
230
|
+
estimate = SwapEstimate.de_json({
|
|
231
|
+
"fromAsset": "USDT", "toAsset": "BTC", "fromAmount": "100.5", "toAmount": "0.001", "rate": "0.00001",
|
|
232
|
+
})
|
|
233
|
+
route = SwapRoute.de_json({
|
|
234
|
+
"assetIn": {"code": "USDT"}, "assetOut": {"code": "BTC"},
|
|
235
|
+
"params": {"minAmount": "1.5", "maxAmount": "10000"},
|
|
236
|
+
})
|
|
237
|
+
swaps = SwapsList.de_json({"items": [swap_source], "pagination": {"total": 1, "next": "cursor-2"}})
|
|
238
|
+
|
|
239
|
+
self.assertEqual((swap.fromAmount, swap.toAmount, swap.rate), (100.5, 0.001, 0.00001))
|
|
240
|
+
self.assertEqual((estimate.fromAmount, estimate.toAmount, estimate.rate), (100.5, 0.001, 0.00001))
|
|
241
|
+
self.assertIsInstance(route.assetIn, SwapAsset)
|
|
242
|
+
self.assertIsInstance(route.params, SwapRouteParams)
|
|
243
|
+
self.assertEqual((route.params.minAmount, route.params.maxAmount), (1.5, 10000.0))
|
|
244
|
+
self.assertIsInstance(swaps.items[0], Swap)
|
|
245
|
+
self.assertIsInstance(swaps.pagination, CursorPagination)
|
|
246
|
+
|
|
247
|
+
def test_create_swap_serializes_exact_in_amount(self):
|
|
248
|
+
client = self.client(Response(201, {
|
|
249
|
+
"id": "swap-1", "status": "pending", "fromAsset": "USDT", "toAsset": "BTC",
|
|
250
|
+
"fromAmount": "100.5", "toAmount": "0.001", "rate": "0.00001", "clientOrderId": "order-1",
|
|
251
|
+
}))
|
|
252
|
+
|
|
253
|
+
swap = client.create_swap("USDT", "BTC", "order-1", amount=100.5)
|
|
254
|
+
|
|
255
|
+
self.assertIsInstance(swap, Swap)
|
|
256
|
+
self.assertEqual(self.request.call_args.args[:2], ("POST", "https://pay.api.xrocket.exchange/api/v1/swap"))
|
|
257
|
+
self.assertEqual(self.request.call_args.kwargs["json"], {
|
|
258
|
+
"fromAsset": "USDT", "toAsset": "BTC", "clientOrderId": "order-1", "amount": "100.5",
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
def test_estimate_swap_serializes_exact_out_amount(self):
|
|
262
|
+
client = self.client(Response(data={
|
|
263
|
+
"fromAsset": "USDT", "toAsset": "BTC", "fromAmount": "100", "toAmount": "0.001", "rate": "0.00001",
|
|
264
|
+
}))
|
|
265
|
+
|
|
266
|
+
estimate = client.estimate_swap("USDT", "BTC", receive_amount=0.001)
|
|
267
|
+
|
|
268
|
+
self.assertIsInstance(estimate, SwapEstimate)
|
|
269
|
+
self.assertEqual(self.request.call_args.args[:2], ("POST", "https://pay.api.xrocket.exchange/api/v1/swap/estimate"))
|
|
270
|
+
self.assertEqual(self.request.call_args.kwargs["json"], {
|
|
271
|
+
"fromAsset": "USDT", "toAsset": "BTC", "receiveAmount": "0.001",
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
def test_swap_amount_modes_reject_both_or_neither_before_request(self):
|
|
275
|
+
client = self.client(Response(data={}))
|
|
276
|
+
|
|
277
|
+
with self.assertRaises(ValueError):
|
|
278
|
+
client.estimate_swap("USDT", "BTC")
|
|
279
|
+
with self.assertRaises(ValueError):
|
|
280
|
+
client.estimate_swap("USDT", "BTC", amount="1", receive_amount="2")
|
|
281
|
+
with self.assertRaises(TypeError):
|
|
282
|
+
client.estimate_swap("USDT", "BTC", amount=True)
|
|
283
|
+
|
|
284
|
+
self.assertEqual(self.request.call_count, 0)
|
|
285
|
+
|
|
286
|
+
def test_swap_queries_deserialize_typed_responses(self):
|
|
287
|
+
client = self.client(Response(data={
|
|
288
|
+
"items": [{
|
|
289
|
+
"id": "swap-1", "status": "completed", "fromAsset": "USDT", "toAsset": "BTC",
|
|
290
|
+
"fromAmount": "100", "toAmount": "0.001", "rate": "0.00001", "clientOrderId": "order-1",
|
|
291
|
+
}], "pagination": {"total": 1, "next": None},
|
|
292
|
+
}))
|
|
293
|
+
swaps = client.get_swaps_list(cursor="next", limit=10)
|
|
294
|
+
self.assertIsInstance(swaps, SwapsList)
|
|
295
|
+
self.assertIsInstance(swaps.items[0], Swap)
|
|
296
|
+
self.assertEqual(self.request.call_args.kwargs["params"], {"cursor": "next", "limit": 10})
|
|
297
|
+
|
|
298
|
+
self.request.return_value = Response(data={"id": "swap-2", "status": "pending", "fromAsset": "USDT", "toAsset": "BTC", "fromAmount": "1", "toAmount": "0.1", "rate": "0.1", "clientOrderId": "order-2"})
|
|
299
|
+
self.assertIsInstance(client.get_swap_info("swap-2"), Swap)
|
|
300
|
+
self.assertEqual(self.request.call_args.args[:2], ("GET", "https://pay.api.xrocket.exchange/api/v1/swap/swap-2"))
|
|
301
|
+
|
|
302
|
+
self.request.return_value = Response(data={"items": [{"code": "USDT"}]})
|
|
303
|
+
self.assertIsInstance(client.get_swap_input_assets(asset_out="BTC")[0], SwapAsset)
|
|
304
|
+
self.assertEqual(self.request.call_args.kwargs["params"], {"assetOut": "BTC"})
|
|
305
|
+
self.assertIsInstance(client.get_swap_output_assets(asset_in="USDT")[0], SwapAsset)
|
|
306
|
+
self.assertEqual(self.request.call_args.kwargs["params"], {"assetIn": "USDT"})
|
|
307
|
+
|
|
308
|
+
self.request.return_value = Response(data={"assetIn": {"code": "USDT"}, "assetOut": {"code": "BTC"}, "params": {"minAmount": "1", "maxAmount": "100"}})
|
|
309
|
+
route = client.get_swap_route_params("USDT", "BTC")
|
|
310
|
+
self.assertIsInstance(route, SwapRoute)
|
|
311
|
+
self.assertEqual(self.request.call_args.kwargs["params"], {"assetIn": "USDT", "assetOut": "BTC"})
|
|
312
|
+
|
|
211
313
|
def test_invoice_payment_deserializes_transaction_subschemas(self):
|
|
212
314
|
client = self.client(Response(data={
|
|
213
315
|
"items": [{
|
|
@@ -10,6 +10,8 @@ from pyXRocketAPI import (
|
|
|
10
10
|
Payout,
|
|
11
11
|
PayoutWebhook,
|
|
12
12
|
PaymentStatusChangedWebhook,
|
|
13
|
+
Swap,
|
|
14
|
+
SwapWebhook,
|
|
13
15
|
UnknownWebhook,
|
|
14
16
|
Withdrawal,
|
|
15
17
|
WithdrawalWebhook,
|
|
@@ -94,6 +96,36 @@ class XRocketWebhookTests(unittest.TestCase):
|
|
|
94
96
|
self.assertIsInstance(event, webhook_class)
|
|
95
97
|
self.assertIsInstance(event.data, data_class)
|
|
96
98
|
|
|
99
|
+
def test_parses_and_verifies_swap_status_changed_webhook(self):
|
|
100
|
+
body, headers = signed_webhook({
|
|
101
|
+
"id": "delivery-swap-1",
|
|
102
|
+
"timestamp": "2025-01-01T00:00:00.000Z",
|
|
103
|
+
"type": "swap",
|
|
104
|
+
"data": {
|
|
105
|
+
"event": "swap_status_changed",
|
|
106
|
+
"swap": {
|
|
107
|
+
"id": "swap-1", "status": "completed", "fromAsset": "USDT", "toAsset": "BTC",
|
|
108
|
+
"fromAmount": "100.5", "toAmount": "0.001", "rate": "0.00001", "clientOrderId": "order-1",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
event = parse_and_verify_webhook(body, headers, WEBHOOK_SECRET)
|
|
114
|
+
|
|
115
|
+
self.assertIsInstance(event, SwapWebhook)
|
|
116
|
+
self.assertIsInstance(event.data.swap, Swap)
|
|
117
|
+
self.assertEqual(event.data.event, "swap_status_changed")
|
|
118
|
+
self.assertEqual(event.data.swap.fromAmount, 100.5)
|
|
119
|
+
|
|
120
|
+
def test_returns_unknown_webhook_for_future_swap_event(self):
|
|
121
|
+
event = parse_webhook({
|
|
122
|
+
"id": "delivery-swap-2", "timestamp": "now", "type": "swap",
|
|
123
|
+
"data": {"event": "future_swap_event", "swap": {"id": "swap-1"}},
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
self.assertIsInstance(event, UnknownWebhook)
|
|
127
|
+
self.assertEqual(event.data["event"], "future_swap_event")
|
|
128
|
+
|
|
97
129
|
def test_rejects_malformed_webhook_envelope(self):
|
|
98
130
|
with self.assertRaises(xRocketWebhookParseException):
|
|
99
131
|
parse_webhook(b"not json")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|