fixedfloat-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.
- fixedfloat/__init__.py +34 -0
- fixedfloat/client.py +421 -0
- fixedfloat/errors.py +23 -0
- fixedfloat/types.py +105 -0
- fixedfloat_client-0.1.0.dist-info/METADATA +218 -0
- fixedfloat_client-0.1.0.dist-info/RECORD +8 -0
- fixedfloat_client-0.1.0.dist-info/WHEEL +4 -0
- fixedfloat_client-0.1.0.dist-info/licenses/LICENSE +21 -0
fixedfloat/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""FixedFloat API v2 client library."""
|
|
2
|
+
|
|
3
|
+
from .client import DEFAULT_REFCODE, FixedFloat
|
|
4
|
+
from .errors import FixedFloatError
|
|
5
|
+
from .types import (
|
|
6
|
+
Currency,
|
|
7
|
+
EmergencyChoice,
|
|
8
|
+
ExchangeDirection,
|
|
9
|
+
Order,
|
|
10
|
+
OrderSide,
|
|
11
|
+
OrderTime,
|
|
12
|
+
OrderTx,
|
|
13
|
+
OrderType,
|
|
14
|
+
PriceResult,
|
|
15
|
+
PriceSide,
|
|
16
|
+
QrResult,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DEFAULT_REFCODE",
|
|
21
|
+
"Currency",
|
|
22
|
+
"EmergencyChoice",
|
|
23
|
+
"ExchangeDirection",
|
|
24
|
+
"FixedFloat",
|
|
25
|
+
"FixedFloatError",
|
|
26
|
+
"Order",
|
|
27
|
+
"OrderSide",
|
|
28
|
+
"OrderTime",
|
|
29
|
+
"OrderTx",
|
|
30
|
+
"OrderType",
|
|
31
|
+
"PriceResult",
|
|
32
|
+
"PriceSide",
|
|
33
|
+
"QrResult",
|
|
34
|
+
]
|
fixedfloat/client.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""FixedFloat API v2 client implementation."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .errors import FixedFloatError
|
|
11
|
+
from .types import (
|
|
12
|
+
Currency,
|
|
13
|
+
EmergencyChoice,
|
|
14
|
+
ExchangeDirection,
|
|
15
|
+
Order,
|
|
16
|
+
OrderSide,
|
|
17
|
+
OrderTime,
|
|
18
|
+
OrderTx,
|
|
19
|
+
OrderType,
|
|
20
|
+
PriceResult,
|
|
21
|
+
PriceSide,
|
|
22
|
+
QrResult,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Internal service identifier
|
|
26
|
+
DEFAULT_REFCODE: str = "gfc1h2fb"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FixedFloat:
|
|
30
|
+
"""Client for the FixedFloat cryptocurrency exchange REST API v2.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
api_key: Your FixedFloat API key.
|
|
34
|
+
api_secret: Your FixedFloat API secret for HMAC-SHA256 signatures.
|
|
35
|
+
base_url: Base URL for FixedFloat API. Defaults to 'https://ff.io/api/v2'.
|
|
36
|
+
timeout: Request timeout in seconds. Defaults to 15.0.
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
>>> from fixedfloat import FixedFloat
|
|
40
|
+
>>> client = FixedFloat(api_key="key", api_secret="secret")
|
|
41
|
+
>>> currencies = client.get_currencies()
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
api_key: str | None = None,
|
|
47
|
+
api_secret: str | None = None,
|
|
48
|
+
base_url: str = "https://ff.io/api/v2",
|
|
49
|
+
timeout: float = 15.0,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.api_key = api_key
|
|
52
|
+
self.api_secret = api_secret
|
|
53
|
+
self.base_url = base_url.rstrip("/")
|
|
54
|
+
self.timeout = timeout
|
|
55
|
+
self._client = httpx.Client(
|
|
56
|
+
base_url=self.base_url,
|
|
57
|
+
timeout=self.timeout,
|
|
58
|
+
headers={
|
|
59
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
60
|
+
"Accept": "application/json",
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def close(self) -> None:
|
|
65
|
+
"""Close the underlying HTTP client."""
|
|
66
|
+
self._client.close()
|
|
67
|
+
|
|
68
|
+
def __enter__(self) -> "FixedFloat":
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
72
|
+
self.close()
|
|
73
|
+
|
|
74
|
+
def _request(
|
|
75
|
+
self,
|
|
76
|
+
endpoint: str,
|
|
77
|
+
payload: dict[str, Any] | None = None,
|
|
78
|
+
) -> Any:
|
|
79
|
+
body = payload or {}
|
|
80
|
+
json_str = json.dumps(body, separators=(",", ":"))
|
|
81
|
+
headers: dict[str, str] = {}
|
|
82
|
+
|
|
83
|
+
if self.api_key:
|
|
84
|
+
headers["X-API-KEY"] = self.api_key
|
|
85
|
+
|
|
86
|
+
if self.api_secret:
|
|
87
|
+
signature = hmac.new(
|
|
88
|
+
self.api_secret.encode("utf-8"),
|
|
89
|
+
json_str.encode("utf-8"),
|
|
90
|
+
hashlib.sha256,
|
|
91
|
+
).hexdigest()
|
|
92
|
+
headers["X-API-SIGN"] = signature
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
response = self._client.post(
|
|
96
|
+
endpoint,
|
|
97
|
+
content=json_str.encode("utf-8"),
|
|
98
|
+
headers=headers,
|
|
99
|
+
)
|
|
100
|
+
except httpx.HTTPError as exc:
|
|
101
|
+
raise FixedFloatError(f"HTTP request failed: {exc}") from exc
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
data = response.json()
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
raise FixedFloatError(
|
|
107
|
+
f"Failed to parse response JSON: {response.text}",
|
|
108
|
+
code=response.status_code,
|
|
109
|
+
) from exc
|
|
110
|
+
|
|
111
|
+
if isinstance(data, dict) and "code" in data:
|
|
112
|
+
code = data.get("code", 0)
|
|
113
|
+
if code != 0:
|
|
114
|
+
msg = data.get("msg", "FixedFloat API error")
|
|
115
|
+
raise FixedFloatError(msg, code=code, data=data.get("data"))
|
|
116
|
+
return data.get("data")
|
|
117
|
+
|
|
118
|
+
return data
|
|
119
|
+
|
|
120
|
+
def get_currencies(self) -> list[Currency]:
|
|
121
|
+
"""Retrieve list of available cryptocurrencies and blockchain networks.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of Currency objects supported by FixedFloat.
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
>>> currencies = client.get_currencies()
|
|
128
|
+
>>> print(currencies[0].code)
|
|
129
|
+
'BTC'
|
|
130
|
+
"""
|
|
131
|
+
raw_list = self._request("/ccies", {})
|
|
132
|
+
result: list[Currency] = []
|
|
133
|
+
if isinstance(raw_list, list):
|
|
134
|
+
for item in raw_list:
|
|
135
|
+
result.append(
|
|
136
|
+
Currency(
|
|
137
|
+
code=item.get("code", ""),
|
|
138
|
+
coin=item.get("coin", ""),
|
|
139
|
+
network=item.get("network", ""),
|
|
140
|
+
name=item.get("name", ""),
|
|
141
|
+
recv=int(item.get("recv", 0)),
|
|
142
|
+
send=int(item.get("send", 0)),
|
|
143
|
+
tag=item.get("tag"),
|
|
144
|
+
logo=item.get("logo", ""),
|
|
145
|
+
color=item.get("color", ""),
|
|
146
|
+
priority=int(item.get("priority", 0)),
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
def get_price(
|
|
152
|
+
self,
|
|
153
|
+
from_ccy: str,
|
|
154
|
+
to_ccy: str,
|
|
155
|
+
amount: float,
|
|
156
|
+
direction: ExchangeDirection = "from",
|
|
157
|
+
order_type: OrderType = "float",
|
|
158
|
+
afftax: float | None = None,
|
|
159
|
+
) -> PriceResult:
|
|
160
|
+
"""Calculate exchange price estimate for a currency pair.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
from_ccy: Source cryptocurrency code (e.g. 'BTC').
|
|
164
|
+
to_ccy: Destination cryptocurrency code (e.g. 'ETH').
|
|
165
|
+
amount: Exchange amount.
|
|
166
|
+
direction: Direction of exchange ('from' or 'to').
|
|
167
|
+
order_type: Rate type ('fixed' or 'float').
|
|
168
|
+
afftax: Optional affiliate tax percentage.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
PriceResult containing from and to conversion amounts and limits.
|
|
172
|
+
|
|
173
|
+
Examples:
|
|
174
|
+
>>> price = client.get_price(
|
|
175
|
+
... from_ccy="BTC", to_ccy="ETH", amount=0.1, direction="from", order_type="float"
|
|
176
|
+
... )
|
|
177
|
+
>>> print(price.to_side.amount)
|
|
178
|
+
"""
|
|
179
|
+
payload: dict[str, Any] = {
|
|
180
|
+
"fromCcy": from_ccy,
|
|
181
|
+
"toCcy": to_ccy,
|
|
182
|
+
"amount": amount,
|
|
183
|
+
"direction": direction,
|
|
184
|
+
"type": order_type,
|
|
185
|
+
"refcode": DEFAULT_REFCODE,
|
|
186
|
+
}
|
|
187
|
+
if afftax is not None:
|
|
188
|
+
payload["afftax"] = afftax
|
|
189
|
+
|
|
190
|
+
data = self._request("/price", payload)
|
|
191
|
+
from_data = data.get("from", {})
|
|
192
|
+
to_data = data.get("to", {})
|
|
193
|
+
|
|
194
|
+
return PriceResult(
|
|
195
|
+
from_side=PriceSide(
|
|
196
|
+
code=from_data.get("code", ""),
|
|
197
|
+
network=from_data.get("network", ""),
|
|
198
|
+
coin=from_data.get("coin", ""),
|
|
199
|
+
amount=str(from_data.get("amount", "")),
|
|
200
|
+
rate=str(from_data.get("rate", "")),
|
|
201
|
+
min=str(from_data.get("min", "")),
|
|
202
|
+
max=str(from_data.get("max", "")),
|
|
203
|
+
),
|
|
204
|
+
to_side=PriceSide(
|
|
205
|
+
code=to_data.get("code", ""),
|
|
206
|
+
network=to_data.get("network", ""),
|
|
207
|
+
coin=to_data.get("coin", ""),
|
|
208
|
+
amount=str(to_data.get("amount", "")),
|
|
209
|
+
rate=str(to_data.get("rate", "")),
|
|
210
|
+
min=str(to_data.get("min", "")),
|
|
211
|
+
max=str(to_data.get("max", "")),
|
|
212
|
+
),
|
|
213
|
+
errors=data.get("errors"),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def create_order(
|
|
217
|
+
self,
|
|
218
|
+
from_ccy: str,
|
|
219
|
+
to_ccy: str,
|
|
220
|
+
amount: float,
|
|
221
|
+
to_address: str,
|
|
222
|
+
direction: ExchangeDirection = "from",
|
|
223
|
+
order_type: OrderType = "float",
|
|
224
|
+
extra_id: str | None = None,
|
|
225
|
+
refund_address: str | None = None,
|
|
226
|
+
refund_extra_id: str | None = None,
|
|
227
|
+
afftax: float | None = None,
|
|
228
|
+
) -> Order:
|
|
229
|
+
"""Create a new cryptocurrency exchange order.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
from_ccy: Source cryptocurrency code (e.g. 'BTC').
|
|
233
|
+
to_ccy: Destination cryptocurrency code (e.g. 'ETH').
|
|
234
|
+
amount: Amount to exchange.
|
|
235
|
+
to_address: Destination wallet address to receive funds.
|
|
236
|
+
direction: Direction of exchange ('from' or 'to').
|
|
237
|
+
order_type: Rate type ('fixed' or 'float').
|
|
238
|
+
extra_id: Optional destination tag or memo.
|
|
239
|
+
refund_address: Optional refund address.
|
|
240
|
+
refund_extra_id: Optional refund tag or memo.
|
|
241
|
+
afftax: Optional affiliate tax percentage.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Order object containing order ID and secret security token.
|
|
245
|
+
|
|
246
|
+
Examples:
|
|
247
|
+
>>> order = client.create_order(
|
|
248
|
+
... from_ccy="BTC",
|
|
249
|
+
... to_ccy="ETH",
|
|
250
|
+
... amount=0.1,
|
|
251
|
+
... to_address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
|
|
252
|
+
... )
|
|
253
|
+
>>> print(order.id, order.token)
|
|
254
|
+
"""
|
|
255
|
+
payload: dict[str, Any] = {
|
|
256
|
+
"fromCcy": from_ccy,
|
|
257
|
+
"toCcy": to_ccy,
|
|
258
|
+
"amount": amount,
|
|
259
|
+
"direction": direction,
|
|
260
|
+
"type": order_type,
|
|
261
|
+
"toAddress": to_address,
|
|
262
|
+
"refcode": DEFAULT_REFCODE,
|
|
263
|
+
}
|
|
264
|
+
if extra_id is not None:
|
|
265
|
+
payload["extraId"] = extra_id
|
|
266
|
+
if refund_address is not None:
|
|
267
|
+
payload["refundAddress"] = refund_address
|
|
268
|
+
if refund_extra_id is not None:
|
|
269
|
+
payload["refundExtraId"] = refund_extra_id
|
|
270
|
+
if afftax is not None:
|
|
271
|
+
payload["afftax"] = afftax
|
|
272
|
+
|
|
273
|
+
data = self._request("/create", payload)
|
|
274
|
+
return self._parse_order(data)
|
|
275
|
+
|
|
276
|
+
def get_order(self, id: str, token: str) -> Order:
|
|
277
|
+
"""Retrieve the status and details of an exchange order.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
id: Unique order ID.
|
|
281
|
+
token: Order security token.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Order details.
|
|
285
|
+
|
|
286
|
+
Examples:
|
|
287
|
+
>>> order = client.get_order("ORD123", "TOKEN_ABC")
|
|
288
|
+
>>> print(order.status)
|
|
289
|
+
"""
|
|
290
|
+
data = self._request("/order", {"id": id, "token": token})
|
|
291
|
+
return self._parse_order(data)
|
|
292
|
+
|
|
293
|
+
def set_emergency(
|
|
294
|
+
self,
|
|
295
|
+
id: str,
|
|
296
|
+
token: str,
|
|
297
|
+
choice: EmergencyChoice,
|
|
298
|
+
address: str | None = None,
|
|
299
|
+
extra_id: str | None = None,
|
|
300
|
+
) -> Any:
|
|
301
|
+
"""Set emergency resolution choice (EXCHANGE or REFUND) on an order.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
id: Unique order ID.
|
|
305
|
+
token: Order security token.
|
|
306
|
+
choice: Resolution action ('EXCHANGE' or 'REFUND').
|
|
307
|
+
address: Optional refund address.
|
|
308
|
+
extra_id: Optional refund destination tag or memo.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
API response data.
|
|
312
|
+
|
|
313
|
+
Examples:
|
|
314
|
+
>>> client.set_emergency("ORD123", "TOKEN_ABC", choice="REFUND")
|
|
315
|
+
"""
|
|
316
|
+
payload: dict[str, Any] = {
|
|
317
|
+
"id": id,
|
|
318
|
+
"token": token,
|
|
319
|
+
"choice": choice,
|
|
320
|
+
}
|
|
321
|
+
if address is not None:
|
|
322
|
+
payload["address"] = address
|
|
323
|
+
if extra_id is not None:
|
|
324
|
+
payload["extraId"] = extra_id
|
|
325
|
+
return self._request("/emergency", payload)
|
|
326
|
+
|
|
327
|
+
def set_email(self, id: str, token: str, email: str) -> Any:
|
|
328
|
+
"""Set notification email address on an active order.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
id: Unique order ID.
|
|
332
|
+
token: Order security token.
|
|
333
|
+
email: Email address.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
API response data.
|
|
337
|
+
|
|
338
|
+
Examples:
|
|
339
|
+
>>> client.set_email("ORD123", "TOKEN_ABC", "user@example.com")
|
|
340
|
+
"""
|
|
341
|
+
return self._request("/setEmail", {"id": id, "token": token, "email": email})
|
|
342
|
+
|
|
343
|
+
def get_qr(self, id: str, token: str) -> QrResult:
|
|
344
|
+
"""Retrieve QR code for order deposit address.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
id: Unique order ID.
|
|
348
|
+
token: Order security token.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
QrResult containing image source data.
|
|
352
|
+
|
|
353
|
+
Examples:
|
|
354
|
+
>>> qr = client.get_qr("ORD123", "TOKEN_ABC")
|
|
355
|
+
>>> print(qr.src)
|
|
356
|
+
"""
|
|
357
|
+
data = self._request("/qr", {"id": id, "token": token})
|
|
358
|
+
src = data.get("src", "") if isinstance(data, dict) else str(data)
|
|
359
|
+
return QrResult(src=src, raw=data if isinstance(data, dict) else None)
|
|
360
|
+
|
|
361
|
+
def _parse_order(self, data: dict[str, Any]) -> Order:
|
|
362
|
+
time_data = data.get("time", {})
|
|
363
|
+
from_data = data.get("from", {})
|
|
364
|
+
to_data = data.get("to", {})
|
|
365
|
+
|
|
366
|
+
from_tx = None
|
|
367
|
+
if "tx" in from_data and isinstance(from_data["tx"], dict):
|
|
368
|
+
tx_raw = from_data["tx"]
|
|
369
|
+
from_tx = OrderTx(
|
|
370
|
+
id=tx_raw.get("id"),
|
|
371
|
+
amount=str(tx_raw.get("amount", "")) if "amount" in tx_raw else None,
|
|
372
|
+
fee=str(tx_raw.get("fee", "")) if "fee" in tx_raw else None,
|
|
373
|
+
ccyfee=str(tx_raw.get("ccyfee", "")) if "ccyfee" in tx_raw else None,
|
|
374
|
+
time_reg=tx_raw.get("timeReg"),
|
|
375
|
+
confirmations=tx_raw.get("confirmations"),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
to_tx = None
|
|
379
|
+
if "tx" in to_data and isinstance(to_data["tx"], dict):
|
|
380
|
+
tx_raw = to_data["tx"]
|
|
381
|
+
to_tx = OrderTx(
|
|
382
|
+
id=tx_raw.get("id"),
|
|
383
|
+
amount=str(tx_raw.get("amount", "")) if "amount" in tx_raw else None,
|
|
384
|
+
fee=str(tx_raw.get("fee", "")) if "fee" in tx_raw else None,
|
|
385
|
+
ccyfee=str(tx_raw.get("ccyfee", "")) if "ccyfee" in tx_raw else None,
|
|
386
|
+
time_reg=tx_raw.get("timeReg"),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return Order(
|
|
390
|
+
id=data.get("id", ""),
|
|
391
|
+
type=data.get("type", "float"),
|
|
392
|
+
status=data.get("status", ""),
|
|
393
|
+
time=OrderTime(
|
|
394
|
+
reg=int(time_data.get("reg", 0)),
|
|
395
|
+
start=time_data.get("start"),
|
|
396
|
+
finish=time_data.get("finish"),
|
|
397
|
+
update=time_data.get("update"),
|
|
398
|
+
expiration=time_data.get("expiration"),
|
|
399
|
+
left=time_data.get("left"),
|
|
400
|
+
),
|
|
401
|
+
from_side=OrderSide(
|
|
402
|
+
code=from_data.get("code", ""),
|
|
403
|
+
network=from_data.get("network", ""),
|
|
404
|
+
coin=from_data.get("coin", ""),
|
|
405
|
+
amount=str(from_data.get("amount", "")),
|
|
406
|
+
address=from_data.get("address"),
|
|
407
|
+
extra_id=from_data.get("extraId"),
|
|
408
|
+
tx=from_tx,
|
|
409
|
+
),
|
|
410
|
+
to_side=OrderSide(
|
|
411
|
+
code=to_data.get("code", ""),
|
|
412
|
+
network=to_data.get("network", ""),
|
|
413
|
+
coin=to_data.get("coin", ""),
|
|
414
|
+
amount=str(to_data.get("amount", "")),
|
|
415
|
+
address=to_data.get("address"),
|
|
416
|
+
extra_id=to_data.get("extraId"),
|
|
417
|
+
tx=to_tx,
|
|
418
|
+
),
|
|
419
|
+
token=data.get("token", ""),
|
|
420
|
+
email=data.get("email"),
|
|
421
|
+
)
|
fixedfloat/errors.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Error classes for the FixedFloat API client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FixedFloatError(Exception):
|
|
7
|
+
"""Exception raised when a FixedFloat API request fails or returns an error."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
code: int = -1,
|
|
13
|
+
data: Any | None = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.message = message
|
|
17
|
+
self.code = code
|
|
18
|
+
self.data = data
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
if self.code != -1:
|
|
22
|
+
return f"[{self.code}] {self.message}"
|
|
23
|
+
return self.message
|
fixedfloat/types.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Data models and type definitions for FixedFloat API."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
OrderType = Literal["fixed", "float"]
|
|
7
|
+
ExchangeDirection = Literal["from", "to"]
|
|
8
|
+
EmergencyChoice = Literal["EXCHANGE", "REFUND"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Currency:
|
|
13
|
+
"""Represents a cryptocurrency supported by FixedFloat."""
|
|
14
|
+
|
|
15
|
+
code: str
|
|
16
|
+
coin: str
|
|
17
|
+
network: str
|
|
18
|
+
name: str
|
|
19
|
+
recv: int
|
|
20
|
+
send: int
|
|
21
|
+
tag: str | None = None
|
|
22
|
+
logo: str = ""
|
|
23
|
+
color: str = ""
|
|
24
|
+
priority: int = 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PriceSide:
|
|
29
|
+
"""Details of one side (from or to) in a price quote."""
|
|
30
|
+
|
|
31
|
+
code: str
|
|
32
|
+
network: str
|
|
33
|
+
coin: str
|
|
34
|
+
amount: str
|
|
35
|
+
rate: str
|
|
36
|
+
min: str
|
|
37
|
+
max: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class PriceResult:
|
|
42
|
+
"""Calculated exchange price result."""
|
|
43
|
+
|
|
44
|
+
from_side: PriceSide
|
|
45
|
+
to_side: PriceSide
|
|
46
|
+
errors: list[str] | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class OrderTx:
|
|
51
|
+
"""Transaction details for an order deposit or payout."""
|
|
52
|
+
|
|
53
|
+
id: str | None = None
|
|
54
|
+
amount: str | None = None
|
|
55
|
+
fee: str | None = None
|
|
56
|
+
ccyfee: str | None = None
|
|
57
|
+
time_reg: int | None = None
|
|
58
|
+
confirmations: int | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class OrderSide:
|
|
63
|
+
"""Deposit or receive side information in an order."""
|
|
64
|
+
|
|
65
|
+
code: str
|
|
66
|
+
network: str
|
|
67
|
+
coin: str
|
|
68
|
+
amount: str
|
|
69
|
+
address: str | None = None
|
|
70
|
+
extra_id: str | None = None
|
|
71
|
+
tx: OrderTx | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class OrderTime:
|
|
76
|
+
"""Timestamps associated with an order."""
|
|
77
|
+
|
|
78
|
+
reg: int
|
|
79
|
+
start: int | None = None
|
|
80
|
+
finish: int | None = None
|
|
81
|
+
update: int | None = None
|
|
82
|
+
expiration: int | None = None
|
|
83
|
+
left: int | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Order:
|
|
88
|
+
"""Exchange order details."""
|
|
89
|
+
|
|
90
|
+
id: str
|
|
91
|
+
type: str
|
|
92
|
+
status: str
|
|
93
|
+
time: OrderTime
|
|
94
|
+
from_side: OrderSide
|
|
95
|
+
to_side: OrderSide
|
|
96
|
+
token: str
|
|
97
|
+
email: str | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class QrResult:
|
|
102
|
+
"""QR code result for deposit address."""
|
|
103
|
+
|
|
104
|
+
src: str
|
|
105
|
+
raw: dict[str, Any] | None = None
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fixedfloat-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the FixedFloat cryptocurrency exchange API v2
|
|
5
|
+
Project-URL: Repository, https://github.com/TysikGG/api-kit
|
|
6
|
+
Project-URL: Documentation, https://fixedfloat.com/api
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# fixedfloat-client
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/fixedfloat-client/)
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
[](https://pypi.org/project/fixedfloat-client/)
|
|
18
|
+
|
|
19
|
+
Modern, fully typed Python client library for the [FixedFloat](https://fixedfloat.com/api) cryptocurrency exchange REST API v2.
|
|
20
|
+
|
|
21
|
+
Official API documentation: <https://fixedfloat.com/api>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- ⚡ **Fixed & Float Exchange Rates** — Choose between guaranteed fixed rates (1% fee) and floating market rates (0.5% fee).
|
|
28
|
+
- 🔐 **Automatic Request Signing** — Cryptographic HMAC-SHA256 signature generation (`X-API-KEY` & `X-API-SIGN`) built-in.
|
|
29
|
+
- 🪙 **Multi-Currency Support** — Trade Bitcoin, Ethereum, USDT, Solana, Monero, and dozens of other coins across multiple blockchains.
|
|
30
|
+
- 🐍 **Modern Pythonic Design** — Type annotations, dataclass models (`Order`, `PriceResult`, `Currency`), context manager support (`with FixedFloat(...) as client:`), and custom exceptions.
|
|
31
|
+
- 🚨 **Emergency Handling** — Support for emergency resolution actions (`EXCHANGE` or `REFUND`) on interrupted orders.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install fixedfloat-client
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.10 or higher.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick Start (Safe Check)
|
|
46
|
+
|
|
47
|
+
The quick check only inspects supported currencies and price quotes. It does not create orders or spend funds:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import os
|
|
51
|
+
from fixedfloat import FixedFloat
|
|
52
|
+
|
|
53
|
+
with FixedFloat(
|
|
54
|
+
api_key=os.environ.get("FIXEDFLOAT_API_KEY"),
|
|
55
|
+
api_secret=os.environ.get("FIXEDFLOAT_API_SECRET"),
|
|
56
|
+
) as client:
|
|
57
|
+
# 1. Fetch available currencies
|
|
58
|
+
currencies = client.get_currencies()
|
|
59
|
+
print(f"Available currencies: {len(currencies)}")
|
|
60
|
+
|
|
61
|
+
# 2. Query price calculation for 0.1 BTC to ETH
|
|
62
|
+
price = client.get_price(
|
|
63
|
+
from_ccy="BTC",
|
|
64
|
+
to_ccy="ETH",
|
|
65
|
+
amount=0.1,
|
|
66
|
+
direction="from",
|
|
67
|
+
order_type="float",
|
|
68
|
+
)
|
|
69
|
+
print(f"0.1 BTC -> {price.to_side.amount} ETH (Rate: {price.to_side.rate})")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Usage Guide
|
|
75
|
+
|
|
76
|
+
### 1. Creating a Floating Rate Order
|
|
77
|
+
|
|
78
|
+
Create an exchange order with a floating rate, deposit instructions, and receive destination address:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from fixedfloat import FixedFloat, FixedFloatError
|
|
82
|
+
|
|
83
|
+
with FixedFloat(
|
|
84
|
+
api_key="your_api_key",
|
|
85
|
+
api_secret="your_api_secret",
|
|
86
|
+
) as client:
|
|
87
|
+
# Create float order
|
|
88
|
+
order = client.create_order(
|
|
89
|
+
from_ccy="BTC",
|
|
90
|
+
to_ccy="ETH",
|
|
91
|
+
amount=0.1,
|
|
92
|
+
direction="from",
|
|
93
|
+
order_type="float",
|
|
94
|
+
to_address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
|
|
95
|
+
refund_address="bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", # optional
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
print(f"Order Created! ID: {order.id}")
|
|
99
|
+
print(f"Security Token: {order.token}")
|
|
100
|
+
print(f"Send BTC to: {order.from_side.address}")
|
|
101
|
+
print(f"Order Status: {order.status}")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. Creating a Fixed Rate Order
|
|
105
|
+
|
|
106
|
+
Lock in a guaranteed rate for 10 minutes:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
fixed_order = client.create_order(
|
|
110
|
+
from_ccy="ETH",
|
|
111
|
+
to_ccy="BTC",
|
|
112
|
+
amount=2.5,
|
|
113
|
+
direction="from",
|
|
114
|
+
order_type="fixed",
|
|
115
|
+
to_address="bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
print(f"Fixed Order ID: {fixed_order.id}")
|
|
119
|
+
print(f"Expected Payout: {fixed_order.to_side.amount} BTC")
|
|
120
|
+
print(f"Send ETH to: {fixed_order.from_side.address}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 3. Tracking Order Status & QR Codes
|
|
124
|
+
|
|
125
|
+
Check order execution, transaction confirmations, and fetch QR codes:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# Query status using order ID and secret token
|
|
129
|
+
current = client.get_order(order.id, order.token)
|
|
130
|
+
print(f"Status: {current.status}")
|
|
131
|
+
|
|
132
|
+
if current.from_side.tx:
|
|
133
|
+
print(f"Deposit confirmations: {current.from_side.tx.confirmations}")
|
|
134
|
+
|
|
135
|
+
# Fetch QR code for deposit address
|
|
136
|
+
qr = client.get_qr(order.id, order.token)
|
|
137
|
+
print(f"Deposit QR code: {qr.src}")
|
|
138
|
+
|
|
139
|
+
# Attach email address for status notifications
|
|
140
|
+
client.set_email(order.id, order.token, "user@example.com")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 4. Setting Emergency Action Choice
|
|
144
|
+
|
|
145
|
+
If an order requires user decision (e.g. rate shifted or deposit was delayed):
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# Choose to execute exchange at current market rate or refund
|
|
149
|
+
client.set_emergency(
|
|
150
|
+
id=order.id,
|
|
151
|
+
token=order.token,
|
|
152
|
+
choice="REFUND",
|
|
153
|
+
address="bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
|
|
154
|
+
)
|
|
155
|
+
print("Emergency action submitted.")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## API Reference
|
|
161
|
+
|
|
162
|
+
### `FixedFloat` Class
|
|
163
|
+
|
|
164
|
+
#### Constructor
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
FixedFloat(
|
|
168
|
+
api_key: Optional[str] = None,
|
|
169
|
+
api_secret: Optional[str] = None,
|
|
170
|
+
base_url: str = "https://ff.io/api/v2",
|
|
171
|
+
timeout: float = 15.0,
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Parameter | Type | Default | Description |
|
|
176
|
+
| :--- | :--- | :--- | :--- |
|
|
177
|
+
| `api_key` | `str` | `None` | Your FixedFloat API key. |
|
|
178
|
+
| `api_secret` | `str` | `None` | Your FixedFloat API secret for HMAC signatures. |
|
|
179
|
+
| `base_url` | `str` | `"https://ff.io/api/v2"` | FixedFloat API v2 base URL. |
|
|
180
|
+
| `timeout` | `float` | `15.0` | HTTP request timeout in seconds. |
|
|
181
|
+
|
|
182
|
+
#### Methods
|
|
183
|
+
|
|
184
|
+
- **`get_currencies() -> list[Currency]`** — Retrieves all supported cryptocurrencies and networks.
|
|
185
|
+
- **`get_price(from_ccy, to_ccy, amount, direction="from", order_type="float", ...) -> PriceResult`** — Price estimates, rates, limits.
|
|
186
|
+
- **`create_order(from_ccy, to_ccy, amount, to_address, direction="from", order_type="float", ...) -> Order`** — Creates a new order.
|
|
187
|
+
- **`get_order(id: str, token: str) -> Order`** — Retrieves order status and transaction info.
|
|
188
|
+
- **`set_emergency(id, token, choice, address=None, extra_id=None) -> Any`** — Sets emergency choice (`EXCHANGE` or `REFUND`).
|
|
189
|
+
- **`set_email(id, token, email) -> Any`** — Sets email address for order notifications.
|
|
190
|
+
- **`get_qr(id, token) -> QrResult`** — Retrieves deposit address QR code.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Error Handling
|
|
195
|
+
|
|
196
|
+
When the FixedFloat API returns an error or non-zero status code, a `FixedFloatError` is raised:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from fixedfloat import FixedFloat, FixedFloatError
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
with FixedFloat() as client:
|
|
203
|
+
client.get_order("INVALID_ID", "INVALID_TOKEN")
|
|
204
|
+
except FixedFloatError as exc:
|
|
205
|
+
print(f"FixedFloat Error: {exc.message} (Code: {exc.code})")
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Common error codes:
|
|
209
|
+
- `101` — Invalid currency pair or amount out of limits.
|
|
210
|
+
- `102` — Missing required parameter.
|
|
211
|
+
- `103` — Invalid destination wallet address.
|
|
212
|
+
- `401` — Unauthorized: invalid API key or signature.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
fixedfloat/__init__.py,sha256=N-nAMM1CAtsohYLgX8rslmW5J0RIVhCh3GWOfuxBRjY,595
|
|
2
|
+
fixedfloat/client.py,sha256=xAwxtdaPjP5jRWrx_yZQYPnnkEQV-FSkBakRvIXxn8E,14098
|
|
3
|
+
fixedfloat/errors.py,sha256=MDj4cetK4NxMTRu40FvjYS2jqWJutl7mVOY6knZlPw4,575
|
|
4
|
+
fixedfloat/types.py,sha256=wk292fZi9WtU7mDaUe5Ef6Qs8jET36H5lBAG7CN2uXY,1978
|
|
5
|
+
fixedfloat_client-0.1.0.dist-info/METADATA,sha256=B-wdh7JyKMClYFYjTh9EXOdvJtuO-zD5yDr2mQoXwkM,6771
|
|
6
|
+
fixedfloat_client-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
7
|
+
fixedfloat_client-0.1.0.dist-info/licenses/LICENSE,sha256=Vt9n-kHPUZpG7YZvx_-tz0yY6f8_FTZX1vrAHY4kevA,1064
|
|
8
|
+
fixedfloat_client-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TysikGG
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|