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.
@@ -0,0 +1,985 @@
1
+ Metadata-Version: 2.3
2
+ Name: hotstuff-python-sdk
3
+ Version: 0.0.1b1
4
+ Summary: Python SDK for Hotstuff Labs decentralized exchange
5
+ License: MIT
6
+ Keywords: hotstuff,trading,blockchain,defi,exchange
7
+ Author: hotstuff
8
+ Requires-Python: >=3.8,<4.0
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Dist: aiohttp (>=3.9.0,<4.0.0)
21
+ Requires-Dist: eth-account (>=0.11.0,<0.12.0)
22
+ Requires-Dist: eth-utils (>=4.0.0,<5.0.0)
23
+ Requires-Dist: msgpack (>=1.0.0,<2.0.0)
24
+ Requires-Dist: pydantic (>=2.0.0,<3.0.0)
25
+ Requires-Dist: web3 (>=6.0.0,<7.0.0)
26
+ Requires-Dist: websockets (>=12.0,<13.0)
27
+ Project-URL: Homepage, https://github.com/hotstuff-labs/python-sdk
28
+ Project-URL: Repository, https://github.com/hotstuff-labs/python-sdk
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Hotstuff Python SDK
32
+
33
+ [![PyPI version](https://img.shields.io/pypi/v/hotstuff-python-sdk.svg)](https://pypi.org/project/hotstuff-python-sdk/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
35
+ [![Python Versions](https://img.shields.io/pypi/pyversions/hotstuff-python-sdk.svg)](https://pypi.org/project/hotstuff-python-sdk/)
36
+
37
+ > Python SDK for interacting with Hotstuff Labs decentralized exchange
38
+
39
+ ## Table of Contents
40
+
41
+ - [Installation](#installation)
42
+ - [Quick Start](#quick-start)
43
+ - [API Clients](#api-clients)
44
+ - [InfoClient](#infoclient)
45
+ - [ExchangeClient](#exchangeclient)
46
+ - [SubscriptionClient](#subscriptionclient)
47
+ - [Transports](#transports)
48
+ - [HttpTransport](#httptransport)
49
+ - [WebSocketTransport](#websockettransport)
50
+ - [Advanced Usage](#advanced-usage)
51
+ - [Error Handling](#error-handling)
52
+ - [Examples](#examples)
53
+
54
+ ## Installation
55
+
56
+ ### Using pip
57
+
58
+ ```bash
59
+ pip install hotstuff-python-sdk
60
+ ```
61
+
62
+ ### Using Poetry
63
+
64
+ ```bash
65
+ poetry add hotstuff-python-sdk
66
+ ```
67
+
68
+ ### Install from source
69
+
70
+ ```bash
71
+ git clone https://github.com/hotstuff-labs/python-sdk.git
72
+ cd python-sdk
73
+
74
+ # Using Poetry (recommended)
75
+ poetry install
76
+
77
+ # Or using pip
78
+ pip install -e .
79
+ ```
80
+
81
+ ## Quick Start
82
+
83
+ ```python
84
+ import asyncio
85
+ from hotstuff import (
86
+ HttpTransport,
87
+ WebSocketTransport,
88
+ InfoClient,
89
+ ExchangeClient,
90
+ SubscriptionClient,
91
+ HttpTransportOptions,
92
+ WebSocketTransportOptions,
93
+ )
94
+
95
+ async def main():
96
+ # Create transports
97
+ http_transport = HttpTransport(
98
+ HttpTransportOptions(is_testnet=True)
99
+ )
100
+
101
+ ws_transport = WebSocketTransport(
102
+ WebSocketTransportOptions(is_testnet=True)
103
+ )
104
+
105
+ # Query market data (read-only)
106
+ info = InfoClient(transport=http_transport)
107
+ ticker = await info.ticker({"symbol": "BTC-PERP"})
108
+ print(f"Current BTC-PERP ticker: {ticker}")
109
+
110
+ # Subscribe to real-time updates
111
+ subscriptions = SubscriptionClient(transport=ws_transport)
112
+
113
+ def handle_ticker(data):
114
+ print(f"Live ticker: {data.data}")
115
+
116
+ sub = await subscriptions.ticker(
117
+ {"symbol": "BTC-PERP"},
118
+ handle_ticker
119
+ )
120
+
121
+ # Keep running for a bit
122
+ await asyncio.sleep(10)
123
+
124
+ # Unsubscribe
125
+ await sub["unsubscribe"]()
126
+
127
+ # Clean up
128
+ await http_transport.close()
129
+ await ws_transport.disconnect()
130
+
131
+ if __name__ == "__main__":
132
+ asyncio.run(main())
133
+ ```
134
+
135
+ ## API Clients
136
+
137
+ ### InfoClient
138
+
139
+ Query market data, account information, vault details, and blockchain explorer data.
140
+
141
+ #### Creating an InfoClient
142
+
143
+ ```python
144
+ from hotstuff import HttpTransport, InfoClient, HttpTransportOptions
145
+
146
+ async def setup():
147
+ transport = HttpTransport(HttpTransportOptions(is_testnet=True))
148
+ info = InfoClient(transport=transport)
149
+ return info
150
+ ```
151
+
152
+ #### Market Data Methods
153
+
154
+ ```python
155
+ # Get all instruments (perps, spot, options)
156
+ instruments = await info.instruments({"type": "all"})
157
+
158
+ # Get supported collateral
159
+ collateral = await info.supported_collateral({})
160
+
161
+ # Get oracle prices
162
+ oracle = await info.oracle({})
163
+
164
+ # Get ticker for a specific symbol
165
+ ticker = await info.ticker({"symbol": "BTC-PERP"})
166
+
167
+ # Get orderbook with depth
168
+ orderbook = await info.orderbook({"symbol": "BTC-PERP", "depth": 20})
169
+
170
+ # Get recent trades
171
+ trades = await info.trades({"symbol": "BTC-PERP", "limit": 50})
172
+
173
+ # Get mid prices for all instruments
174
+ mids = await info.mids({})
175
+
176
+ # Get best bid/offer
177
+ bbo = await info.bbo({"symbol": "BTC-PERP"})
178
+
179
+ # Get chart data (candles or funding)
180
+ chart = await info.chart({
181
+ "symbol": "BTC-PERP",
182
+ "resolution": "1h",
183
+ "chart_type": "candles",
184
+ })
185
+ ```
186
+
187
+ #### Account Methods
188
+
189
+ ```python
190
+ user_address = "0x1234..."
191
+
192
+ # Get account summary
193
+ summary = await info.account_summary({"user": user_address})
194
+
195
+ # Get account info
196
+ account_info = await info.account_info({"user": user_address})
197
+
198
+ # Get user balance
199
+ balance = await info.user_balance({"user": user_address})
200
+
201
+ # Get open orders
202
+ open_orders = await info.open_orders({"user": user_address})
203
+
204
+ # Get current positions
205
+ positions = await info.positions({"user": user_address})
206
+
207
+ # Get order history
208
+ order_history = await info.order_history({
209
+ "user": user_address,
210
+ "limit": 100,
211
+ })
212
+
213
+ # Get trade history (fills)
214
+ trade_history = await info.trade_history({"user": user_address})
215
+
216
+ # Get funding history
217
+ funding_history = await info.funding_history({"user": user_address})
218
+
219
+ # Get transfer history
220
+ transfer_history = await info.transfer_history({"user": user_address})
221
+
222
+ # Get account history with time range
223
+ account_history = await info.account_history({
224
+ "user": user_address,
225
+ "from": int(time.time()) - 86400, # 24h ago
226
+ "to": int(time.time()),
227
+ })
228
+
229
+ # Get user fee information
230
+ fee_info = await info.user_fee_info({"user": user_address})
231
+
232
+ # Get instrument leverage settings
233
+ leverage = await info.instrument_leverage({
234
+ "user": user_address,
235
+ "instrumentId": 1,
236
+ })
237
+
238
+ # Get referral info
239
+ referral_info = await info.get_referral_info({"user": user_address})
240
+
241
+ # Get referral summary
242
+ referral_summary = await info.referral_summary({"user": user_address})
243
+
244
+ # Get sub-accounts list
245
+ sub_accounts = await info.sub_accounts_list({"user": user_address})
246
+
247
+ # Get agents
248
+ agents = await info.agents({"user": user_address})
249
+ ```
250
+
251
+ #### Vault Methods
252
+
253
+ ```python
254
+ # Get all vaults
255
+ vaults = await info.vaults({})
256
+
257
+ # Get sub-vaults for a specific vault
258
+ sub_vaults = await info.sub_vaults({"vaultId": 1})
259
+
260
+ # Get vault balances
261
+ vault_balances = await info.vault_balances({"vaultId": 1})
262
+ ```
263
+
264
+ #### Explorer Methods
265
+
266
+ ```python
267
+ # Get recent blocks
268
+ blocks = await info.blocks({"limit": 10})
269
+
270
+ # Get specific block details
271
+ block_details = await info.block_details({"blockNumber": 12345})
272
+
273
+ # Get recent transactions
274
+ transactions = await info.transactions({"limit": 20})
275
+
276
+ # Get specific transaction details
277
+ tx_details = await info.transaction_details({"txHash": "0xabc..."})
278
+ ```
279
+
280
+ ---
281
+
282
+ ### ExchangeClient
283
+
284
+ Execute signed trading actions and account management operations.
285
+
286
+ #### Creating an ExchangeClient
287
+
288
+ ```python
289
+ from hotstuff import HttpTransport, ExchangeClient, HttpTransportOptions
290
+ from eth_account import Account
291
+
292
+ async def setup():
293
+ transport = HttpTransport(HttpTransportOptions(is_testnet=True))
294
+
295
+ # Create account from private key
296
+ account = Account.from_key("0xYOUR_PRIVATE_KEY")
297
+
298
+ exchange = ExchangeClient(
299
+ transport=transport,
300
+ wallet=account
301
+ )
302
+ return exchange
303
+ ```
304
+
305
+ #### Trading Methods
306
+
307
+ ```python
308
+ import time
309
+
310
+ # Place order(s)
311
+ await exchange.place_order({
312
+ "orders": [
313
+ {
314
+ "instrumentId": 1,
315
+ "side": "b", # 'b' for buy, 's' for sell
316
+ "positionSide": "LONG", # 'LONG', 'SHORT', or 'BOTH'
317
+ "price": "50000.00",
318
+ "size": "0.1",
319
+ "tif": "GTC", # 'GTC', 'IOC', or 'FOK'
320
+ "ro": False, # reduce-only
321
+ "po": False, # post-only
322
+ "cloid": "my-order-123", # client order ID
323
+ "triggerPx": "51000.00", # optional trigger price
324
+ "isMarket": False, # optional market order flag
325
+ "tpsl": "", # optional: 'tp', 'sl', or ''
326
+ "grouping": "normal", # optional: 'position', 'normal', or ''
327
+ },
328
+ ],
329
+ "brokerConfig": { # optional broker configuration
330
+ "broker": "0x0000000000000000000000000000000000000000",
331
+ "fee": "0.001",
332
+ },
333
+ "expiresAfter": int(time.time()) + 3600, # 1 hour from now
334
+ })
335
+
336
+ # Cancel order by order ID
337
+ await exchange.cancel_by_oid({
338
+ "cancels": [
339
+ {"oid": 123456, "instrumentId": 1},
340
+ {"oid": 123457, "instrumentId": 1},
341
+ ],
342
+ "expiresAfter": int(time.time()) + 3600,
343
+ })
344
+
345
+ # Cancel order by client order ID
346
+ await exchange.cancel_by_cloid({
347
+ "cancels": [{"cloid": "my-order-123", "instrumentId": 1}],
348
+ "expiresAfter": int(time.time()) + 3600,
349
+ })
350
+
351
+ # Cancel all orders
352
+ await exchange.cancel_all({
353
+ "expiresAfter": int(time.time()) + 3600,
354
+ })
355
+ ```
356
+
357
+ #### Account Management
358
+
359
+ ```python
360
+ # Add an agent (requires agent private key)
361
+ await exchange.add_agent({
362
+ "agent_name": "my-trading-bot",
363
+ "agent": "0xagent...",
364
+ "for_account": "",
365
+ "agent_private_key": "0xprivatekey...",
366
+ "signer": "0xsigner...",
367
+ "valid_until": int(time.time()) + 86400, # 24 hours
368
+ })
369
+
370
+ # Revoke an agent
371
+ await exchange.revoke_agent({
372
+ "agent": "0xagent...",
373
+ "for_account": "", # optional: sub-account address
374
+ })
375
+
376
+ # Update leverage for a perpetual instrument
377
+ await exchange.update_perp_instrument_leverage({
378
+ "instrument_id": 1,
379
+ "leverage": 10, # 10x leverage
380
+ })
381
+
382
+ # Approve broker fee
383
+ await exchange.approve_broker_fee({
384
+ "broker": "0xbroker...",
385
+ "max_fee_rate": "0.001", # 0.1% max fee
386
+ })
387
+
388
+ # Create a referral code
389
+ await exchange.create_referral_code({
390
+ "code": "MY_REFERRAL_CODE",
391
+ })
392
+
393
+ # Set referrer using a referral code
394
+ await exchange.set_referrer({
395
+ "code": "FRIEND_REFERRAL_CODE",
396
+ })
397
+
398
+ # Claim referral rewards
399
+ await exchange.claim_referral_rewards({
400
+ "collateral_id": 1,
401
+ "spot": True, # True for spot account, False for derivatives
402
+ })
403
+ ```
404
+
405
+ #### Collateral Transfer Methods
406
+
407
+ ```python
408
+ # Request spot collateral withdrawal to external chain
409
+ await exchange.account_spot_withdraw_request({
410
+ "collateral_id": 1,
411
+ "amount": "100.0",
412
+ "chain_id": 1, # Ethereum mainnet
413
+ })
414
+
415
+ # Request derivative collateral withdrawal to external chain
416
+ await exchange.account_derivative_withdraw_request({
417
+ "collateral_id": 1,
418
+ "amount": "100.0",
419
+ "chain_id": 1,
420
+ })
421
+
422
+ # Transfer spot balance to another address on Hotstuff
423
+ await exchange.account_spot_balance_transfer_request({
424
+ "collateral_id": 1,
425
+ "amount": "50.0",
426
+ "destination": "0xrecipient...",
427
+ })
428
+
429
+ # Transfer derivative balance to another address on Hotstuff
430
+ await exchange.account_derivative_balance_transfer_request({
431
+ "collateral_id": 1,
432
+ "amount": "50.0",
433
+ "destination": "0xrecipient...",
434
+ })
435
+
436
+ # Transfer balance between spot and derivatives accounts
437
+ await exchange.account_internal_balance_transfer_request({
438
+ "collateral_id": 1,
439
+ "amount": "25.0",
440
+ "to_derivatives_account": True, # True: spot -> derivatives, False: derivatives -> spot
441
+ })
442
+ ```
443
+
444
+ #### Vault Methods
445
+
446
+ ```python
447
+ # Deposit to a vault
448
+ await exchange.deposit_to_vault({
449
+ "vault_address": "0xvault...",
450
+ "amount": "1000.0",
451
+ })
452
+
453
+ # Redeem shares from a vault
454
+ await exchange.redeem_from_vault({
455
+ "vault_address": "0xvault...",
456
+ "shares": "500.0",
457
+ })
458
+ ```
459
+
460
+ ---
461
+
462
+ ### SubscriptionClient
463
+
464
+ Subscribe to real-time data streams via WebSocket.
465
+
466
+ #### Creating a SubscriptionClient
467
+
468
+ ```python
469
+ from hotstuff import WebSocketTransport, SubscriptionClient, WebSocketTransportOptions
470
+
471
+ async def setup():
472
+ transport = WebSocketTransport(WebSocketTransportOptions(is_testnet=True))
473
+ subscriptions = SubscriptionClient(transport=transport)
474
+ return subscriptions
475
+ ```
476
+
477
+ #### Market Subscriptions
478
+
479
+ ```python
480
+ # Subscribe to ticker updates
481
+ def handle_ticker(data):
482
+ print(f"Ticker: {data.data}")
483
+
484
+ ticker_sub = await subscriptions.ticker(
485
+ {"symbol": "BTC-PERP"},
486
+ handle_ticker
487
+ )
488
+
489
+ # Subscribe to mid prices
490
+ mids_sub = await subscriptions.mids(
491
+ {"symbol": "BTC-PERP"},
492
+ lambda data: print(f"Mids: {data.data}")
493
+ )
494
+
495
+ # Subscribe to best bid/offer
496
+ bbo_sub = await subscriptions.bbo(
497
+ {"symbol": "BTC-PERP"},
498
+ lambda data: print(f"BBO: {data.data}")
499
+ )
500
+
501
+ # Subscribe to orderbook updates
502
+ orderbook_sub = await subscriptions.orderbook(
503
+ {"symbol": "BTC-PERP"},
504
+ lambda data: print(f"Orderbook: {data.data}")
505
+ )
506
+
507
+ # Subscribe to trades
508
+ trade_sub = await subscriptions.trade(
509
+ {"symbol": "BTC-PERP"},
510
+ lambda data: print(f"Trade: {data.data}")
511
+ )
512
+
513
+ # Subscribe to index prices
514
+ index_sub = await subscriptions.index(
515
+ lambda data: print(f"Index: {data.data}")
516
+ )
517
+
518
+ # Subscribe to chart updates
519
+ chart_sub = await subscriptions.chart(
520
+ {
521
+ "symbol": "BTC-PERP",
522
+ "chart_type": "candles",
523
+ "resolution": "1m",
524
+ },
525
+ lambda data: print(f"Chart: {data.data}")
526
+ )
527
+ ```
528
+
529
+ #### Account Subscriptions
530
+
531
+ ```python
532
+ user_address = "0x1234..."
533
+
534
+ # Subscribe to order updates
535
+ order_sub = await subscriptions.account_order_updates(
536
+ {"address": user_address},
537
+ lambda data: print(f"Order update: {data.data}")
538
+ )
539
+
540
+ # Subscribe to balance updates
541
+ balance_sub = await subscriptions.account_balance_updates(
542
+ {"address": user_address},
543
+ lambda data: print(f"Balance update: {data.data}")
544
+ )
545
+
546
+ # Subscribe to position updates
547
+ position_sub = await subscriptions.positions(
548
+ {"address": user_address},
549
+ lambda data: print(f"Position update: {data.data}")
550
+ )
551
+
552
+ # Subscribe to fills
553
+ fills_sub = await subscriptions.fills(
554
+ {"address": user_address},
555
+ lambda data: print(f"Fill: {data.data}")
556
+ )
557
+
558
+ # Subscribe to account summary
559
+ account_summary_sub = await subscriptions.account_summary(
560
+ {"user": user_address},
561
+ lambda data: print(f"Account summary: {data.data}")
562
+ )
563
+ ```
564
+
565
+ #### Explorer Subscriptions
566
+
567
+ ```python
568
+ # Subscribe to new blocks
569
+ blocks_sub = await subscriptions.blocks(
570
+ {},
571
+ lambda data: print(f"New block: {data.data}")
572
+ )
573
+
574
+ # Subscribe to new transactions
575
+ tx_sub = await subscriptions.transactions(
576
+ {},
577
+ lambda data: print(f"New transaction: {data.data}")
578
+ )
579
+ ```
580
+
581
+ #### Unsubscribing
582
+
583
+ All subscription methods return a dictionary with an `unsubscribe` function:
584
+
585
+ ```python
586
+ sub = await subscriptions.ticker(
587
+ {"symbol": "BTC-PERP"},
588
+ handle_ticker
589
+ )
590
+
591
+ # Later...
592
+ await sub["unsubscribe"]()
593
+ ```
594
+
595
+ ---
596
+
597
+ ## Transports
598
+
599
+ ### HttpTransport
600
+
601
+ HTTP transport for making API requests to the Hotstuff Labs API.
602
+
603
+ #### Configuration
604
+
605
+ ```python
606
+ from hotstuff import HttpTransport, HttpTransportOptions
607
+
608
+ transport = HttpTransport(
609
+ HttpTransportOptions(
610
+ # Use testnet or mainnet (default: False = mainnet)
611
+ is_testnet=True,
612
+
613
+ # Request timeout in seconds (default: 3.0, set None to disable)
614
+ timeout=5.0,
615
+
616
+ # Custom server endpoints
617
+ server={
618
+ "mainnet": {
619
+ "api": "https://api.hotstuff.trade/",
620
+ "rpc": "https://rpc.hotstuff.trade/",
621
+ },
622
+ "testnet": {
623
+ "api": "https://testnet-api.hotstuff.trade/",
624
+ "rpc": "https://testnet-api.hotstuff.trade/",
625
+ },
626
+ },
627
+
628
+ # Additional headers
629
+ headers={
630
+ "X-Custom-Header": "value",
631
+ },
632
+ )
633
+ )
634
+ ```
635
+
636
+ #### Default Endpoints
637
+
638
+ - **Mainnet:** `https://testnet-api.hotstuff.trade/`
639
+ - **Testnet:** `https://testnet-api.hotstuff.trade/`
640
+
641
+ ---
642
+
643
+ ### WebSocketTransport
644
+
645
+ WebSocket transport for real-time subscriptions using JSON-RPC 2.0.
646
+
647
+ #### Configuration
648
+
649
+ ```python
650
+ from hotstuff import WebSocketTransport, WebSocketTransportOptions
651
+
652
+ transport = WebSocketTransport(
653
+ WebSocketTransportOptions(
654
+ # Use testnet or mainnet (default: False = mainnet)
655
+ is_testnet=True,
656
+
657
+ # Request timeout in seconds (default: 10.0)
658
+ timeout=15.0,
659
+
660
+ # Custom server endpoints
661
+ server={
662
+ "mainnet": "wss://api.hotstuff.trade/ws/",
663
+ "testnet": "wss://testnet-api.hotstuff.trade/ws/",
664
+ },
665
+
666
+ # Keep-alive ping configuration
667
+ keep_alive={
668
+ "interval": 30.0, # ping every 30 seconds
669
+ "timeout": 10.0, # timeout after 10 seconds
670
+ },
671
+
672
+ # Auto-connect on creation (default: True)
673
+ auto_connect=True,
674
+ )
675
+ )
676
+ ```
677
+
678
+ #### Connection Management
679
+
680
+ ```python
681
+ # Manually connect (if auto_connect is False)
682
+ await transport.connect()
683
+
684
+ # Check connection status
685
+ if transport.is_connected():
686
+ print("Connected!")
687
+
688
+ # Manually disconnect
689
+ await transport.disconnect()
690
+
691
+ # Send ping
692
+ pong = await transport.ping()
693
+ ```
694
+
695
+ #### Reconnection
696
+
697
+ The WebSocket transport automatically reconnects with exponential backoff:
698
+
699
+ - Maximum attempts: 5
700
+ - Initial delay: 1 second
701
+ - Delay multiplier: attempt number
702
+
703
+ #### Default Endpoints
704
+
705
+ - **Mainnet:** `wss://testnet-api.hotstuff.trade/ws/`
706
+ - **Testnet:** `wss://testnet-api.hotstuff.trade/ws/`
707
+
708
+ ---
709
+
710
+ ## Advanced Usage
711
+
712
+ ### Using Context Managers
713
+
714
+ Both transports support async context managers for automatic cleanup:
715
+
716
+ ```python
717
+ async with HttpTransport(HttpTransportOptions(is_testnet=True)) as transport:
718
+ info = InfoClient(transport=transport)
719
+ ticker = await info.ticker({"symbol": "BTC-PERP"})
720
+ print(ticker)
721
+ # Transport is automatically closed
722
+
723
+ async with WebSocketTransport(WebSocketTransportOptions(is_testnet=True)) as transport:
724
+ subscriptions = SubscriptionClient(transport=transport)
725
+ # Use subscriptions...
726
+ # Transport is automatically disconnected
727
+ ```
728
+
729
+ ### Managing Multiple Subscriptions
730
+
731
+ ```python
732
+ subscriptions = SubscriptionClient(transport=ws_transport)
733
+ active_subs = []
734
+
735
+ # Subscribe to multiple channels
736
+ symbols = ["BTC-PERP", "ETH-PERP", "SOL-PERP"]
737
+ for symbol in symbols:
738
+ sub = await subscriptions.ticker(
739
+ {"symbol": symbol},
740
+ lambda data: print(f"{symbol}: {data.data}")
741
+ )
742
+ active_subs.append(sub)
743
+
744
+ # Unsubscribe from all
745
+ for sub in active_subs:
746
+ await sub["unsubscribe"]()
747
+ ```
748
+
749
+ ### Environment-Specific Configuration
750
+
751
+ ```python
752
+ import os
753
+
754
+ is_production = os.getenv("ENV") == "production"
755
+
756
+ http_transport = HttpTransport(
757
+ HttpTransportOptions(
758
+ is_testnet=not is_production,
759
+ timeout=5.0 if is_production else 10.0,
760
+ )
761
+ )
762
+
763
+ ws_transport = WebSocketTransport(
764
+ WebSocketTransportOptions(
765
+ is_testnet=not is_production,
766
+ keep_alive={
767
+ "interval": 30.0 if is_production else 60.0,
768
+ "timeout": 10.0,
769
+ },
770
+ )
771
+ )
772
+ ```
773
+
774
+ ---
775
+
776
+ ## Error Handling
777
+
778
+ ### HTTP Errors
779
+
780
+ HTTP transport raises exceptions with descriptive messages from the server:
781
+
782
+ ```python
783
+ try:
784
+ await exchange.place_order({
785
+ # ... order params
786
+ })
787
+ except Exception as e:
788
+ print(f"Failed to place order: {e}")
789
+ ```
790
+
791
+ ### WebSocket Errors
792
+
793
+ WebSocket subscriptions can fail during subscribe:
794
+
795
+ ```python
796
+ try:
797
+ sub = await subscriptions.ticker(
798
+ {"symbol": "BTC-PERP"},
799
+ handle_ticker
800
+ )
801
+ except Exception as e:
802
+ print(f"Subscription failed: {e}")
803
+ ```
804
+
805
+ ---
806
+
807
+ ## Examples
808
+
809
+ ### Complete Trading Bot Example
810
+
811
+ ```python
812
+ import asyncio
813
+ import time
814
+ from hotstuff import (
815
+ HttpTransport,
816
+ WebSocketTransport,
817
+ InfoClient,
818
+ ExchangeClient,
819
+ SubscriptionClient,
820
+ HttpTransportOptions,
821
+ WebSocketTransportOptions,
822
+ )
823
+ from eth_account import Account
824
+
825
+ async def main():
826
+ # Setup
827
+ http_transport = HttpTransport(HttpTransportOptions(is_testnet=True))
828
+ ws_transport = WebSocketTransport(WebSocketTransportOptions(is_testnet=True))
829
+
830
+ account = Account.from_key("0xYOUR_PRIVATE_KEY")
831
+
832
+ info = InfoClient(transport=http_transport)
833
+ exchange = ExchangeClient(transport=http_transport, wallet=account)
834
+ subscriptions = SubscriptionClient(transport=ws_transport)
835
+
836
+ # Get current market data
837
+ ticker = await info.ticker({"symbol": "BTC-PERP"})
838
+ print(f"Current price: {ticker}")
839
+
840
+ # Subscribe to live updates
841
+ async def handle_ticker(data):
842
+ price = data.data.get("last")
843
+ print(f"Live price: {price}")
844
+
845
+ # Simple trading logic
846
+ if price and price < 50000:
847
+ try:
848
+ await exchange.place_order({
849
+ "orders": [{
850
+ "instrumentId": 1,
851
+ "side": "b",
852
+ "positionSide": "LONG",
853
+ "price": str(price),
854
+ "size": "0.1",
855
+ "tif": "GTC",
856
+ "ro": False,
857
+ "po": False,
858
+ "cloid": f"order-{int(time.time())}",
859
+ }],
860
+ "expiresAfter": int(time.time()) + 3600,
861
+ })
862
+ print("Order placed!")
863
+ except Exception as e:
864
+ print(f"Order failed: {e}")
865
+
866
+ ticker_sub = await subscriptions.ticker(
867
+ {"symbol": "BTC-PERP"},
868
+ handle_ticker
869
+ )
870
+
871
+ # Run for 1 hour then cleanup
872
+ await asyncio.sleep(3600)
873
+ await ticker_sub["unsubscribe"]()
874
+ await http_transport.close()
875
+ await ws_transport.disconnect()
876
+
877
+ if __name__ == "__main__":
878
+ asyncio.run(main())
879
+ ```
880
+
881
+ ### Broker Fee with Agent Trading Example
882
+
883
+ This example demonstrates the full flow of approving a broker fee from the main account, creating an agent, and placing orders through the agent with broker configuration.
884
+
885
+ ```python
886
+ import asyncio
887
+ import time
888
+ import os
889
+ from hotstuff import (
890
+ HttpTransport,
891
+ ExchangeClient,
892
+ HttpTransportOptions,
893
+ )
894
+ from eth_account import Account
895
+ from hotstuff.methods.exchange.account import (
896
+ AddAgentParams,
897
+ ApproveBrokerFeeParams,
898
+ )
899
+ from hotstuff.methods.exchange.trading import (
900
+ PlaceOrderParams,
901
+ UnitOrder,
902
+ BrokerConfig,
903
+ )
904
+
905
+
906
+ async def broker_agent_trading_example():
907
+ transport = HttpTransport(HttpTransportOptions(is_testnet=True))
908
+
909
+ # Main account setup (the account that will approve broker fees and create agent)
910
+ main_account = Account.from_key(os.getenv("MAIN_PRIVATE_KEY"))
911
+ main_exchange = ExchangeClient(transport=transport, wallet=main_account)
912
+
913
+ # Broker address that will receive fees
914
+ broker_address = "0xBrokerAddress..."
915
+
916
+ # Step 1: Approve broker fee from main account
917
+ print("Approving broker fee...")
918
+ await main_exchange.approve_broker_fee(
919
+ ApproveBrokerFeeParams(
920
+ broker=broker_address,
921
+ max_fee_rate="0.001", # 0.1% max fee rate
922
+ )
923
+ )
924
+ print("Broker fee approved!")
925
+
926
+ # Step 2: Generate agent credentials and add agent
927
+ agent_account = Account.create()
928
+ agent_private_key = agent_account.key.hex()
929
+
930
+ print("Adding agent...")
931
+ await main_exchange.add_agent(
932
+ AddAgentParams(
933
+ agent_name="broker-trading-agent",
934
+ agent=agent_account.address,
935
+ for_account="",
936
+ agent_private_key=agent_private_key,
937
+ signer=main_account.address,
938
+ valid_until=int(time.time() * 1000) + 86400000 * 30, # Valid for 30 days
939
+ )
940
+ )
941
+ print(f"Agent added: {agent_account.address}")
942
+
943
+ # Step 3: Create exchange client for the agent
944
+ agent_exchange = ExchangeClient(transport=transport, wallet=agent_account)
945
+
946
+ # Step 4: Place order from agent with broker config
947
+ print("Placing order with broker fee...")
948
+ await agent_exchange.place_order(
949
+ PlaceOrderParams(
950
+ orders=[
951
+ UnitOrder(
952
+ instrument_id=1,
953
+ side="b",
954
+ position_side="BOTH",
955
+ price="50000.00",
956
+ size="0.1",
957
+ tif="GTC",
958
+ ro=False,
959
+ po=False,
960
+ cloid=f"broker-order-{int(time.time())}",
961
+ trigger_px=None,
962
+ is_market=False,
963
+ tpsl="",
964
+ grouping="",
965
+ )
966
+ ],
967
+ broker_config=BrokerConfig(
968
+ broker=broker_address,
969
+ fee="0.0005", # 0.05% fee (must be <= approved maxFeeRate)
970
+ ),
971
+ expires_after=int(time.time() * 1000) + 3600000,
972
+ )
973
+ )
974
+ print("Order placed with broker fee!")
975
+
976
+ # Optional: Revoke agent when done
977
+ # await main_exchange.revoke_agent(RevokeAgentParams(agent=agent_account.address))
978
+
979
+ await transport.close()
980
+
981
+
982
+ if __name__ == "__main__":
983
+ asyncio.run(broker_agent_trading_example())
984
+ ```
985
+