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 +53 -0
- hotstuff/apis/__init__.py +10 -0
- hotstuff/apis/exchange.py +510 -0
- hotstuff/apis/info.py +291 -0
- hotstuff/apis/subscription.py +278 -0
- hotstuff/methods/__init__.py +10 -0
- hotstuff/methods/exchange/__init__.py +14 -0
- hotstuff/methods/exchange/account.py +120 -0
- hotstuff/methods/exchange/collateral.py +94 -0
- hotstuff/methods/exchange/op_codes.py +28 -0
- hotstuff/methods/exchange/trading.py +117 -0
- hotstuff/methods/exchange/vault.py +59 -0
- hotstuff/methods/info/__init__.py +14 -0
- hotstuff/methods/info/account.py +371 -0
- hotstuff/methods/info/explorer.py +57 -0
- hotstuff/methods/info/global.py +249 -0
- hotstuff/methods/info/vault.py +86 -0
- hotstuff/methods/subscription/__init__.py +8 -0
- hotstuff/methods/subscription/global.py +200 -0
- hotstuff/transports/__init__.py +9 -0
- hotstuff/transports/http.py +142 -0
- hotstuff/transports/websocket.py +401 -0
- hotstuff/types/__init__.py +62 -0
- hotstuff/types/clients.py +34 -0
- hotstuff/types/exchange.py +88 -0
- hotstuff/types/transports.py +110 -0
- hotstuff/utils/__init__.py +13 -0
- hotstuff/utils/address.py +37 -0
- hotstuff/utils/endpoints.py +15 -0
- hotstuff/utils/nonce.py +25 -0
- hotstuff/utils/signing.py +76 -0
- hotstuff_python_sdk-0.0.1b1.dist-info/LICENSE +21 -0
- hotstuff_python_sdk-0.0.1b1.dist-info/METADATA +985 -0
- hotstuff_python_sdk-0.0.1b1.dist-info/RECORD +35 -0
- hotstuff_python_sdk-0.0.1b1.dist-info/WHEEL +4 -0
|
@@ -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
|
+
[](https://pypi.org/project/hotstuff-python-sdk/)
|
|
34
|
+
[](https://opensource.org/licenses/MIT)
|
|
35
|
+
[](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
|
+
|