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 +74 -4
- hotstuff/apis/exchange.py +4 -7
- hotstuff/apis/info.py +7 -7
- hotstuff/apis/subscription.py +9 -22
- hotstuff/exceptions.py +72 -0
- hotstuff/methods/exchange/account.py +1 -23
- hotstuff/methods/exchange/collateral.py +2 -24
- hotstuff/methods/exchange/trading.py +47 -16
- hotstuff/methods/exchange/vault.py +2 -24
- hotstuff/methods/info/account.py +54 -32
- hotstuff/methods/info/global.py +65 -249
- hotstuff/methods/info/market.py +256 -0
- hotstuff/methods/subscription/channels.py +200 -0
- hotstuff/methods/subscription/global.py +55 -200
- hotstuff/transports/http.py +36 -6
- hotstuff/utils/__init__.py +2 -0
- hotstuff/utils/signing.py +42 -21
- {hotstuff_python_sdk-0.0.1b1.dist-info → hotstuff_python_sdk-0.0.1b3.dist-info}/METADATA +566 -160
- hotstuff_python_sdk-0.0.1b3.dist-info/RECORD +38 -0
- hotstuff_python_sdk-0.0.1b1.dist-info/RECORD +0 -35
- {hotstuff_python_sdk-0.0.1b1.dist-info → hotstuff_python_sdk-0.0.1b3.dist-info}/LICENSE +0 -0
- {hotstuff_python_sdk-0.0.1b1.dist-info → hotstuff_python_sdk-0.0.1b3.dist-info}/WHEEL +0 -0
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
]
|
hotstuff/transports/http.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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."""
|
hotstuff/utils/__init__.py
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
from hotstuff.utils.endpoints import ENDPOINTS_URLS
|
|
3
3
|
from hotstuff.utils.nonce import NonceManager
|
|
4
4
|
from hotstuff.utils.signing import sign_action
|
|
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
|
+
"validate_ethereum_address",
|
|
11
13
|
"EXCHANGE_OP_CODES",
|
|
12
14
|
]
|
|
13
15
|
|
hotstuff/utils/signing.py
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
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
|
-
|
|
15
|
+
|
|
16
|
+
def sign_action(
|
|
10
17
|
wallet: Account,
|
|
11
18
|
action: dict,
|
|
12
19
|
tx_type: int,
|
|
@@ -15,6 +22,9 @@ async def sign_action(
|
|
|
15
22
|
"""
|
|
16
23
|
Sign an action using EIP-712.
|
|
17
24
|
|
|
25
|
+
This is a synchronous function that generates an EIP-712 signature
|
|
26
|
+
for the given action data.
|
|
27
|
+
|
|
18
28
|
Args:
|
|
19
29
|
wallet: The account to sign with
|
|
20
30
|
action: The action data
|
|
@@ -22,7 +32,7 @@ async def sign_action(
|
|
|
22
32
|
is_testnet: Whether this is for testnet
|
|
23
33
|
|
|
24
34
|
Returns:
|
|
25
|
-
str: The signature
|
|
35
|
+
str: The signature (hex string)
|
|
26
36
|
"""
|
|
27
37
|
# Encode action to msgpack
|
|
28
38
|
action_bytes = msgpack.packb(action)
|
|
@@ -38,14 +48,8 @@ async def sign_action(
|
|
|
38
48
|
"verifyingContract": "0x1234567890123456789012345678901234567890",
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
# EIP-712 types
|
|
42
|
-
|
|
43
|
-
"EIP712Domain": [
|
|
44
|
-
{"name": "name", "type": "string"},
|
|
45
|
-
{"name": "version", "type": "string"},
|
|
46
|
-
{"name": "chainId", "type": "uint256"},
|
|
47
|
-
{"name": "verifyingContract", "type": "address"},
|
|
48
|
-
],
|
|
51
|
+
# EIP-712 message types (without EIP712Domain for new API)
|
|
52
|
+
message_types = {
|
|
49
53
|
"Action": [
|
|
50
54
|
{"name": "source", "type": "string"},
|
|
51
55
|
{"name": "hash", "type": "bytes32"},
|
|
@@ -53,23 +57,40 @@ async def sign_action(
|
|
|
53
57
|
],
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
# Message
|
|
60
|
+
# Message data
|
|
57
61
|
message = {
|
|
58
62
|
"source": "Testnet" if is_testnet else "Mainnet",
|
|
59
63
|
"hash": payload_hash,
|
|
60
64
|
"txType": tx_type,
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
if _USE_NEW_API:
|
|
68
|
+
# New API (eth-account >= 0.9): use 3-argument form
|
|
69
|
+
encoded_data = encode_typed_data(
|
|
70
|
+
domain_data=domain,
|
|
71
|
+
message_types=message_types,
|
|
72
|
+
message_data=message,
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
# Legacy API (eth-account < 0.9): use full_message form
|
|
76
|
+
# Include EIP712Domain in types for legacy API
|
|
77
|
+
types_with_domain = {
|
|
78
|
+
"EIP712Domain": [
|
|
79
|
+
{"name": "name", "type": "string"},
|
|
80
|
+
{"name": "version", "type": "string"},
|
|
81
|
+
{"name": "chainId", "type": "uint256"},
|
|
82
|
+
{"name": "verifyingContract", "type": "address"},
|
|
83
|
+
],
|
|
84
|
+
**message_types,
|
|
85
|
+
}
|
|
86
|
+
structured_data = {
|
|
87
|
+
"types": types_with_domain,
|
|
88
|
+
"primaryType": "Action",
|
|
89
|
+
"domain": domain,
|
|
90
|
+
"message": message,
|
|
91
|
+
}
|
|
92
|
+
encoded_data = encode_structured_data(structured_data)
|
|
70
93
|
|
|
71
|
-
# Encode and sign
|
|
72
|
-
encoded_data = encode_structured_data(structured_data)
|
|
73
94
|
signed_message = wallet.sign_message(encoded_data)
|
|
74
95
|
|
|
75
96
|
return signed_message.signature.hex()
|