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 +35 -0
- debridge/_errors.py +66 -0
- debridge/_transport.py +124 -0
- debridge/client.py +590 -0
- debridge/constants.py +59 -0
- debridge/models.py +334 -0
- debridge/py.typed +0 -0
- debridge_py-0.1.0.dist-info/METADATA +157 -0
- debridge_py-0.1.0.dist-info/RECORD +11 -0
- debridge_py-0.1.0.dist-info/WHEEL +4 -0
- debridge_py-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|