hotstuff-python-sdk 0.0.1b1__py3-none-any.whl → 0.0.1b3__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 CHANGED
@@ -1,19 +1,41 @@
1
1
  """Hotstuff Python SDK.
2
2
 
3
- A Python SDK for interacting with Hotstuff Labs decentralized exchange.
3
+ A Python SDK for interacting with Hotstuff L1.
4
4
  """
5
5
 
6
- __version__ = "0.0.1-beta.1"
6
+ __version__ = "0.0.1-beta.3"
7
7
 
8
+ # Transports
8
9
  from hotstuff.transports import HttpTransport, WebSocketTransport
10
+
11
+ # Clients
9
12
  from hotstuff.apis import InfoClient, ExchangeClient, SubscriptionClient
13
+
14
+ # Transport Types
10
15
  from hotstuff.types import (
11
16
  HttpTransportOptions,
12
17
  WebSocketTransportOptions,
13
18
  )
19
+
20
+ # Utils
14
21
  from hotstuff.utils import NonceManager, sign_action, EXCHANGE_OP_CODES
15
22
 
16
- # Export method types for convenience
23
+ # Exceptions
24
+ from hotstuff.exceptions import (
25
+ HotstuffError,
26
+ HotstuffAPIError,
27
+ HotstuffConnectionError,
28
+ HotstuffTimeoutError,
29
+ HotstuffAuthenticationError,
30
+ HotstuffValidationError,
31
+ HotstuffInsufficientFundsError,
32
+ HotstuffOrderError,
33
+ HotstuffRateLimitError,
34
+ HotstuffWebSocketError,
35
+ HotstuffSubscriptionError,
36
+ )
37
+
38
+ # Exchange Method Types (for convenience)
17
39
  from hotstuff.methods.exchange.trading import (
18
40
  UnitOrder,
19
41
  BrokerConfig,
@@ -24,6 +46,27 @@ from hotstuff.methods.exchange.trading import (
24
46
  )
25
47
  from hotstuff.methods.exchange.account import AddAgentParams
26
48
 
49
+ # Market data types (clean imports without 'global' keyword)
50
+ from hotstuff.methods.info.market import (
51
+ TickerParams,
52
+ Ticker,
53
+ OrderbookParams,
54
+ OrderbookResponse,
55
+ InstrumentsParams,
56
+ InstrumentsResponse,
57
+ TradesParams,
58
+ Trade,
59
+ OracleParams,
60
+ OracleResponse,
61
+ )
62
+
63
+ # Subscription types (clean imports)
64
+ from hotstuff.methods.subscription.channels import (
65
+ TickerSubscriptionParams,
66
+ TradeSubscriptionParams,
67
+ OrderbookSubscriptionParams,
68
+ )
69
+
27
70
  __all__ = [
28
71
  # Version
29
72
  "__version__",
@@ -37,7 +80,19 @@ __all__ = [
37
80
  # Transport Types
38
81
  "HttpTransportOptions",
39
82
  "WebSocketTransportOptions",
40
- # Exchange Method Types (for backward compatibility)
83
+ # Exceptions
84
+ "HotstuffError",
85
+ "HotstuffAPIError",
86
+ "HotstuffConnectionError",
87
+ "HotstuffTimeoutError",
88
+ "HotstuffAuthenticationError",
89
+ "HotstuffValidationError",
90
+ "HotstuffInsufficientFundsError",
91
+ "HotstuffOrderError",
92
+ "HotstuffRateLimitError",
93
+ "HotstuffWebSocketError",
94
+ "HotstuffSubscriptionError",
95
+ # Exchange Method Types
41
96
  "UnitOrder",
42
97
  "BrokerConfig",
43
98
  "PlaceOrderParams",
@@ -45,6 +100,21 @@ __all__ = [
45
100
  "CancelByCloidParams",
46
101
  "CancelAllParams",
47
102
  "AddAgentParams",
103
+ # Market Data Types
104
+ "TickerParams",
105
+ "Ticker",
106
+ "OrderbookParams",
107
+ "OrderbookResponse",
108
+ "InstrumentsParams",
109
+ "InstrumentsResponse",
110
+ "TradesParams",
111
+ "Trade",
112
+ "OracleParams",
113
+ "OracleResponse",
114
+ # Subscription Types
115
+ "TickerSubscriptionParams",
116
+ "TradeSubscriptionParams",
117
+ "OrderbookSubscriptionParams",
48
118
  # Utils
49
119
  "NonceManager",
50
120
  "sign_action",
hotstuff/apis/exchange.py CHANGED
@@ -38,7 +38,6 @@ class ExchangeClient:
38
38
  async def add_agent(
39
39
  self,
40
40
  params: AM.AddAgentParams,
41
- execute: bool = True,
42
41
  signal: Optional[Any] = None
43
42
  ) -> Dict[str, Any]:
44
43
  """
@@ -46,7 +45,6 @@ class ExchangeClient:
46
45
 
47
46
  Args:
48
47
  params: Agent parameters
49
- execute: Whether to execute the action
50
48
  signal: Optional abort signal
51
49
 
52
50
  Returns:
@@ -58,7 +56,7 @@ class ExchangeClient:
58
56
  agent_account = Account.from_key(params.agent_private_key)
59
57
 
60
58
  # Sign with agent account
61
- agent_signature = await sign_action(
59
+ agent_signature = sign_action(
62
60
  wallet=agent_account,
63
61
  action={
64
62
  "signer": params.signer,
@@ -80,8 +78,7 @@ class ExchangeClient:
80
78
 
81
79
  return await self._execute_action(
82
80
  {"action": "addAgent", "params": params_dict},
83
- signal,
84
- execute
81
+ signal
85
82
  )
86
83
 
87
84
  async def revoke_agent(
@@ -484,11 +481,11 @@ class ExchangeClient:
484
481
  params["nonce"] = await self.nonce()
485
482
 
486
483
  # Sign the action
487
- signature = await sign_action(
484
+ signature = sign_action(
488
485
  wallet=self.wallet,
489
486
  action=params,
490
487
  tx_type=EXCHANGE_OP_CODES[action],
491
- is_testnet=True,
488
+ is_testnet=self.transport.is_testnet,
492
489
  )
493
490
 
494
491
  if execute:
hotstuff/apis/info.py CHANGED
@@ -1,11 +1,10 @@
1
1
  """Info API client."""
2
2
  from typing import Optional, Any, List
3
- import importlib
4
3
 
5
- GM = importlib.import_module("hotstuff.methods.info.global")
6
- AM = importlib.import_module("hotstuff.methods.info.account")
7
- VM = importlib.import_module("hotstuff.methods.info.vault")
8
- EM = importlib.import_module("hotstuff.methods.info.explorer")
4
+ from hotstuff.methods.info import market as GM
5
+ from hotstuff.methods.info import account as AM
6
+ from hotstuff.methods.info import vault as VM
7
+ from hotstuff.methods.info import explorer as EM
9
8
 
10
9
 
11
10
  class InfoClient:
@@ -110,11 +109,12 @@ class InfoClient:
110
109
 
111
110
  async def positions(
112
111
  self, params: AM.PositionsParams, signal: Optional[Any] = None
113
- ) -> AM.PositionsResponse:
112
+ ) -> Any:
114
113
  """Get current positions."""
115
114
  request = {"method": "positions", "params": params.model_dump()}
116
115
  response = await self.transport.request("info", request, signal)
117
- return AM.PositionsResponse.model_validate(response)
116
+ # Returns a list of positions
117
+ return response
118
118
 
119
119
  async def account_summary(
120
120
  self, params: AM.AccountSummaryParams, signal: Optional[Any] = None
@@ -1,8 +1,7 @@
1
1
  """Subscription API client for real-time data."""
2
2
  from typing import Callable, Dict, Any
3
- import importlib
4
3
 
5
- SM = importlib.import_module("hotstuff.methods.subscription.global")
4
+ from hotstuff.methods.subscription import channels as SM
6
5
 
7
6
 
8
7
  class SubscriptionClient:
@@ -153,16 +152,13 @@ class SubscriptionClient:
153
152
  Subscribe to order updates.
154
153
 
155
154
  Args:
156
- params: Subscription parameters (address)
155
+ params: Subscription parameters (user address)
157
156
  listener: Callback function for updates
158
157
 
159
158
  Returns:
160
159
  Subscription object with unsubscribe method
161
160
  """
162
- # Convert to format expected by API (both address and user)
163
- params_dict = params.model_dump()
164
- params_dict["user"] = params_dict["address"]
165
- return await self.transport.subscribe("accountOrderUpdates", params_dict, listener)
161
+ return await self.transport.subscribe("accountOrderUpdates", params.model_dump(), listener)
166
162
 
167
163
  async def account_balance_updates(
168
164
  self,
@@ -173,16 +169,13 @@ class SubscriptionClient:
173
169
  Subscribe to balance updates.
174
170
 
175
171
  Args:
176
- params: Subscription parameters (address)
172
+ params: Subscription parameters (user address)
177
173
  listener: Callback function for updates
178
174
 
179
175
  Returns:
180
176
  Subscription object with unsubscribe method
181
177
  """
182
- # Convert to format expected by API (both address and user)
183
- params_dict = params.model_dump()
184
- params_dict["user"] = params_dict["address"]
185
- return await self.transport.subscribe("accountBalanceUpdates", params_dict, listener)
178
+ return await self.transport.subscribe("accountBalanceUpdates", params.model_dump(), listener)
186
179
 
187
180
  async def positions(
188
181
  self,
@@ -193,16 +186,13 @@ class SubscriptionClient:
193
186
  Subscribe to position updates.
194
187
 
195
188
  Args:
196
- params: Subscription parameters (address)
189
+ params: Subscription parameters (user address)
197
190
  listener: Callback function for updates
198
191
 
199
192
  Returns:
200
193
  Subscription object with unsubscribe method
201
194
  """
202
- # Convert to format expected by API (both address and user)
203
- params_dict = params.model_dump()
204
- params_dict["user"] = params_dict["address"]
205
- return await self.transport.subscribe("positions", params_dict, listener)
195
+ return await self.transport.subscribe("positions", params.model_dump(), listener)
206
196
 
207
197
  async def fills(
208
198
  self,
@@ -213,16 +203,13 @@ class SubscriptionClient:
213
203
  Subscribe to fills.
214
204
 
215
205
  Args:
216
- params: Subscription parameters (address)
206
+ params: Subscription parameters (user address)
217
207
  listener: Callback function for updates
218
208
 
219
209
  Returns:
220
210
  Subscription object with unsubscribe method
221
211
  """
222
- # Convert to format expected by API (both address and user)
223
- params_dict = params.model_dump()
224
- params_dict["user"] = params_dict["address"]
225
- return await self.transport.subscribe("fills", params_dict, listener)
212
+ return await self.transport.subscribe("fills", params.model_dump(), listener)
226
213
 
227
214
  async def account_summary(
228
215
  self,
hotstuff/exceptions.py ADDED
@@ -0,0 +1,72 @@
1
+ """Custom exceptions for the Hotstuff SDK."""
2
+
3
+
4
+ class HotstuffError(Exception):
5
+ """Base exception for all Hotstuff SDK errors."""
6
+ pass
7
+
8
+
9
+ class HotstuffAPIError(HotstuffError):
10
+ """Error returned from the Hotstuff API."""
11
+
12
+ def __init__(self, message: str, status_code: int = None, error_code: str = None):
13
+ self.message = message
14
+ self.status_code = status_code
15
+ self.error_code = error_code
16
+ super().__init__(self.message)
17
+
18
+ def __str__(self):
19
+ parts = [self.message]
20
+ if self.status_code:
21
+ parts.append(f"(HTTP {self.status_code})")
22
+ if self.error_code:
23
+ parts.append(f"[{self.error_code}]")
24
+ return " ".join(parts)
25
+
26
+
27
+ class HotstuffConnectionError(HotstuffError):
28
+ """Error connecting to the Hotstuff API."""
29
+ pass
30
+
31
+
32
+ class HotstuffTimeoutError(HotstuffError):
33
+ """Request to the Hotstuff API timed out."""
34
+ pass
35
+
36
+
37
+ class HotstuffAuthenticationError(HotstuffAPIError):
38
+ """Authentication error (invalid signature, expired, etc.)."""
39
+ pass
40
+
41
+
42
+ class HotstuffValidationError(HotstuffError):
43
+ """Error validating request parameters."""
44
+ pass
45
+
46
+
47
+ class HotstuffInsufficientFundsError(HotstuffAPIError):
48
+ """Insufficient funds for the requested operation."""
49
+ pass
50
+
51
+
52
+ class HotstuffOrderError(HotstuffAPIError):
53
+ """Error related to order placement or management."""
54
+ pass
55
+
56
+
57
+ class HotstuffRateLimitError(HotstuffAPIError):
58
+ """Rate limit exceeded."""
59
+
60
+ def __init__(self, message: str = "Rate limit exceeded", retry_after: int = None):
61
+ super().__init__(message, status_code=429)
62
+ self.retry_after = retry_after
63
+
64
+
65
+ class HotstuffWebSocketError(HotstuffError):
66
+ """WebSocket-related error."""
67
+ pass
68
+
69
+
70
+ class HotstuffSubscriptionError(HotstuffWebSocketError):
71
+ """Error subscribing to a channel."""
72
+ pass
@@ -1,30 +1,8 @@
1
1
  """Account exchange method types."""
2
2
  from typing import Optional
3
3
  from pydantic import BaseModel, Field, ConfigDict, field_validator
4
- from eth_utils import is_address, to_checksum_address
5
4
 
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)
5
+ from hotstuff.utils.address import validate_ethereum_address
28
6
 
29
7
 
30
8
  # Add Agent Method
@@ -1,30 +1,8 @@
1
1
  """Collateral exchange method types."""
2
2
  from typing import Optional
3
3
  from pydantic import BaseModel, Field, ConfigDict, 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)
4
+
5
+ from hotstuff.utils.address import validate_ethereum_address
28
6
 
29
7
 
30
8
  # Account Spot Withdraw Request Method
@@ -1,30 +1,40 @@
1
1
  """Trading exchange method types."""
2
+ import re
2
3
  from typing import List, Literal, Optional, Any
3
4
  from pydantic import BaseModel, Field, ConfigDict, field_validator, field_serializer
4
- from eth_utils import is_address, to_checksum_address
5
5
 
6
+ from hotstuff.utils.address import validate_ethereum_address
6
7
 
7
- def validate_ethereum_address(value: str) -> str:
8
+
9
+ # Regex pattern for valid cloid: 0x followed by exactly 32 hex digits (128-bit)
10
+ CLOID_PATTERN = re.compile(r'^0x[0-9a-fA-F]{32}$')
11
+
12
+
13
+ def validate_cloid(value: Optional[str]) -> Optional[str]:
8
14
  """
9
- Validate and normalize an Ethereum address.
15
+ Validate client order ID format.
10
16
 
11
17
  Args:
12
- value: The address string to validate
18
+ value: The cloid string to validate
13
19
 
14
20
  Returns:
15
- Checksummed address string
21
+ The validated cloid string (lowercase hex)
16
22
 
17
23
  Raises:
18
- ValueError: If the address is invalid
24
+ ValueError: If the cloid format is invalid
19
25
  """
20
- if not isinstance(value, str):
21
- raise ValueError(f"Address must be a string, got {type(value)}")
26
+ if value is None or value == "":
27
+ return None
22
28
 
23
- if not is_address(value):
24
- raise ValueError(f"Invalid Ethereum address: {value}")
29
+ if not CLOID_PATTERN.match(value):
30
+ raise ValueError(
31
+ f"Invalid cloid format: '{value}'. "
32
+ "ClOrdID must be 0x followed by 32 hex digits (128-bit). "
33
+ "Example: 0x1234567890abcdef1234567890abcdef"
34
+ )
25
35
 
26
- # Return checksummed address (EIP-55)
27
- return to_checksum_address(value)
36
+ # Return lowercase for consistency
37
+ return value.lower()
28
38
 
29
39
 
30
40
  # Place Order Method
@@ -38,7 +48,10 @@ class UnitOrder(BaseModel):
38
48
  tif: Literal["GTC", "IOC", "FOK"] = Field(..., description="Time in force")
39
49
  ro: bool = Field(..., description="Reduce-only flag")
40
50
  po: bool = Field(..., description="Post-only flag")
41
- cloid: str = Field(..., description="Client order ID")
51
+ cloid: Optional[str] = Field(
52
+ None,
53
+ description="Client order ID (optional). Format: 0x + 32 hex digits (128-bit). Example: 0x1234567890abcdef1234567890abcdef"
54
+ )
42
55
  trigger_px: Optional[str] = Field(None, alias="triggerPx", description="Trigger price")
43
56
  is_market: Optional[bool] = Field(None, alias="isMarket", description="Market order flag")
44
57
  tpsl: Optional[Literal["tp", "sl", ""]] = Field(None, description="Take profit/stop loss")
@@ -46,9 +59,15 @@ class UnitOrder(BaseModel):
46
59
 
47
60
  model_config = ConfigDict(populate_by_name=True)
48
61
 
49
- @field_serializer('trigger_px', 'tpsl', 'grouping')
62
+ @field_validator('cloid', mode='before')
63
+ @classmethod
64
+ def validate_cloid_format(cls, v: Optional[str]) -> Optional[str]:
65
+ """Validate cloid format: 0x + 32 hex digits."""
66
+ return validate_cloid(v)
67
+
68
+ @field_serializer('cloid', 'trigger_px', 'tpsl', 'grouping')
50
69
  def serialize_optional_strings(self, value: Optional[str], _info) -> str:
51
- """Convert None to empty string for optional string fields to match original SDK."""
70
+ """Convert None to empty string for optional string fields to match API expectations."""
52
71
  return "" if value is None else value
53
72
 
54
73
 
@@ -95,8 +114,20 @@ class CancelByOidParams(BaseModel):
95
114
  # Cancel By Cloid Method
96
115
  class UnitCancelByClOrderId(BaseModel):
97
116
  """Cancel by client order ID unit."""
98
- cloid: str = Field(..., description="Client order ID")
117
+ cloid: str = Field(
118
+ ...,
119
+ description="Client order ID. Format: 0x + 32 hex digits (128-bit). Example: 0x1234567890abcdef1234567890abcdef"
120
+ )
99
121
  instrument_id: int = Field(..., gt=0, description="Instrument ID")
122
+
123
+ @field_validator('cloid', mode='before')
124
+ @classmethod
125
+ def validate_cloid_format(cls, v: str) -> str:
126
+ """Validate cloid format: 0x + 32 hex digits."""
127
+ result = validate_cloid(v)
128
+ if result is None:
129
+ raise ValueError("cloid is required for cancel by cloid")
130
+ return result
100
131
 
101
132
 
102
133
  class CancelByCloidParams(BaseModel):
@@ -1,30 +1,8 @@
1
1
  """Vault exchange method types."""
2
2
  from typing import Optional
3
3
  from pydantic import BaseModel, Field, ConfigDict, 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)
4
+
5
+ from hotstuff.utils.address import validate_ethereum_address
28
6
 
29
7
 
30
8
  # Deposit To Vault Method