hotstuff-python-sdk 0.0.1b1__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.
hotstuff/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """Hotstuff Python SDK.
2
+
3
+ A Python SDK for interacting with Hotstuff Labs decentralized exchange.
4
+ """
5
+
6
+ __version__ = "0.0.1-beta.1"
7
+
8
+ from hotstuff.transports import HttpTransport, WebSocketTransport
9
+ from hotstuff.apis import InfoClient, ExchangeClient, SubscriptionClient
10
+ from hotstuff.types import (
11
+ HttpTransportOptions,
12
+ WebSocketTransportOptions,
13
+ )
14
+ from hotstuff.utils import NonceManager, sign_action, EXCHANGE_OP_CODES
15
+
16
+ # Export method types for convenience
17
+ from hotstuff.methods.exchange.trading import (
18
+ UnitOrder,
19
+ BrokerConfig,
20
+ PlaceOrderParams,
21
+ CancelByOidParams,
22
+ CancelByCloidParams,
23
+ CancelAllParams,
24
+ )
25
+ from hotstuff.methods.exchange.account import AddAgentParams
26
+
27
+ __all__ = [
28
+ # Version
29
+ "__version__",
30
+ # Transports
31
+ "HttpTransport",
32
+ "WebSocketTransport",
33
+ # Clients
34
+ "InfoClient",
35
+ "ExchangeClient",
36
+ "SubscriptionClient",
37
+ # Transport Types
38
+ "HttpTransportOptions",
39
+ "WebSocketTransportOptions",
40
+ # Exchange Method Types (for backward compatibility)
41
+ "UnitOrder",
42
+ "BrokerConfig",
43
+ "PlaceOrderParams",
44
+ "CancelByOidParams",
45
+ "CancelByCloidParams",
46
+ "CancelAllParams",
47
+ "AddAgentParams",
48
+ # Utils
49
+ "NonceManager",
50
+ "sign_action",
51
+ "EXCHANGE_OP_CODES",
52
+ ]
53
+
@@ -0,0 +1,10 @@
1
+ """Clients package."""
2
+ from hotstuff.apis.info import InfoClient
3
+ from hotstuff.apis.exchange import ExchangeClient
4
+ from hotstuff.apis.subscription import SubscriptionClient
5
+
6
+ __all__ = [
7
+ "InfoClient",
8
+ "ExchangeClient",
9
+ "SubscriptionClient",
10
+ ]
@@ -0,0 +1,510 @@
1
+ """Exchange API client for trading operations."""
2
+ from typing import Optional, Any, Dict, Callable, Awaitable
3
+ from eth_account import Account
4
+
5
+ from hotstuff.utils import sign_action, NonceManager
6
+ from hotstuff.methods.exchange import (
7
+ trading as TM,
8
+ account as AM,
9
+ collateral as CM,
10
+ vault as VM,
11
+ )
12
+ from hotstuff.methods.exchange.op_codes import EXCHANGE_OP_CODES
13
+
14
+
15
+ class ExchangeClient:
16
+ """Client for executing trading actions and account management."""
17
+
18
+ def __init__(
19
+ self,
20
+ transport,
21
+ wallet: Account,
22
+ nonce: Optional[Callable[[], Awaitable[int]]] = None
23
+ ):
24
+ """
25
+ Initialize ExchangeClient.
26
+
27
+ Args:
28
+ transport: The transport layer to use
29
+ wallet: The wallet/account for signing
30
+ nonce: Optional nonce generator function
31
+ """
32
+ self.transport = transport
33
+ self.wallet = wallet
34
+ self.nonce = nonce or NonceManager().get_nonce
35
+
36
+ # Account Actions
37
+
38
+ async def add_agent(
39
+ self,
40
+ params: AM.AddAgentParams,
41
+ execute: bool = True,
42
+ signal: Optional[Any] = None
43
+ ) -> Dict[str, Any]:
44
+ """
45
+ Add an agent.
46
+
47
+ Args:
48
+ params: Agent parameters
49
+ execute: Whether to execute the action
50
+ signal: Optional abort signal
51
+
52
+ Returns:
53
+ Response from the server
54
+ """
55
+ nonce = await self.nonce()
56
+
57
+ # Create agent account from private key
58
+ agent_account = Account.from_key(params.agent_private_key)
59
+
60
+ # Sign with agent account
61
+ agent_signature = await sign_action(
62
+ wallet=agent_account,
63
+ action={
64
+ "signer": params.signer,
65
+ "nonce": nonce,
66
+ },
67
+ tx_type=EXCHANGE_OP_CODES["addAgent"],
68
+ is_testnet=self.transport.is_testnet,
69
+ )
70
+
71
+ # Prepare params for API
72
+ params_dict = {
73
+ "agentName": params.agent_name,
74
+ "agent": params.agent,
75
+ "forAccount": params.for_account if params.for_account else "",
76
+ "signature": agent_signature,
77
+ "validUntil": params.valid_until,
78
+ "nonce": nonce,
79
+ }
80
+
81
+ return await self._execute_action(
82
+ {"action": "addAgent", "params": params_dict},
83
+ signal,
84
+ execute
85
+ )
86
+
87
+ async def revoke_agent(
88
+ self,
89
+ params: AM.RevokeAgentParams,
90
+ signal: Optional[Any] = None
91
+ ) -> Dict[str, Any]:
92
+ """
93
+ Revoke an agent.
94
+
95
+ Args:
96
+ params: Revoke agent parameters
97
+ signal: Optional abort signal
98
+
99
+ Returns:
100
+ Response from the server
101
+ """
102
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
103
+ return await self._execute_action(
104
+ {"action": "revokeAgent", "params": params_dict},
105
+ signal
106
+ )
107
+
108
+ async def update_perp_instrument_leverage(
109
+ self,
110
+ params: AM.UpdatePerpInstrumentLeverageParams,
111
+ signal: Optional[Any] = None
112
+ ) -> Dict[str, Any]:
113
+ """
114
+ Update perp instrument leverage.
115
+
116
+ Args:
117
+ params: Update leverage parameters
118
+ signal: Optional abort signal
119
+
120
+ Returns:
121
+ Response from the server
122
+ """
123
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
124
+ return await self._execute_action(
125
+ {"action": "updatePerpInstrumentLeverage", "params": params_dict},
126
+ signal
127
+ )
128
+
129
+ async def approve_broker_fee(
130
+ self,
131
+ params: AM.ApproveBrokerFeeParams,
132
+ signal: Optional[Any] = None
133
+ ) -> Dict[str, Any]:
134
+ """
135
+ Approve broker fee.
136
+
137
+ Args:
138
+ params: Approve broker fee parameters
139
+ signal: Optional abort signal
140
+
141
+ Returns:
142
+ Response from the server
143
+ """
144
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
145
+ return await self._execute_action(
146
+ {"action": "approveBrokerFee", "params": params_dict},
147
+ signal
148
+ )
149
+
150
+ async def create_referral_code(
151
+ self,
152
+ params: AM.CreateReferralCodeParams,
153
+ signal: Optional[Any] = None
154
+ ) -> Dict[str, Any]:
155
+ """
156
+ Create a referral code.
157
+
158
+ Args:
159
+ params: Create referral code parameters
160
+ signal: Optional abort signal
161
+
162
+ Returns:
163
+ Response from the server
164
+ """
165
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
166
+ return await self._execute_action(
167
+ {"action": "createReferralCode", "params": params_dict},
168
+ signal
169
+ )
170
+
171
+ async def set_referrer(
172
+ self,
173
+ params: AM.SetReferrerParams,
174
+ signal: Optional[Any] = None
175
+ ) -> Dict[str, Any]:
176
+ """
177
+ Set a referrer.
178
+
179
+ Args:
180
+ params: Set referrer parameters
181
+ signal: Optional abort signal
182
+
183
+ Returns:
184
+ Response from the server
185
+ """
186
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
187
+ return await self._execute_action(
188
+ {"action": "setReferrer", "params": params_dict},
189
+ signal
190
+ )
191
+
192
+ async def claim_referral_rewards(
193
+ self,
194
+ params: AM.ClaimReferralRewardsParams,
195
+ signal: Optional[Any] = None
196
+ ) -> Dict[str, Any]:
197
+ """
198
+ Claim referral rewards.
199
+
200
+ Args:
201
+ params: Claim referral rewards parameters
202
+ signal: Optional abort signal
203
+
204
+ Returns:
205
+ Response from the server
206
+ """
207
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
208
+ return await self._execute_action(
209
+ {"action": "claimReferralRewards", "params": params_dict},
210
+ signal
211
+ )
212
+
213
+ # Trading Actions
214
+
215
+ async def place_order(
216
+ self,
217
+ params: TM.PlaceOrderParams,
218
+ signal: Optional[Any] = None
219
+ ) -> Dict[str, Any]:
220
+ """
221
+ Place order(s).
222
+
223
+ Args:
224
+ params: Order parameters
225
+ signal: Optional abort signal
226
+
227
+ Returns:
228
+ Response from the server
229
+ """
230
+ # Convert to dict with proper aliases, ensuring nested models are converted
231
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
232
+ # Reorder dict to match original SDK order for consistent msgpack encoding
233
+ ordered_params = {
234
+ "orders": params_dict["orders"],
235
+ "brokerConfig": params_dict.get("brokerConfig"),
236
+ "expiresAfter": params_dict["expiresAfter"],
237
+ }
238
+ # Remove None brokerConfig if not provided
239
+ if ordered_params["brokerConfig"] is None:
240
+ ordered_params.pop("brokerConfig")
241
+ return await self._execute_action(
242
+ {"action": "placeOrder", "params": ordered_params},
243
+ signal
244
+ )
245
+
246
+ async def cancel_by_oid(
247
+ self,
248
+ params: TM.CancelByOidParams,
249
+ signal: Optional[Any] = None
250
+ ) -> Dict[str, Any]:
251
+ """
252
+ Cancel order by order ID.
253
+
254
+ Args:
255
+ params: Cancel parameters
256
+ signal: Optional abort signal
257
+
258
+ Returns:
259
+ Response from the server
260
+ """
261
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
262
+ return await self._execute_action(
263
+ {"action": "cancelByOid", "params": params_dict},
264
+ signal
265
+ )
266
+
267
+ async def cancel_by_cloid(
268
+ self,
269
+ params: TM.CancelByCloidParams,
270
+ signal: Optional[Any] = None
271
+ ) -> Dict[str, Any]:
272
+ """
273
+ Cancel order by client order ID.
274
+
275
+ Args:
276
+ params: Cancel parameters
277
+ signal: Optional abort signal
278
+
279
+ Returns:
280
+ Response from the server
281
+ """
282
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
283
+ return await self._execute_action(
284
+ {"action": "cancelByCloid", "params": params_dict},
285
+ signal
286
+ )
287
+
288
+ async def cancel_all(
289
+ self,
290
+ params: TM.CancelAllParams,
291
+ signal: Optional[Any] = None
292
+ ) -> Dict[str, Any]:
293
+ """
294
+ Cancel all orders.
295
+
296
+ Args:
297
+ params: Cancel parameters
298
+ signal: Optional abort signal
299
+
300
+ Returns:
301
+ Response from the server
302
+ """
303
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
304
+ return await self._execute_action(
305
+ {"action": "cancelAll", "params": params_dict},
306
+ signal
307
+ )
308
+
309
+ # Collateral Transfer Methods
310
+
311
+ async def account_spot_withdraw_request(
312
+ self,
313
+ params: CM.AccountSpotWithdrawRequestParams,
314
+ signal: Optional[Any] = None
315
+ ) -> Dict[str, Any]:
316
+ """
317
+ Request spot account withdrawal.
318
+
319
+ Args:
320
+ params: Spot withdraw request parameters
321
+ signal: Optional abort signal
322
+
323
+ Returns:
324
+ Response from the server
325
+ """
326
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
327
+ return await self._execute_action(
328
+ {"action": "accountSpotWithdrawRequest", "params": params_dict},
329
+ signal
330
+ )
331
+
332
+ async def account_derivative_withdraw_request(
333
+ self,
334
+ params: CM.AccountDerivativeWithdrawRequestParams,
335
+ signal: Optional[Any] = None
336
+ ) -> Dict[str, Any]:
337
+ """
338
+ Request derivative account withdrawal.
339
+
340
+ Args:
341
+ params: Derivative withdraw request parameters
342
+ signal: Optional abort signal
343
+
344
+ Returns:
345
+ Response from the server
346
+ """
347
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
348
+ return await self._execute_action(
349
+ {"action": "accountDerivativeWithdrawRequest", "params": params_dict},
350
+ signal
351
+ )
352
+
353
+ async def account_spot_balance_transfer_request(
354
+ self,
355
+ params: CM.AccountSpotBalanceTransferRequestParams,
356
+ signal: Optional[Any] = None
357
+ ) -> Dict[str, Any]:
358
+ """
359
+ Request spot balance transfer.
360
+
361
+ Args:
362
+ params: Spot balance transfer request parameters
363
+ signal: Optional abort signal
364
+
365
+ Returns:
366
+ Response from the server
367
+ """
368
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
369
+ return await self._execute_action(
370
+ {"action": "accountSpotBalanceTransferRequest", "params": params_dict},
371
+ signal
372
+ )
373
+
374
+ async def account_derivative_balance_transfer_request(
375
+ self,
376
+ params: CM.AccountDerivativeBalanceTransferRequestParams,
377
+ signal: Optional[Any] = None
378
+ ) -> Dict[str, Any]:
379
+ """
380
+ Request derivative balance transfer.
381
+
382
+ Args:
383
+ params: Derivative balance transfer request parameters
384
+ signal: Optional abort signal
385
+
386
+ Returns:
387
+ Response from the server
388
+ """
389
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
390
+ return await self._execute_action(
391
+ {"action": "accountDerivativeBalanceTransferRequest", "params": params_dict},
392
+ signal
393
+ )
394
+
395
+ async def account_internal_balance_transfer_request(
396
+ self,
397
+ params: CM.AccountInternalBalanceTransferRequestParams,
398
+ signal: Optional[Any] = None
399
+ ) -> Dict[str, Any]:
400
+ """
401
+ Request internal balance transfer between spot and derivative accounts.
402
+
403
+ Args:
404
+ params: Internal balance transfer request parameters
405
+ signal: Optional abort signal
406
+
407
+ Returns:
408
+ Response from the server
409
+ """
410
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
411
+ return await self._execute_action(
412
+ {"action": "accountInternalBalanceTransferRequest", "params": params_dict},
413
+ signal
414
+ )
415
+
416
+ # Vault Methods
417
+
418
+ async def deposit_to_vault(
419
+ self,
420
+ params: VM.DepositToVaultParams,
421
+ signal: Optional[Any] = None
422
+ ) -> Dict[str, Any]:
423
+ """
424
+ Deposit to a vault.
425
+
426
+ Args:
427
+ params: Deposit to vault parameters
428
+ signal: Optional abort signal
429
+
430
+ Returns:
431
+ Response from the server
432
+ """
433
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
434
+ return await self._execute_action(
435
+ {"action": "depositToVault", "params": params_dict},
436
+ signal
437
+ )
438
+
439
+ async def redeem_from_vault(
440
+ self,
441
+ params: VM.RedeemFromVaultParams,
442
+ signal: Optional[Any] = None
443
+ ) -> Dict[str, Any]:
444
+ """
445
+ Redeem from a vault.
446
+
447
+ Args:
448
+ params: Redeem from vault parameters
449
+ signal: Optional abort signal
450
+
451
+ Returns:
452
+ Response from the server
453
+ """
454
+ params_dict = params.model_dump(by_alias=True, exclude={"nonce"}, mode='python')
455
+ return await self._execute_action(
456
+ {"action": "redeemFromVault", "params": params_dict},
457
+ signal
458
+ )
459
+
460
+ # Private Methods
461
+
462
+ async def _execute_action(
463
+ self,
464
+ request: Dict[str, Any],
465
+ signal: Optional[Any] = None,
466
+ execute: bool = True
467
+ ) -> Dict[str, Any]:
468
+ """
469
+ Execute an action.
470
+
471
+ Args:
472
+ request: Action request
473
+ signal: Optional abort signal
474
+ execute: Whether to execute the action
475
+
476
+ Returns:
477
+ Response or signature data
478
+ """
479
+ action = request["action"]
480
+ params = request["params"]
481
+
482
+ # Set nonce if not present
483
+ if "nonce" not in params or params["nonce"] is None:
484
+ params["nonce"] = await self.nonce()
485
+
486
+ # Sign the action
487
+ signature = await sign_action(
488
+ wallet=self.wallet,
489
+ action=params,
490
+ tx_type=EXCHANGE_OP_CODES[action],
491
+ is_testnet=True,
492
+ )
493
+
494
+ if execute:
495
+ # Send to server
496
+ response = await self.transport.request(
497
+ "exchange",
498
+ {
499
+ "action": {
500
+ "data": params,
501
+ "type": str(EXCHANGE_OP_CODES[action]),
502
+ },
503
+ "signature": signature,
504
+ "nonce": params["nonce"],
505
+ },
506
+ signal,
507
+ )
508
+ return response
509
+
510
+ return {"params": params, "signature": signature}