exolix-client 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.
- exolix/__init__.py +28 -0
- exolix/client.py +377 -0
- exolix/errors.py +23 -0
- exolix/types.py +103 -0
- exolix_client-0.1.0.dist-info/METADATA +206 -0
- exolix_client-0.1.0.dist-info/RECORD +8 -0
- exolix_client-0.1.0.dist-info/WHEEL +4 -0
- exolix_client-0.1.0.dist-info/licenses/LICENSE +15 -0
exolix/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Exolix cryptocurrency exchange API (v2) client."""
|
|
2
|
+
|
|
3
|
+
from .client import EXOLIX_API_KEY_HEADER, Exolix
|
|
4
|
+
from .errors import ExolixError
|
|
5
|
+
from .types import (
|
|
6
|
+
Currency,
|
|
7
|
+
Network,
|
|
8
|
+
PaginatedCurrencies,
|
|
9
|
+
PaginatedNetworks,
|
|
10
|
+
PaginatedTransactions,
|
|
11
|
+
Rate,
|
|
12
|
+
RateType,
|
|
13
|
+
Transaction,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"EXOLIX_API_KEY_HEADER",
|
|
18
|
+
"Currency",
|
|
19
|
+
"Exolix",
|
|
20
|
+
"ExolixError",
|
|
21
|
+
"Network",
|
|
22
|
+
"PaginatedCurrencies",
|
|
23
|
+
"PaginatedNetworks",
|
|
24
|
+
"PaginatedTransactions",
|
|
25
|
+
"Rate",
|
|
26
|
+
"RateType",
|
|
27
|
+
"Transaction",
|
|
28
|
+
]
|
exolix/client.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from urllib.parse import quote
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .errors import ExolixError
|
|
7
|
+
from .types import (
|
|
8
|
+
Network,
|
|
9
|
+
PaginatedCurrencies,
|
|
10
|
+
PaginatedNetworks,
|
|
11
|
+
PaginatedTransactions,
|
|
12
|
+
Rate,
|
|
13
|
+
RateType,
|
|
14
|
+
SortOrder,
|
|
15
|
+
Transaction,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Header that carries the affiliate API key.
|
|
19
|
+
#
|
|
20
|
+
# Partner attribution is the API key itself. The default affiliate share is
|
|
21
|
+
# 0.3%. There is no referral parameter.
|
|
22
|
+
EXOLIX_API_KEY_HEADER = "Authorization"
|
|
23
|
+
|
|
24
|
+
DEFAULT_BASE_URL = "https://exolix.com/api/v2"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _defined(values: dict[str, Any]) -> dict[str, Any]:
|
|
28
|
+
params: dict[str, Any] = {}
|
|
29
|
+
for key, value in values.items():
|
|
30
|
+
if value is None or value == "":
|
|
31
|
+
continue
|
|
32
|
+
if isinstance(value, bool):
|
|
33
|
+
params[key] = str(value).lower()
|
|
34
|
+
elif isinstance(value, list):
|
|
35
|
+
if not value:
|
|
36
|
+
continue
|
|
37
|
+
params[key] = ",".join(str(item) for item in value)
|
|
38
|
+
else:
|
|
39
|
+
params[key] = value
|
|
40
|
+
return params
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _amount(value: float | str | None) -> str | None:
|
|
44
|
+
if value is None:
|
|
45
|
+
return None
|
|
46
|
+
return str(value)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _error_parts(data: Any, fallback: str) -> tuple[str, str | None]:
|
|
50
|
+
if isinstance(data, str) and data.strip():
|
|
51
|
+
return data.strip(), None
|
|
52
|
+
if isinstance(data, dict):
|
|
53
|
+
code_source = data.get("error")
|
|
54
|
+
code = None if code_source is None else str(code_source)
|
|
55
|
+
message_source = data.get("message", data.get("error"))
|
|
56
|
+
if isinstance(message_source, list):
|
|
57
|
+
message_source = ", ".join(str(item) for item in message_source)
|
|
58
|
+
message = fallback if message_source in (None, "") else str(message_source)
|
|
59
|
+
return message, code
|
|
60
|
+
return fallback, None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Exolix:
|
|
64
|
+
"""Client for the Exolix cryptocurrency exchange API (v2).
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
api_key: Affiliate API key. Sent only in the Authorization header.
|
|
68
|
+
base_url: Base URL of the API.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
api_key: str | None = None,
|
|
74
|
+
base_url: str | None = None,
|
|
75
|
+
timeout: float = 30.0,
|
|
76
|
+
user_agent: str | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Initializes the Exolix client.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
api_key: Affiliate API key. Not read from the environment.
|
|
82
|
+
Attribution is this key (default affiliate share 0.3%).
|
|
83
|
+
base_url: API base URL. Defaults to https://exolix.com/api/v2.
|
|
84
|
+
timeout: Timeout in seconds for HTTP requests. Defaults to 30.0.
|
|
85
|
+
user_agent: Custom User-Agent header string.
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
>>> from exolix import Exolix
|
|
89
|
+
>>> client = Exolix(api_key="your-api-key")
|
|
90
|
+
>>> currencies = client.get_currencies(page=1, size=10)
|
|
91
|
+
>>> print(currencies["count"])
|
|
92
|
+
"""
|
|
93
|
+
self.api_key = api_key
|
|
94
|
+
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
95
|
+
ua = user_agent or "exolix-client/0.1.0"
|
|
96
|
+
|
|
97
|
+
headers: dict[str, str] = {
|
|
98
|
+
"User-Agent": ua,
|
|
99
|
+
"Accept": "application/json",
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
}
|
|
102
|
+
if self.api_key:
|
|
103
|
+
headers[EXOLIX_API_KEY_HEADER] = self.api_key
|
|
104
|
+
|
|
105
|
+
self._client = httpx.Client(
|
|
106
|
+
base_url=self.base_url,
|
|
107
|
+
timeout=timeout,
|
|
108
|
+
headers=headers,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def close(self) -> None:
|
|
112
|
+
"""Closes the underlying HTTP client session."""
|
|
113
|
+
self._client.close()
|
|
114
|
+
|
|
115
|
+
def __enter__(self) -> "Exolix":
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
119
|
+
self.close()
|
|
120
|
+
|
|
121
|
+
def _request(
|
|
122
|
+
self,
|
|
123
|
+
method: str,
|
|
124
|
+
path: str,
|
|
125
|
+
params: dict[str, Any] | None = None,
|
|
126
|
+
json_data: dict[str, Any] | None = None,
|
|
127
|
+
) -> Any:
|
|
128
|
+
try:
|
|
129
|
+
response = self._client.request(method, path, params=params, json=json_data)
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
return response.json()
|
|
132
|
+
except httpx.HTTPStatusError as exc:
|
|
133
|
+
try:
|
|
134
|
+
data_err = exc.response.json()
|
|
135
|
+
except ValueError:
|
|
136
|
+
data_err = exc.response.text or None
|
|
137
|
+
message, code = _error_parts(data_err, "Exolix API error")
|
|
138
|
+
raise ExolixError(
|
|
139
|
+
message=message,
|
|
140
|
+
status_code=exc.response.status_code,
|
|
141
|
+
code=code,
|
|
142
|
+
response_body=data_err,
|
|
143
|
+
) from exc
|
|
144
|
+
except httpx.RequestError as exc:
|
|
145
|
+
raise ExolixError(f"HTTP request failed: {exc}") from exc
|
|
146
|
+
|
|
147
|
+
def get_currencies(
|
|
148
|
+
self,
|
|
149
|
+
page: int | None = None,
|
|
150
|
+
size: int | None = None,
|
|
151
|
+
search: str | None = None,
|
|
152
|
+
with_networks: bool | None = None,
|
|
153
|
+
) -> PaginatedCurrencies:
|
|
154
|
+
"""Lists currencies. Set with_networks to include each coin's networks.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
page: Page number.
|
|
158
|
+
size: Page size.
|
|
159
|
+
search: Currency code or name filter.
|
|
160
|
+
with_networks: Include networks on each currency.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Page of currencies and the total count.
|
|
164
|
+
|
|
165
|
+
Examples:
|
|
166
|
+
>>> page = client.get_currencies(search="BTC", with_networks=True)
|
|
167
|
+
"""
|
|
168
|
+
params = _defined(
|
|
169
|
+
{"page": page, "size": size, "search": search, "withNetworks": with_networks}
|
|
170
|
+
)
|
|
171
|
+
return self._request("GET", "/currencies", params=params or None)
|
|
172
|
+
|
|
173
|
+
def get_currency_networks(self, code: str) -> list[Network]:
|
|
174
|
+
"""Lists networks for one currency code.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
code: Currency code, for example ``ETH``.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Networks for that currency.
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
>>> networks = client.get_currency_networks("ETH")
|
|
184
|
+
"""
|
|
185
|
+
return self._request("GET", f"/currencies/{quote(code, safe='')}/networks")
|
|
186
|
+
|
|
187
|
+
def get_networks(
|
|
188
|
+
self,
|
|
189
|
+
page: int | None = None,
|
|
190
|
+
size: int | None = None,
|
|
191
|
+
search: str | None = None,
|
|
192
|
+
) -> PaginatedNetworks:
|
|
193
|
+
"""Lists every network in the catalog.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
page: Page number.
|
|
197
|
+
size: Page size.
|
|
198
|
+
search: Network name filter.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Page of networks and the total count.
|
|
202
|
+
|
|
203
|
+
Examples:
|
|
204
|
+
>>> networks = client.get_networks(page=1, size=50)
|
|
205
|
+
"""
|
|
206
|
+
params = _defined({"page": page, "size": size, "search": search})
|
|
207
|
+
return self._request("GET", "/currencies/networks", params=params or None)
|
|
208
|
+
|
|
209
|
+
def get_rate(
|
|
210
|
+
self,
|
|
211
|
+
coin_from: str,
|
|
212
|
+
coin_to: str,
|
|
213
|
+
rate_type: RateType,
|
|
214
|
+
*,
|
|
215
|
+
network_from: str | None = None,
|
|
216
|
+
network_to: str | None = None,
|
|
217
|
+
amount: float | str | None = None,
|
|
218
|
+
withdrawal_amount: float | str | None = None,
|
|
219
|
+
) -> Rate:
|
|
220
|
+
"""Quotes a fixed or floating rate.
|
|
221
|
+
|
|
222
|
+
Pass amount (what you send) or withdrawal_amount (what you want to
|
|
223
|
+
receive). Partner attribution is the API key, not a query field.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
coin_from: Deposit currency code.
|
|
227
|
+
coin_to: Payout currency code.
|
|
228
|
+
rate_type: ``float`` or ``fixed``.
|
|
229
|
+
network_from: Deposit network.
|
|
230
|
+
network_to: Payout network.
|
|
231
|
+
amount: Deposit amount.
|
|
232
|
+
withdrawal_amount: Desired payout amount.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Quote with amounts, rate, and limits.
|
|
236
|
+
|
|
237
|
+
Examples:
|
|
238
|
+
>>> quote = client.get_rate("BTC", "ETH", "float", amount="0.1")
|
|
239
|
+
"""
|
|
240
|
+
params = _defined(
|
|
241
|
+
{
|
|
242
|
+
"coinFrom": coin_from,
|
|
243
|
+
"networkFrom": network_from,
|
|
244
|
+
"coinTo": coin_to,
|
|
245
|
+
"networkTo": network_to,
|
|
246
|
+
"amount": _amount(amount),
|
|
247
|
+
"withdrawalAmount": _amount(withdrawal_amount),
|
|
248
|
+
"rateType": rate_type,
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
return self._request("GET", "/rate", params=params)
|
|
252
|
+
|
|
253
|
+
def create_transaction(
|
|
254
|
+
self,
|
|
255
|
+
coin_from: str,
|
|
256
|
+
coin_to: str,
|
|
257
|
+
network_from: str,
|
|
258
|
+
network_to: str,
|
|
259
|
+
withdrawal_address: str,
|
|
260
|
+
*,
|
|
261
|
+
amount: float | str | None = None,
|
|
262
|
+
withdrawal_amount: float | str | None = None,
|
|
263
|
+
withdrawal_extra_id: str | None = None,
|
|
264
|
+
rate_type: RateType | None = None,
|
|
265
|
+
refund_address: str | None = None,
|
|
266
|
+
refund_extra_id: str | None = None,
|
|
267
|
+
slippage: float | str | None = None,
|
|
268
|
+
) -> Transaction:
|
|
269
|
+
"""Creates an exchange and returns the deposit address.
|
|
270
|
+
|
|
271
|
+
This opens a real swap. Call it only when a transaction should be created.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
coin_from: Deposit currency code.
|
|
275
|
+
coin_to: Payout currency code.
|
|
276
|
+
network_from: Deposit network.
|
|
277
|
+
network_to: Payout network.
|
|
278
|
+
withdrawal_address: Address that receives the payout.
|
|
279
|
+
amount: Deposit amount.
|
|
280
|
+
withdrawal_amount: Desired payout amount.
|
|
281
|
+
withdrawal_extra_id: Memo or extra id for the payout address.
|
|
282
|
+
rate_type: ``float`` or ``fixed``. Defaults to float on the API.
|
|
283
|
+
refund_address: Address used if the swap is refunded.
|
|
284
|
+
refund_extra_id: Memo or extra id for the refund address.
|
|
285
|
+
slippage: Allowed rate movement, percent. Requires a refund address.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Created transaction, including the deposit address.
|
|
289
|
+
|
|
290
|
+
Examples:
|
|
291
|
+
>>> tx = client.create_transaction(
|
|
292
|
+
... "ETH",
|
|
293
|
+
... "USDT",
|
|
294
|
+
... "ETH",
|
|
295
|
+
... "ETH",
|
|
296
|
+
... "0x0E29D1E501f90649Ad5982E900c455006e4522FC",
|
|
297
|
+
... amount=0.5,
|
|
298
|
+
... rate_type="float",
|
|
299
|
+
... )
|
|
300
|
+
"""
|
|
301
|
+
payload = _defined(
|
|
302
|
+
{
|
|
303
|
+
"coinFrom": coin_from,
|
|
304
|
+
"networkFrom": network_from,
|
|
305
|
+
"coinTo": coin_to,
|
|
306
|
+
"networkTo": network_to,
|
|
307
|
+
"amount": amount,
|
|
308
|
+
"withdrawalAmount": withdrawal_amount,
|
|
309
|
+
"withdrawalAddress": withdrawal_address,
|
|
310
|
+
"withdrawalExtraId": withdrawal_extra_id,
|
|
311
|
+
"rateType": rate_type,
|
|
312
|
+
"refundAddress": refund_address,
|
|
313
|
+
"refundExtraId": refund_extra_id,
|
|
314
|
+
"slippage": slippage,
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
return self._request("POST", "/transactions", json_data=payload)
|
|
318
|
+
|
|
319
|
+
def get_transaction(self, transaction_id: str) -> Transaction:
|
|
320
|
+
"""Fetches one transaction by id.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
transaction_id: Transaction id returned by create.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Transaction status and addresses.
|
|
327
|
+
|
|
328
|
+
Examples:
|
|
329
|
+
>>> tx = client.get_transaction("ab93061434dd0s")
|
|
330
|
+
"""
|
|
331
|
+
return self._request("GET", f"/transactions/{quote(transaction_id, safe='')}")
|
|
332
|
+
|
|
333
|
+
def get_transactions(
|
|
334
|
+
self,
|
|
335
|
+
page: int | None = None,
|
|
336
|
+
size: int | None = None,
|
|
337
|
+
search: str | None = None,
|
|
338
|
+
sort: str | None = None,
|
|
339
|
+
order: SortOrder | None = None,
|
|
340
|
+
date_from: str | None = None,
|
|
341
|
+
date_to: str | None = None,
|
|
342
|
+
statuses: str | list[str] | None = None,
|
|
343
|
+
) -> PaginatedTransactions:
|
|
344
|
+
"""Lists transaction history for the affiliate API key.
|
|
345
|
+
|
|
346
|
+
Exolix enables this route per key. A rejected request means history is
|
|
347
|
+
not activated for that key.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
page: Page number.
|
|
351
|
+
size: Page size.
|
|
352
|
+
search: Transaction id filter.
|
|
353
|
+
sort: Field to sort by.
|
|
354
|
+
order: ``asc`` or ``desc``.
|
|
355
|
+
date_from: Inclusive start datetime.
|
|
356
|
+
date_to: Inclusive end datetime.
|
|
357
|
+
statuses: One status or a list. A list is sent comma-separated.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Page of transactions and the total count.
|
|
361
|
+
|
|
362
|
+
Examples:
|
|
363
|
+
>>> history = client.get_transactions(page=1, size=25, order="desc")
|
|
364
|
+
"""
|
|
365
|
+
params = _defined(
|
|
366
|
+
{
|
|
367
|
+
"page": page,
|
|
368
|
+
"size": size,
|
|
369
|
+
"search": search,
|
|
370
|
+
"sort": sort,
|
|
371
|
+
"order": order,
|
|
372
|
+
"dateFrom": date_from,
|
|
373
|
+
"dateTo": date_to,
|
|
374
|
+
"statuses": statuses,
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
return self._request("GET", "/transactions", params=params or None)
|
exolix/errors.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExolixError(Exception):
|
|
5
|
+
"""Exception raised when an Exolix API operation fails."""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
message: str,
|
|
10
|
+
status_code: int | None = None,
|
|
11
|
+
code: str | None = None,
|
|
12
|
+
response_body: Any = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.message = message
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
self.code = code
|
|
18
|
+
self.response_body = response_body
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
if self.status_code is not None:
|
|
22
|
+
return f"[{self.status_code}] {self.message}"
|
|
23
|
+
return self.message
|
exolix/types.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Literal, TypedDict
|
|
2
|
+
|
|
3
|
+
RateType = Literal["float", "fixed"]
|
|
4
|
+
SortOrder = Literal["asc", "desc"]
|
|
5
|
+
TransactionStatus = Literal[
|
|
6
|
+
"wait",
|
|
7
|
+
"confirmation",
|
|
8
|
+
"confirmed",
|
|
9
|
+
"exchanging",
|
|
10
|
+
"sending",
|
|
11
|
+
"success",
|
|
12
|
+
"overdue",
|
|
13
|
+
"refund",
|
|
14
|
+
"refunded",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Network(TypedDict, total=False):
|
|
19
|
+
network: str
|
|
20
|
+
name: str
|
|
21
|
+
shortName: str | None
|
|
22
|
+
notes: str | None
|
|
23
|
+
addressRegex: str | None
|
|
24
|
+
isDefault: bool
|
|
25
|
+
blockExplorer: str | None
|
|
26
|
+
memoNeeded: bool
|
|
27
|
+
memoName: str | None
|
|
28
|
+
memoRegex: str | None
|
|
29
|
+
precision: int
|
|
30
|
+
decimal: int | None
|
|
31
|
+
contract: str | None
|
|
32
|
+
icon: str | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Currency(TypedDict, total=False):
|
|
36
|
+
code: str
|
|
37
|
+
name: str
|
|
38
|
+
icon: str
|
|
39
|
+
notes: str | None
|
|
40
|
+
networks: list[Network]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Rate(TypedDict, total=False):
|
|
44
|
+
fromAmount: float
|
|
45
|
+
toAmount: float
|
|
46
|
+
rate: float
|
|
47
|
+
message: str | None
|
|
48
|
+
minAmount: float
|
|
49
|
+
withdrawMin: float
|
|
50
|
+
maxAmount: float
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TransactionCoin(TypedDict, total=False):
|
|
54
|
+
coinCode: str
|
|
55
|
+
coinName: str
|
|
56
|
+
network: str
|
|
57
|
+
networkName: str
|
|
58
|
+
networkShortName: str | None
|
|
59
|
+
icon: str | None
|
|
60
|
+
memoName: str | None
|
|
61
|
+
contract: str | None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TransactionHash(TypedDict, total=False):
|
|
65
|
+
hash: str | None
|
|
66
|
+
link: str | None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Transaction(TypedDict, total=False):
|
|
70
|
+
id: str
|
|
71
|
+
amount: float
|
|
72
|
+
amountTo: float
|
|
73
|
+
coinFrom: TransactionCoin
|
|
74
|
+
coinTo: TransactionCoin
|
|
75
|
+
comment: str | None
|
|
76
|
+
createdAt: str
|
|
77
|
+
depositAddress: str
|
|
78
|
+
depositExtraId: str | None
|
|
79
|
+
withdrawalAddress: str
|
|
80
|
+
withdrawalExtraId: str | None
|
|
81
|
+
hashIn: TransactionHash
|
|
82
|
+
hashOut: TransactionHash
|
|
83
|
+
rate: float
|
|
84
|
+
rateType: str
|
|
85
|
+
refundAddress: str | None
|
|
86
|
+
refundExtraId: str | None
|
|
87
|
+
status: str
|
|
88
|
+
source: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PaginatedCurrencies(TypedDict):
|
|
92
|
+
data: list[Currency]
|
|
93
|
+
count: int
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class PaginatedNetworks(TypedDict):
|
|
97
|
+
data: list[Network]
|
|
98
|
+
count: int
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PaginatedTransactions(TypedDict):
|
|
102
|
+
data: list[Transaction]
|
|
103
|
+
count: int
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: exolix-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Exolix cryptocurrency exchange API (v2)
|
|
5
|
+
Project-URL: Repository, https://github.com/TysikGG/api-kit
|
|
6
|
+
Project-URL: Documentation, https://exolix.com/developers
|
|
7
|
+
License-Expression: ISC
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# exolix-client
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/exolix-client/)
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
[](https://pypi.org/project/exolix-client/)
|
|
18
|
+
|
|
19
|
+
Modern, fully typed Python client library for the [Exolix](https://exolix.com) instant cryptocurrency exchange API (v2).
|
|
20
|
+
|
|
21
|
+
Official API documentation: <https://exolix.com/developers>
|
|
22
|
+
|
|
23
|
+
The PyPI package is `exolix-client`. Import it as `exolix`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- ⚡ **Fixed & Floating Rates** — Quote a swap by deposit `amount` or by desired `withdrawal_amount`, at a floating or fixed rate.
|
|
30
|
+
- 🪙 **Currencies & Networks** — List coins, networks for one coin, and the full network catalog.
|
|
31
|
+
- 🐍 **Modern Pythonic Design** — Type annotations, context manager support (`with Exolix(...) as client:`), and a custom exception.
|
|
32
|
+
- 🔄 **Transactions** — Create a swap, fetch it by id, and list affiliate transaction history.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install exolix-client
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Python 3.10 or higher.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Authentication
|
|
47
|
+
|
|
48
|
+
Partner attribution is the affiliate API key. The default affiliate share is 0.3%. The client sends the key in one header and does not add a referral parameter:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from exolix import EXOLIX_API_KEY_HEADER
|
|
52
|
+
|
|
53
|
+
print(EXOLIX_API_KEY_HEADER) # "Authorization"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pass the key to the constructor. The client does not read `EXOLIX_API_KEY` from the environment. Public reads work without a key. Transaction history requires a key that Exolix has enabled for that route.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick Start (Safe Check)
|
|
61
|
+
|
|
62
|
+
The quick check only queries currencies and a rate. It does not create a transaction:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import os
|
|
66
|
+
from exolix import Exolix
|
|
67
|
+
|
|
68
|
+
with Exolix(api_key=os.environ["EXOLIX_API_KEY"]) as client:
|
|
69
|
+
currencies = client.get_currencies(page=1, size=5, with_networks=True)
|
|
70
|
+
print(f"Available currencies: {currencies['count']}")
|
|
71
|
+
|
|
72
|
+
quote = client.get_rate(
|
|
73
|
+
"BTC",
|
|
74
|
+
"ETH",
|
|
75
|
+
"float",
|
|
76
|
+
network_from="BTC",
|
|
77
|
+
network_to="ETH",
|
|
78
|
+
amount="0.1",
|
|
79
|
+
)
|
|
80
|
+
print(f"0.1 BTC -> {quote['toAmount']} ETH (rate {quote['rate']})")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Usage Guide
|
|
86
|
+
|
|
87
|
+
### 1. Creating a Floating Rate Swap
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from exolix import Exolix, ExolixError
|
|
91
|
+
|
|
92
|
+
with Exolix(api_key="your_api_key") as client:
|
|
93
|
+
quote = client.get_rate(
|
|
94
|
+
"ETH",
|
|
95
|
+
"USDT",
|
|
96
|
+
"float",
|
|
97
|
+
network_from="ETH",
|
|
98
|
+
network_to="ETH",
|
|
99
|
+
amount="0.5",
|
|
100
|
+
)
|
|
101
|
+
print(f"Min {quote['minAmount']}, you receive {quote['toAmount']} USDT")
|
|
102
|
+
|
|
103
|
+
tx = client.create_transaction(
|
|
104
|
+
"ETH",
|
|
105
|
+
"USDT",
|
|
106
|
+
"ETH",
|
|
107
|
+
"ETH",
|
|
108
|
+
"0x0E29D1E501f90649Ad5982E900c455006e4522FC",
|
|
109
|
+
amount=0.5,
|
|
110
|
+
rate_type="float",
|
|
111
|
+
)
|
|
112
|
+
print(f"Send ETH to {tx['depositAddress']} ({tx['id']})")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`create_transaction` opens a real swap. Quote with `get_rate` first.
|
|
116
|
+
|
|
117
|
+
### 2. Fixed Rate by Withdrawal Amount
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
quote = client.get_rate(
|
|
121
|
+
"ETH",
|
|
122
|
+
"USDT",
|
|
123
|
+
"fixed",
|
|
124
|
+
network_from="ETH",
|
|
125
|
+
network_to="ETH",
|
|
126
|
+
withdrawal_amount="50",
|
|
127
|
+
)
|
|
128
|
+
print(f"Send {quote['fromAmount']} ETH to receive {quote['toAmount']} USDT")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 3. Tracking Status and History
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
tx = client.get_transaction("ab93061434dd0s")
|
|
135
|
+
print(tx["status"]) # wait, confirmation, confirmed, exchanging, sending, success, ...
|
|
136
|
+
|
|
137
|
+
history = client.get_transactions(page=1, size=25, order="desc")
|
|
138
|
+
print(history["count"])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
History is authenticated. Exolix enables it per API key.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## API Reference
|
|
146
|
+
|
|
147
|
+
### `Exolix` Class
|
|
148
|
+
|
|
149
|
+
#### Constructor
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
Exolix(
|
|
153
|
+
api_key: str | None = None,
|
|
154
|
+
base_url: str | None = None,
|
|
155
|
+
timeout: float = 30.0,
|
|
156
|
+
user_agent: str | None = None,
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Option | Type | Default | Description |
|
|
161
|
+
| :--- | :--- | :--- | :--- |
|
|
162
|
+
| `api_key` | `str` | unset | Affiliate API key. Sent as `Authorization`. |
|
|
163
|
+
| `base_url` | `str` | `"https://exolix.com/api/v2"` | API base URL. |
|
|
164
|
+
| `timeout` | `float` | `30.0` | Request timeout in seconds. |
|
|
165
|
+
| `user_agent` | `str` | `"exolix-client/0.1.0"` | User-Agent header. |
|
|
166
|
+
|
|
167
|
+
#### Methods
|
|
168
|
+
|
|
169
|
+
- **`get_currencies(...)`** — `GET /currencies`
|
|
170
|
+
- **`get_currency_networks(code)`** — `GET /currencies/{code}/networks`
|
|
171
|
+
- **`get_networks(...)`** — `GET /currencies/networks`
|
|
172
|
+
- **`get_rate(...)`** — `GET /rate` (`rate_type` `float` or `fixed`, `amount` or `withdrawal_amount`)
|
|
173
|
+
- **`create_transaction(...)`** — `POST /transactions`
|
|
174
|
+
- **`get_transaction(transaction_id)`** — `GET /transactions/{id}`
|
|
175
|
+
- **`get_transactions(...)`** — `GET /transactions` (authenticated history)
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Error Handling
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from exolix import Exolix, ExolixError
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
with Exolix(api_key="invalid_key") as client:
|
|
186
|
+
client.get_transactions()
|
|
187
|
+
except ExolixError as error:
|
|
188
|
+
print(f"{error.code}: {error.message} (HTTP {error.status_code})")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Live Tests
|
|
194
|
+
|
|
195
|
+
Unit tests mock HTTP and always run. The live suite calls the real API and runs only when `EXOLIX_API_KEY` is set. It is read-only: currencies, networks, a floating rate, a fixed rate, and transaction history. It never creates a transaction.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv run pytest -m "not live"
|
|
199
|
+
EXOLIX_API_KEY=your_key uv run pytest -m live
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
ISC
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
exolix/__init__.py,sha256=_Ih3H6gLxTzRxfBlSn6siMIooicgxGPS-bhWKl4uh5s,539
|
|
2
|
+
exolix/client.py,sha256=_jwlJ_HXltbYbmIV0qr_Vc5_pQBlIwTzO0-Oas6lph8,12346
|
|
3
|
+
exolix/errors.py,sha256=cDe2uBKoCCBpIeQBk6gDA9UpucvNtiFv1OmP4V6LOq0,626
|
|
4
|
+
exolix/types.py,sha256=nvAZe2HkPb17LS1Vefq7aIMUvC1utJbYLibDJFTmITE,2018
|
|
5
|
+
exolix_client-0.1.0.dist-info/METADATA,sha256=H3NccXFjsCZ_jAiUtj8HKidzTjbbWJNA-weWPrE8bnY,5678
|
|
6
|
+
exolix_client-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
7
|
+
exolix_client-0.1.0.dist-info/licenses/LICENSE,sha256=8S0uYmn_aeljwFudfBoV91HcIo79yab9s0yhMVsOHJ0,751
|
|
8
|
+
exolix_client-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 api-kit contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|