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,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,8 @@
1
+ """Subscription method types."""
2
+ import importlib
3
+
4
+ GlobalSubscriptionMethods = importlib.import_module("hotstuff.methods.subscription.global")
5
+
6
+ __all__ = [
7
+ "GlobalSubscriptionMethods",
8
+ ]
@@ -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,9 @@
1
+ """Transports package."""
2
+ from hotstuff.transports.http import HttpTransport
3
+ from hotstuff.transports.websocket import WebSocketTransport
4
+
5
+ __all__ = [
6
+ "HttpTransport",
7
+ "WebSocketTransport",
8
+ ]
9
+
@@ -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
+