debridge-py 0.1.0__py3-none-any.whl

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.
debridge/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """Typed Python client for the deBridge DLN cross-chain swap/order API."""
2
+
3
+ from debridge import constants
4
+ from debridge._errors import DebridgeAPIError, DebridgeError
5
+ from debridge.client import AsyncDebridgeClient, DebridgeClient
6
+ from debridge.models import (
7
+ CreateOrderResponse,
8
+ Estimation,
9
+ OrderDetails,
10
+ OrderStatus,
11
+ SupportedChain,
12
+ SupportedChainsResponse,
13
+ TokenInfo,
14
+ TokenListResponse,
15
+ Transaction,
16
+ )
17
+
18
+ __all__ = [
19
+ "AsyncDebridgeClient",
20
+ "CreateOrderResponse",
21
+ "DebridgeAPIError",
22
+ "DebridgeClient",
23
+ "DebridgeError",
24
+ "Estimation",
25
+ "OrderDetails",
26
+ "OrderStatus",
27
+ "SupportedChain",
28
+ "SupportedChainsResponse",
29
+ "TokenInfo",
30
+ "TokenListResponse",
31
+ "Transaction",
32
+ "constants",
33
+ ]
34
+
35
+ __version__ = "0.1.0"
debridge/_errors.py ADDED
@@ -0,0 +1,66 @@
1
+ """Exception hierarchy for the deBridge client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ class DebridgeError(Exception):
9
+ """Base class for all errors raised by this library.
10
+
11
+ Catch this to handle any failure originating from ``debridge`` (currently
12
+ just :class:`DebridgeAPIError`). Network-level failures from the underlying
13
+ ``httpx`` client propagate as ``httpx`` exceptions and are not wrapped.
14
+ """
15
+
16
+
17
+ class DebridgeAPIError(DebridgeError):
18
+ """Raised when the deBridge API returns an error response.
19
+
20
+ The DLN API signals errors with a JSON body of the shape
21
+ ``{"errorCode", "errorId", "errorMessage", "reqId"}``. Crucially, errors are
22
+ surfaced in **two** ways:
23
+
24
+ * a non-2xx HTTP status (e.g. ``400 INVALID_QUERY_PARAMETERS`` /
25
+ ``UNKNOWN_ORDER``), and
26
+ * an HTTP **200** response whose body nonetheless carries an ``errorId``
27
+ (e.g. ``COMPLIANCE_ADDRESS_BLOCKED``).
28
+
29
+ The client detects both, so callers should rely on catching this exception
30
+ rather than inspecting the HTTP status code themselves.
31
+
32
+ Attributes:
33
+ error_id: The API's ``errorId`` string (e.g. ``"UNKNOWN_ORDER"``), or
34
+ ``None`` if the response carried no error id.
35
+ error_code: The numeric ``errorCode``, or ``None``.
36
+ message: The ``errorMessage`` text, falling back to ``"HTTP <status>"``.
37
+ req_id: The API's ``reqId`` (useful for support tickets), or ``None``.
38
+ status_code: The HTTP status code of the response (may be ``200``).
39
+
40
+ Example::
41
+
42
+ from debridge import DebridgeClient, DebridgeAPIError
43
+
44
+ with DebridgeClient() as client:
45
+ try:
46
+ client.get_order_status("0xnot-an-order")
47
+ except DebridgeAPIError as exc:
48
+ print(exc.error_id, exc.status_code) # 'UNKNOWN_ORDER' 400
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ *,
54
+ error_id: Optional[str],
55
+ error_code: Optional[int],
56
+ message: str,
57
+ req_id: Optional[str],
58
+ status_code: int,
59
+ ) -> None:
60
+ self.error_id = error_id
61
+ self.error_code = error_code
62
+ self.message = message
63
+ self.req_id = req_id
64
+ self.status_code = status_code
65
+ label = error_id or f"HTTP {status_code}"
66
+ super().__init__(f"[{label}] {message}")
debridge/_transport.py ADDED
@@ -0,0 +1,124 @@
1
+ """Shared request-building and response/error-handling helpers.
2
+
3
+ These are VM-agnostic and used by both the sync and async clients so the
4
+ behaviour (query construction, error detection) is identical.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, Optional, Union
10
+
11
+ import httpx
12
+
13
+ from debridge._errors import DebridgeAPIError
14
+
15
+
16
+ def normalize_base_url(base_url: str) -> str:
17
+ """Strip a single trailing slash so path joins are predictable."""
18
+ return base_url.rstrip("/")
19
+
20
+
21
+ def build_create_order_params(
22
+ *,
23
+ src_chain_id: int,
24
+ src_chain_token_in: str,
25
+ src_chain_token_in_amount: str,
26
+ dst_chain_id: int,
27
+ dst_chain_token_out: str,
28
+ dst_chain_token_out_recipient: str,
29
+ sender_address: str,
30
+ dst_chain_token_out_amount: str = "auto",
31
+ src_chain_order_authority_address: Optional[str] = None,
32
+ dst_chain_order_authority_address: Optional[str] = None,
33
+ referral_code: Optional[int] = None,
34
+ affiliate_fee_percent: Optional[float] = None,
35
+ affiliate_fee_recipient: Optional[str] = None,
36
+ prepend_operating_expenses: Optional[bool] = None,
37
+ extra: Optional[Dict[str, Any]] = None,
38
+ ) -> Dict[str, str]:
39
+ """Build the query params for ``GET /dln/order/create-tx``.
40
+
41
+ The order authority addresses default to the sender (EVM) / recipient
42
+ (Solana) when omitted, matching the deBridge dApp's behaviour: the source
43
+ authority defaults to the sender, the destination authority to the
44
+ recipient.
45
+ """
46
+ params: Dict[str, str] = {
47
+ # int() unwraps IntEnum members (e.g. ChainId.BASE) so they render as
48
+ # "8453" rather than "ChainId.BASE" (Python < 3.11 str() behaviour).
49
+ "srcChainId": str(int(src_chain_id)),
50
+ "srcChainTokenIn": src_chain_token_in,
51
+ "srcChainTokenInAmount": src_chain_token_in_amount,
52
+ "dstChainId": str(int(dst_chain_id)),
53
+ "dstChainTokenOut": dst_chain_token_out,
54
+ "dstChainTokenOutAmount": dst_chain_token_out_amount,
55
+ "dstChainTokenOutRecipient": dst_chain_token_out_recipient,
56
+ "senderAddress": sender_address,
57
+ "srcChainOrderAuthorityAddress": src_chain_order_authority_address or sender_address,
58
+ "dstChainOrderAuthorityAddress": dst_chain_order_authority_address
59
+ or dst_chain_token_out_recipient,
60
+ }
61
+ if referral_code is not None:
62
+ params["referralCode"] = str(int(referral_code))
63
+ if affiliate_fee_percent is not None:
64
+ params["affiliateFeePercent"] = str(affiliate_fee_percent)
65
+ if affiliate_fee_recipient is not None:
66
+ params["affiliateFeeRecipient"] = affiliate_fee_recipient
67
+ if prepend_operating_expenses is not None:
68
+ params["prependOperatingExpenses"] = str(prepend_operating_expenses).lower()
69
+ if extra:
70
+ params.update({k: str(v) for k, v in extra.items()})
71
+ return params
72
+
73
+
74
+ def parse_response(response: httpx.Response) -> Dict[str, Any]:
75
+ """Return the JSON body, raising ``DebridgeAPIError`` on any error.
76
+
77
+ deBridge signals errors in two ways: a non-2xx status, OR an HTTP 200 body
78
+ that nonetheless carries an ``errorId``/``errorCode`` (e.g. compliance
79
+ blocks). Both are handled here.
80
+ """
81
+ data: Union[Dict[str, Any], Any]
82
+ try:
83
+ data = response.json()
84
+ except ValueError:
85
+ # Non-JSON body (e.g. an HTML 5xx page). Surface the status.
86
+ if response.is_error:
87
+ raise DebridgeAPIError(
88
+ error_id=None,
89
+ error_code=None,
90
+ message=response.text or f"HTTP {response.status_code}",
91
+ req_id=None,
92
+ status_code=response.status_code,
93
+ ) from None
94
+ raise DebridgeAPIError(
95
+ error_id=None,
96
+ error_code=None,
97
+ message="Response body was not valid JSON",
98
+ req_id=None,
99
+ status_code=response.status_code,
100
+ ) from None
101
+
102
+ # An error if the body carries an errorId (even on HTTP 200, e.g. compliance
103
+ # blocks) OR the HTTP status itself is non-2xx.
104
+ has_error_body = isinstance(data, dict) and "errorId" in data
105
+ if has_error_body or response.is_error:
106
+ body = data if isinstance(data, dict) else {}
107
+ message = str(body.get("errorMessage") or "") or f"HTTP {response.status_code}"
108
+ raise DebridgeAPIError(
109
+ error_id=body.get("errorId"),
110
+ error_code=body.get("errorCode"),
111
+ message=message,
112
+ req_id=body.get("reqId"),
113
+ status_code=response.status_code,
114
+ )
115
+
116
+ if not isinstance(data, dict):
117
+ raise DebridgeAPIError(
118
+ error_id=None,
119
+ error_code=None,
120
+ message="Expected a JSON object response",
121
+ req_id=None,
122
+ status_code=response.status_code,
123
+ )
124
+ return data