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,86 @@
|
|
|
1
|
+
"""Vault info method types."""
|
|
2
|
+
from typing import Optional, Annotated
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
|
4
|
+
from eth_utils import is_address, to_checksum_address
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_ethereum_address(value: str) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Validate and normalize an Ethereum address.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
value: The address string to validate
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Checksummed address string
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
ValueError: If the address is invalid
|
|
19
|
+
"""
|
|
20
|
+
if not isinstance(value, str):
|
|
21
|
+
raise ValueError(f"Address must be a string, got {type(value)}")
|
|
22
|
+
|
|
23
|
+
if not is_address(value):
|
|
24
|
+
raise ValueError(f"Invalid Ethereum address: {value}")
|
|
25
|
+
|
|
26
|
+
# Return checksummed address (EIP-55)
|
|
27
|
+
return to_checksum_address(value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Type alias for validated Ethereum addresses (similar to viem's Address type)
|
|
31
|
+
EthereumAddress = Annotated[
|
|
32
|
+
str,
|
|
33
|
+
Field(
|
|
34
|
+
...,
|
|
35
|
+
description="Ethereum address (validated and checksummed)",
|
|
36
|
+
examples=["0x1234567890123456789012345678901234567890"],
|
|
37
|
+
),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Vaults Method
|
|
42
|
+
class VaultsParams(BaseModel):
|
|
43
|
+
"""Parameters for vaults query."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class VaultsResponse(BaseModel):
|
|
48
|
+
"""Vaults response."""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Sub Vaults Method
|
|
53
|
+
class SubVaultsParams(BaseModel):
|
|
54
|
+
"""Parameters for sub vaults query."""
|
|
55
|
+
vault_address: EthereumAddress = Field(..., description="Vault address")
|
|
56
|
+
|
|
57
|
+
@field_validator('vault_address', mode='before')
|
|
58
|
+
@classmethod
|
|
59
|
+
def validate_vault_address(cls, v: str) -> str:
|
|
60
|
+
"""Validate and checksum the vault address."""
|
|
61
|
+
return validate_ethereum_address(v)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SubVaultsResponse(BaseModel):
|
|
65
|
+
"""Sub vaults response."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Vault Balances Method
|
|
70
|
+
class VaultBalancesParams(BaseModel):
|
|
71
|
+
"""Parameters for vault balances query."""
|
|
72
|
+
vault_address: EthereumAddress = Field(..., description="Vault address")
|
|
73
|
+
user: Optional[EthereumAddress] = Field(None, description="User address")
|
|
74
|
+
|
|
75
|
+
@field_validator('vault_address', 'user', mode='before')
|
|
76
|
+
@classmethod
|
|
77
|
+
def validate_addresses(cls, v: Optional[str]) -> Optional[str]:
|
|
78
|
+
"""Validate and checksum Ethereum addresses."""
|
|
79
|
+
if v is None:
|
|
80
|
+
return None
|
|
81
|
+
return validate_ethereum_address(v)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class VaultBalancesResponse(BaseModel):
|
|
85
|
+
"""Vault balances response."""
|
|
86
|
+
pass
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Global subscription method types."""
|
|
2
|
+
from typing import List, Literal, Optional
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator
|
|
4
|
+
from eth_utils import is_address, to_checksum_address
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def validate_ethereum_address(value: str) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Validate and normalize an Ethereum address.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
value: The address string to validate
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Checksummed address string
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
ValueError: If the address is invalid
|
|
19
|
+
"""
|
|
20
|
+
if not isinstance(value, str):
|
|
21
|
+
raise ValueError(f"Address must be a string, got {type(value)}")
|
|
22
|
+
|
|
23
|
+
if not is_address(value):
|
|
24
|
+
raise ValueError(f"Invalid Ethereum address: {value}")
|
|
25
|
+
|
|
26
|
+
return to_checksum_address(value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Chart types
|
|
30
|
+
SupportedChartResolutions = Literal["1", "5", "15", "60", "240", "1D", "1W"]
|
|
31
|
+
SupportedChartTypes = Literal["mark", "ltp", "index"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Subscription parameter types
|
|
35
|
+
class TickerSubscriptionParams(BaseModel):
|
|
36
|
+
"""Parameters for ticker subscription."""
|
|
37
|
+
symbol: str = Field(..., description="Trading pair symbol")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MidsSubscriptionParams(BaseModel):
|
|
41
|
+
"""Parameters for mids subscription."""
|
|
42
|
+
symbol: str = Field(..., description="Trading pair symbol")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BBOSubscriptionParams(BaseModel):
|
|
46
|
+
"""Parameters for BBO subscription."""
|
|
47
|
+
symbol: str = Field(..., description="Trading pair symbol")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class OrderbookSubscriptionParams(BaseModel):
|
|
51
|
+
"""Parameters for orderbook subscription."""
|
|
52
|
+
instrument_id: str = Field(..., description="Instrument ID")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TradeSubscriptionParams(BaseModel):
|
|
56
|
+
"""Parameters for trade subscription."""
|
|
57
|
+
instrument_id: str = Field(..., description="Instrument ID")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ChartSubscriptionParams(BaseModel):
|
|
61
|
+
"""Parameters for chart subscription."""
|
|
62
|
+
symbol: str = Field(..., description="Trading pair symbol")
|
|
63
|
+
chart_type: SupportedChartTypes = Field(..., description="Chart type")
|
|
64
|
+
resolution: SupportedChartResolutions = Field(..., description="Chart resolution")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AccountOrderUpdatesParams(BaseModel):
|
|
68
|
+
"""Parameters for account order updates subscription."""
|
|
69
|
+
address: str = Field(..., description="User address")
|
|
70
|
+
|
|
71
|
+
@field_validator('address', mode='before')
|
|
72
|
+
@classmethod
|
|
73
|
+
def validate_address(cls, v: str) -> str:
|
|
74
|
+
"""Validate and checksum the user address."""
|
|
75
|
+
return validate_ethereum_address(v)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AccountBalanceUpdatesParams(BaseModel):
|
|
79
|
+
"""Parameters for account balance updates subscription."""
|
|
80
|
+
address: str = Field(..., description="User address")
|
|
81
|
+
|
|
82
|
+
@field_validator('address', mode='before')
|
|
83
|
+
@classmethod
|
|
84
|
+
def validate_address(cls, v: str) -> str:
|
|
85
|
+
"""Validate and checksum the user address."""
|
|
86
|
+
return validate_ethereum_address(v)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PositionsSubscriptionParams(BaseModel):
|
|
90
|
+
"""Parameters for positions subscription."""
|
|
91
|
+
address: str = Field(..., description="User address")
|
|
92
|
+
|
|
93
|
+
@field_validator('address', mode='before')
|
|
94
|
+
@classmethod
|
|
95
|
+
def validate_address(cls, v: str) -> str:
|
|
96
|
+
"""Validate and checksum the user address."""
|
|
97
|
+
return validate_ethereum_address(v)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class FillsSubscriptionParams(BaseModel):
|
|
101
|
+
"""Parameters for fills subscription."""
|
|
102
|
+
address: str = Field(..., description="User address")
|
|
103
|
+
|
|
104
|
+
@field_validator('address', mode='before')
|
|
105
|
+
@classmethod
|
|
106
|
+
def validate_address(cls, v: str) -> str:
|
|
107
|
+
"""Validate and checksum the user address."""
|
|
108
|
+
return validate_ethereum_address(v)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AccountSummarySubscriptionParams(BaseModel):
|
|
112
|
+
"""Parameters for account summary subscription."""
|
|
113
|
+
user: str = Field(..., description="User address")
|
|
114
|
+
|
|
115
|
+
@field_validator('user', mode='before')
|
|
116
|
+
@classmethod
|
|
117
|
+
def validate_user_address(cls, v: str) -> str:
|
|
118
|
+
"""Validate and checksum the user address."""
|
|
119
|
+
return validate_ethereum_address(v)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class BlocksSubscriptionParams(BaseModel):
|
|
123
|
+
"""Parameters for blocks subscription."""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TransactionsSubscriptionParams(BaseModel):
|
|
128
|
+
"""Parameters for transactions subscription."""
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Orderbook subscription
|
|
133
|
+
class OrderbookItem(BaseModel):
|
|
134
|
+
"""Orderbook item."""
|
|
135
|
+
price: float
|
|
136
|
+
size: float
|
|
137
|
+
amount: Optional[float] = None # Alternative field name
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Orderbook(BaseModel):
|
|
141
|
+
"""Orderbook subscription data."""
|
|
142
|
+
instrument_id: str
|
|
143
|
+
instrument_name: Optional[str] = None # Alternative field name
|
|
144
|
+
asks: List[OrderbookItem]
|
|
145
|
+
bids: List[OrderbookItem]
|
|
146
|
+
timestamp: int
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Trade subscription
|
|
150
|
+
class Trade(BaseModel):
|
|
151
|
+
"""Trade subscription data."""
|
|
152
|
+
id: str
|
|
153
|
+
instrument: str
|
|
154
|
+
instrument_name: Optional[str] = None
|
|
155
|
+
maker: str
|
|
156
|
+
taker: str
|
|
157
|
+
price: float
|
|
158
|
+
size: float
|
|
159
|
+
timestamp: int
|
|
160
|
+
side: Literal["buy", "sell"]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# Order update subscription
|
|
164
|
+
class OrderUpdate(BaseModel):
|
|
165
|
+
"""Order update subscription data."""
|
|
166
|
+
id: str
|
|
167
|
+
account: str
|
|
168
|
+
instrument: str
|
|
169
|
+
price: float
|
|
170
|
+
size: float
|
|
171
|
+
side: Literal["buy", "sell"]
|
|
172
|
+
status: str
|
|
173
|
+
timestamp: int
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Account balance update subscription
|
|
177
|
+
class BalanceItem(BaseModel):
|
|
178
|
+
"""Balance item."""
|
|
179
|
+
asset: str
|
|
180
|
+
total: float
|
|
181
|
+
available: float
|
|
182
|
+
locked: float
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class AccountBalanceUpdate(BaseModel):
|
|
186
|
+
"""Account balance update subscription data."""
|
|
187
|
+
account: str
|
|
188
|
+
balances: List[BalanceItem]
|
|
189
|
+
timestamp: int
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Chart update subscription
|
|
193
|
+
class ChartUpdate(BaseModel):
|
|
194
|
+
"""Chart update subscription data."""
|
|
195
|
+
open: float
|
|
196
|
+
high: float
|
|
197
|
+
low: float
|
|
198
|
+
close: float
|
|
199
|
+
volume: float
|
|
200
|
+
time: int # in milliseconds
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""HTTP transport implementation."""
|
|
2
|
+
import json
|
|
3
|
+
from typing import Optional, Any, Dict, Callable, Awaitable
|
|
4
|
+
import aiohttp
|
|
5
|
+
|
|
6
|
+
from hotstuff.types import HttpTransportOptions
|
|
7
|
+
from hotstuff.utils import ENDPOINTS_URLS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HttpTransport:
|
|
11
|
+
"""HTTP transport for making API requests."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, options: Optional[HttpTransportOptions] = None):
|
|
14
|
+
"""
|
|
15
|
+
Initialize HTTP transport.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
options: Transport configuration options
|
|
19
|
+
"""
|
|
20
|
+
options = options or HttpTransportOptions()
|
|
21
|
+
|
|
22
|
+
self.is_testnet = options.is_testnet
|
|
23
|
+
self.timeout = options.timeout
|
|
24
|
+
|
|
25
|
+
# Setup server endpoints
|
|
26
|
+
self.server = {
|
|
27
|
+
"mainnet": {
|
|
28
|
+
"api": ENDPOINTS_URLS["mainnet"]["api"],
|
|
29
|
+
"rpc": ENDPOINTS_URLS["mainnet"]["rpc"],
|
|
30
|
+
},
|
|
31
|
+
"testnet": {
|
|
32
|
+
"api": ENDPOINTS_URLS["testnet"]["api"],
|
|
33
|
+
"rpc": ENDPOINTS_URLS["testnet"]["rpc"],
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if options.server:
|
|
38
|
+
if "mainnet" in options.server:
|
|
39
|
+
self.server["mainnet"].update(options.server["mainnet"])
|
|
40
|
+
if "testnet" in options.server:
|
|
41
|
+
self.server["testnet"].update(options.server["testnet"])
|
|
42
|
+
|
|
43
|
+
self.headers = options.headers or {}
|
|
44
|
+
self.on_request = options.on_request
|
|
45
|
+
self.on_response = options.on_response
|
|
46
|
+
|
|
47
|
+
# Session will be created lazily
|
|
48
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
49
|
+
|
|
50
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
51
|
+
"""Get or create aiohttp session."""
|
|
52
|
+
if self._session is None or self._session.closed:
|
|
53
|
+
timeout = aiohttp.ClientTimeout(total=self.timeout) if self.timeout else None
|
|
54
|
+
self._session = aiohttp.ClientSession(timeout=timeout)
|
|
55
|
+
return self._session
|
|
56
|
+
|
|
57
|
+
async def request(
|
|
58
|
+
self,
|
|
59
|
+
endpoint: str,
|
|
60
|
+
payload: Any,
|
|
61
|
+
signal: Optional[Any] = None,
|
|
62
|
+
method: str = "POST"
|
|
63
|
+
) -> Any:
|
|
64
|
+
"""
|
|
65
|
+
Make an HTTP request.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
endpoint: The endpoint to call ('info', 'exchange', or 'explorer')
|
|
69
|
+
payload: The request payload
|
|
70
|
+
signal: Optional abort signal
|
|
71
|
+
method: HTTP method (GET or POST)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The response data
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
Exception: If the request fails
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
# Determine the base URL
|
|
81
|
+
network = "testnet" if self.is_testnet else "mainnet"
|
|
82
|
+
base_url = self.server[network]["rpc" if endpoint == "explorer" else "api"]
|
|
83
|
+
url = f"{base_url}{endpoint}"
|
|
84
|
+
|
|
85
|
+
# Prepare headers
|
|
86
|
+
headers = {
|
|
87
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
**self.headers,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Get session
|
|
93
|
+
session = await self._get_session()
|
|
94
|
+
|
|
95
|
+
# Prepare request kwargs
|
|
96
|
+
kwargs: Dict[str, Any] = {
|
|
97
|
+
"headers": headers,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if method == "POST":
|
|
101
|
+
kwargs["json"] = payload
|
|
102
|
+
|
|
103
|
+
# Make request
|
|
104
|
+
async with session.request(method, url, **kwargs) as response:
|
|
105
|
+
# Check if response is OK
|
|
106
|
+
if not response.ok:
|
|
107
|
+
text = await response.text()
|
|
108
|
+
raise Exception(text or f"HTTP {response.status}")
|
|
109
|
+
|
|
110
|
+
# Check content type
|
|
111
|
+
content_type = response.headers.get("Content-Type", "")
|
|
112
|
+
if "application/json" not in content_type:
|
|
113
|
+
text = await response.text()
|
|
114
|
+
raise Exception(text)
|
|
115
|
+
|
|
116
|
+
# Parse response
|
|
117
|
+
body = await response.json()
|
|
118
|
+
|
|
119
|
+
# Check for error in response
|
|
120
|
+
if isinstance(body, dict) and body.get("type") == "error":
|
|
121
|
+
raise Exception(body.get("message", "Unknown error"))
|
|
122
|
+
|
|
123
|
+
return body
|
|
124
|
+
|
|
125
|
+
except aiohttp.ClientError as e:
|
|
126
|
+
raise Exception(f"HTTP request failed: {str(e)}")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise e
|
|
129
|
+
|
|
130
|
+
async def close(self):
|
|
131
|
+
"""Close the HTTP session."""
|
|
132
|
+
if self._session and not self._session.closed:
|
|
133
|
+
await self._session.close()
|
|
134
|
+
|
|
135
|
+
async def __aenter__(self):
|
|
136
|
+
"""Async context manager entry."""
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
140
|
+
"""Async context manager exit."""
|
|
141
|
+
await self.close()
|
|
142
|
+
|