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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyXRocketAPI
3
- Version: 0.3.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 :: 3 - Alpha
11
+ Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3 :: Only
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
- isFeePaidByUser=is_fee_paid_by_user, data=data, platformId=platform_id,
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.
@@ -1,3 +1,3 @@
1
1
  """Package version, read by the build configuration."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "1.0.0"
@@ -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.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 :: 3 - Alpha
11
+ Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3 :: Only
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"
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 :: 3 - Alpha",
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", is_fee_paid_by_user=False
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", "isFeePaidByUser": False,
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