hotstuff-python-sdk 0.0.1b2__py3-none-any.whl → 0.0.1b4__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,200 @@
1
+ """Subscription channel method types."""
2
+ from typing import List, Literal, Optional
3
+ from pydantic import BaseModel, Field, field_validator
4
+
5
+ from hotstuff.utils.address import validate_ethereum_address
6
+
7
+
8
+ # Chart types
9
+ SupportedChartResolutions = Literal["1", "5", "15", "60", "240", "1D", "1W"]
10
+ SupportedChartTypes = Literal["mark", "ltp", "index"]
11
+
12
+
13
+ # Subscription parameter types
14
+ class TickerSubscriptionParams(BaseModel):
15
+ """Parameters for ticker subscription."""
16
+ symbol: str = Field(..., description="Trading pair symbol")
17
+
18
+
19
+ class MidsSubscriptionParams(BaseModel):
20
+ """Parameters for mids subscription."""
21
+ symbol: str = Field(..., description="Trading pair symbol")
22
+
23
+
24
+ class BBOSubscriptionParams(BaseModel):
25
+ """Parameters for BBO subscription."""
26
+ symbol: str = Field(..., description="Trading pair symbol")
27
+
28
+
29
+ class OrderbookSubscriptionParams(BaseModel):
30
+ """Parameters for orderbook subscription."""
31
+ instrument_id: str = Field(..., description="Instrument ID")
32
+
33
+
34
+ class TradeSubscriptionParams(BaseModel):
35
+ """Parameters for trade subscription."""
36
+ instrument_id: str = Field(..., description="Instrument ID")
37
+
38
+
39
+ class ChartSubscriptionParams(BaseModel):
40
+ """Parameters for chart subscription."""
41
+ symbol: str = Field(..., description="Trading pair symbol")
42
+ chart_type: SupportedChartTypes = Field(..., description="Chart type")
43
+ resolution: SupportedChartResolutions = Field(..., description="Chart resolution")
44
+
45
+
46
+ class AccountOrderUpdatesParams(BaseModel):
47
+ """Parameters for account order updates subscription."""
48
+ user: str = Field(..., description="User address")
49
+
50
+ @field_validator('user', mode='before')
51
+ @classmethod
52
+ def validate_user(cls, v: str) -> str:
53
+ """Validate and checksum the user address."""
54
+ return validate_ethereum_address(v)
55
+
56
+ # Alias for backward compatibility
57
+ @property
58
+ def address(self) -> str:
59
+ """Alias for user field (deprecated)."""
60
+ return self.user
61
+
62
+
63
+ class AccountBalanceUpdatesParams(BaseModel):
64
+ """Parameters for account balance updates subscription."""
65
+ user: str = Field(..., description="User address")
66
+
67
+ @field_validator('user', mode='before')
68
+ @classmethod
69
+ def validate_user(cls, v: str) -> str:
70
+ """Validate and checksum the user address."""
71
+ return validate_ethereum_address(v)
72
+
73
+ @property
74
+ def address(self) -> str:
75
+ """Alias for user field (deprecated)."""
76
+ return self.user
77
+
78
+
79
+ class PositionsSubscriptionParams(BaseModel):
80
+ """Parameters for positions subscription."""
81
+ user: str = Field(..., description="User address")
82
+
83
+ @field_validator('user', mode='before')
84
+ @classmethod
85
+ def validate_user(cls, v: str) -> str:
86
+ """Validate and checksum the user address."""
87
+ return validate_ethereum_address(v)
88
+
89
+ @property
90
+ def address(self) -> str:
91
+ """Alias for user field (deprecated)."""
92
+ return self.user
93
+
94
+
95
+ class FillsSubscriptionParams(BaseModel):
96
+ """Parameters for fills subscription."""
97
+ user: str = Field(..., description="User address")
98
+
99
+ @field_validator('user', mode='before')
100
+ @classmethod
101
+ def validate_user(cls, v: str) -> str:
102
+ """Validate and checksum the user address."""
103
+ return validate_ethereum_address(v)
104
+
105
+ @property
106
+ def address(self) -> str:
107
+ """Alias for user field (deprecated)."""
108
+ return self.user
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(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
138
+
139
+
140
+ class Orderbook(BaseModel):
141
+ """Orderbook subscription data."""
142
+ instrument_id: str
143
+ instrument_name: Optional[str] = None
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
@@ -1,200 +1,55 @@
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
1
+ """Global subscription method types.
2
+
3
+ DEPRECATED: This module is deprecated. Import from hotstuff.methods.subscription.channels instead.
4
+ This module exists for backward compatibility only.
5
+ """
6
+ # Re-export everything from channels.py for backward compatibility
7
+ from hotstuff.methods.subscription.channels import (
8
+ SupportedChartResolutions,
9
+ SupportedChartTypes,
10
+ TickerSubscriptionParams,
11
+ MidsSubscriptionParams,
12
+ BBOSubscriptionParams,
13
+ OrderbookSubscriptionParams,
14
+ TradeSubscriptionParams,
15
+ ChartSubscriptionParams,
16
+ AccountOrderUpdatesParams,
17
+ AccountBalanceUpdatesParams,
18
+ PositionsSubscriptionParams,
19
+ FillsSubscriptionParams,
20
+ AccountSummarySubscriptionParams,
21
+ BlocksSubscriptionParams,
22
+ TransactionsSubscriptionParams,
23
+ OrderbookItem,
24
+ Orderbook,
25
+ Trade,
26
+ OrderUpdate,
27
+ BalanceItem,
28
+ AccountBalanceUpdate,
29
+ ChartUpdate,
30
+ )
31
+
32
+ __all__ = [
33
+ "SupportedChartResolutions",
34
+ "SupportedChartTypes",
35
+ "TickerSubscriptionParams",
36
+ "MidsSubscriptionParams",
37
+ "BBOSubscriptionParams",
38
+ "OrderbookSubscriptionParams",
39
+ "TradeSubscriptionParams",
40
+ "ChartSubscriptionParams",
41
+ "AccountOrderUpdatesParams",
42
+ "AccountBalanceUpdatesParams",
43
+ "PositionsSubscriptionParams",
44
+ "FillsSubscriptionParams",
45
+ "AccountSummarySubscriptionParams",
46
+ "BlocksSubscriptionParams",
47
+ "TransactionsSubscriptionParams",
48
+ "OrderbookItem",
49
+ "Orderbook",
50
+ "Trade",
51
+ "OrderUpdate",
52
+ "BalanceItem",
53
+ "AccountBalanceUpdate",
54
+ "ChartUpdate",
55
+ ]
@@ -2,9 +2,17 @@
2
2
  import json
3
3
  from typing import Optional, Any, Dict, Callable, Awaitable
4
4
  import aiohttp
5
+ import asyncio
5
6
 
6
7
  from hotstuff.types import HttpTransportOptions
7
8
  from hotstuff.utils import ENDPOINTS_URLS
9
+ from hotstuff.exceptions import (
10
+ HotstuffAPIError,
11
+ HotstuffConnectionError,
12
+ HotstuffTimeoutError,
13
+ HotstuffRateLimitError,
14
+ HotstuffAuthenticationError,
15
+ )
8
16
 
9
17
 
10
18
  class HttpTransport:
@@ -74,7 +82,10 @@ class HttpTransport:
74
82
  The response data
75
83
 
76
84
  Raises:
77
- Exception: If the request fails
85
+ HotstuffAPIError: If the API returns an error
86
+ HotstuffConnectionError: If connection fails
87
+ HotstuffTimeoutError: If request times out
88
+ HotstuffRateLimitError: If rate limit is exceeded
78
89
  """
79
90
  try:
80
91
  # Determine the base URL
@@ -102,30 +113,49 @@ class HttpTransport:
102
113
 
103
114
  # Make request
104
115
  async with session.request(method, url, **kwargs) as response:
116
+ # Handle rate limiting
117
+ if response.status == 429:
118
+ retry_after = response.headers.get("Retry-After")
119
+ raise HotstuffRateLimitError(
120
+ "Rate limit exceeded",
121
+ retry_after=int(retry_after) if retry_after else None
122
+ )
123
+
124
+ # Handle authentication errors
125
+ if response.status in (401, 403):
126
+ text = await response.text()
127
+ raise HotstuffAuthenticationError(text or "Authentication failed", status_code=response.status)
128
+
105
129
  # Check if response is OK
106
130
  if not response.ok:
107
131
  text = await response.text()
108
- raise Exception(text or f"HTTP {response.status}")
132
+ raise HotstuffAPIError(text or f"HTTP {response.status}", status_code=response.status)
109
133
 
110
134
  # Check content type
111
135
  content_type = response.headers.get("Content-Type", "")
112
136
  if "application/json" not in content_type:
113
137
  text = await response.text()
114
- raise Exception(text)
138
+ raise HotstuffAPIError(f"Unexpected content type: {text}")
115
139
 
116
140
  # Parse response
117
141
  body = await response.json()
118
142
 
119
143
  # Check for error in response
120
144
  if isinstance(body, dict) and body.get("type") == "error":
121
- raise Exception(body.get("message", "Unknown error"))
145
+ raise HotstuffAPIError(body.get("message", "Unknown error"))
122
146
 
123
147
  return body
124
148
 
149
+ except asyncio.TimeoutError:
150
+ raise HotstuffTimeoutError(f"Request to {endpoint} timed out")
151
+ except aiohttp.ClientConnectorError as e:
152
+ raise HotstuffConnectionError(f"Failed to connect to {url}: {str(e)}")
125
153
  except aiohttp.ClientError as e:
126
- raise Exception(f"HTTP request failed: {str(e)}")
154
+ raise HotstuffConnectionError(f"HTTP request failed: {str(e)}")
155
+ except (HotstuffAPIError, HotstuffConnectionError, HotstuffTimeoutError, HotstuffRateLimitError):
156
+ raise
127
157
  except Exception as e:
128
- raise e
158
+ raise HotstuffAPIError(str(e))
129
159
 
130
160
  async def close(self):
131
161
  """Close the HTTP session."""
@@ -1,13 +1,16 @@
1
1
  """Utilities package."""
2
2
  from hotstuff.utils.endpoints import ENDPOINTS_URLS
3
3
  from hotstuff.utils.nonce import NonceManager
4
- from hotstuff.utils.signing import sign_action
4
+ from hotstuff.utils.signing import sign_action, canonicalize_for_signing
5
+ from hotstuff.utils.address import validate_ethereum_address
5
6
  from hotstuff.methods.exchange.op_codes import EXCHANGE_OP_CODES
6
7
 
7
8
  __all__ = [
8
9
  "ENDPOINTS_URLS",
9
10
  "NonceManager",
10
11
  "sign_action",
12
+ "canonicalize_for_signing",
13
+ "validate_ethereum_address",
11
14
  "EXCHANGE_OP_CODES",
12
15
  ]
13
16
 
hotstuff/utils/signing.py CHANGED
@@ -1,12 +1,33 @@
1
1
  """Signing utilities for EIP-712 typed data."""
2
2
  import msgpack
3
3
  from eth_account import Account
4
- from eth_account.messages import encode_structured_data
5
4
  from eth_utils import keccak
6
5
  from hotstuff.methods.exchange.op_codes import EXCHANGE_OP_CODES
7
6
 
7
+ # Check which API to use for encoding typed data
8
+ try:
9
+ from eth_account.messages import encode_typed_data
10
+ _USE_NEW_API = True
11
+ except ImportError:
12
+ from eth_account.messages import encode_structured_data
13
+ _USE_NEW_API = False
8
14
 
9
- async def sign_action(
15
+
16
+ def canonicalize_for_signing(obj):
17
+ """
18
+ Return a copy of obj with all dict keys sorted alphabetically, recursively.
19
+ Use this before signing and in the request body so msgpack bytes are
20
+ deterministic and match backend verification (fixes "invalid order signer"
21
+ for cancelByOid / cancelByCloid when key order differs from backend).
22
+ """
23
+ if isinstance(obj, dict):
24
+ return {k: canonicalize_for_signing(v) for k, v in sorted(obj.items())}
25
+ if isinstance(obj, list):
26
+ return [canonicalize_for_signing(item) for item in obj]
27
+ return obj
28
+
29
+
30
+ def sign_action(
10
31
  wallet: Account,
11
32
  action: dict,
12
33
  tx_type: int,
@@ -15,6 +36,9 @@ async def sign_action(
15
36
  """
16
37
  Sign an action using EIP-712.
17
38
 
39
+ This is a synchronous function that generates an EIP-712 signature
40
+ for the given action data.
41
+
18
42
  Args:
19
43
  wallet: The account to sign with
20
44
  action: The action data
@@ -22,10 +46,12 @@ async def sign_action(
22
46
  is_testnet: Whether this is for testnet
23
47
 
24
48
  Returns:
25
- str: The signature
49
+ str: The signature (hex string)
26
50
  """
51
+ # Canonicalize key order so msgpack bytes match backend (deterministic signing)
52
+ canonical_action = canonicalize_for_signing(action)
27
53
  # Encode action to msgpack
28
- action_bytes = msgpack.packb(action)
54
+ action_bytes = msgpack.packb(canonical_action)
29
55
 
30
56
  # Hash the payload
31
57
  payload_hash = keccak(action_bytes)
@@ -38,14 +64,8 @@ async def sign_action(
38
64
  "verifyingContract": "0x1234567890123456789012345678901234567890",
39
65
  }
40
66
 
41
- # EIP-712 types
42
- types = {
43
- "EIP712Domain": [
44
- {"name": "name", "type": "string"},
45
- {"name": "version", "type": "string"},
46
- {"name": "chainId", "type": "uint256"},
47
- {"name": "verifyingContract", "type": "address"},
48
- ],
67
+ # EIP-712 message types (without EIP712Domain for new API)
68
+ message_types = {
49
69
  "Action": [
50
70
  {"name": "source", "type": "string"},
51
71
  {"name": "hash", "type": "bytes32"},
@@ -53,23 +73,40 @@ async def sign_action(
53
73
  ],
54
74
  }
55
75
 
56
- # Message
76
+ # Message data
57
77
  message = {
58
78
  "source": "Testnet" if is_testnet else "Mainnet",
59
79
  "hash": payload_hash,
60
80
  "txType": tx_type,
61
81
  }
62
82
 
63
- # Create structured data
64
- structured_data = {
65
- "types": types,
66
- "primaryType": "Action",
67
- "domain": domain,
68
- "message": message,
69
- }
83
+ if _USE_NEW_API:
84
+ # New API (eth-account >= 0.9): use 3-argument form
85
+ encoded_data = encode_typed_data(
86
+ domain_data=domain,
87
+ message_types=message_types,
88
+ message_data=message,
89
+ )
90
+ else:
91
+ # Legacy API (eth-account < 0.9): use full_message form
92
+ # Include EIP712Domain in types for legacy API
93
+ types_with_domain = {
94
+ "EIP712Domain": [
95
+ {"name": "name", "type": "string"},
96
+ {"name": "version", "type": "string"},
97
+ {"name": "chainId", "type": "uint256"},
98
+ {"name": "verifyingContract", "type": "address"},
99
+ ],
100
+ **message_types,
101
+ }
102
+ structured_data = {
103
+ "types": types_with_domain,
104
+ "primaryType": "Action",
105
+ "domain": domain,
106
+ "message": message,
107
+ }
108
+ encoded_data = encode_structured_data(structured_data)
70
109
 
71
- # Encode and sign
72
- encoded_data = encode_structured_data(structured_data)
73
110
  signed_message = wallet.sign_message(encoded_data)
74
111
 
75
112
  return signed_message.signature.hex()