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
hotstuff/__init__.py
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
"""Hotstuff Python SDK.
|
|
2
2
|
|
|
3
|
-
A Python SDK for interacting with Hotstuff
|
|
3
|
+
A Python SDK for interacting with Hotstuff L1.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
__version__ = "0.0.1-beta.
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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 =
|
|
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 =
|
|
484
|
+
signature = sign_action(
|
|
488
485
|
wallet=self.wallet,
|
|
489
486
|
action=params,
|
|
490
487
|
tx_type=EXCHANGE_OP_CODES[action],
|
|
491
|
-
is_testnet=
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
hotstuff/apis/subscription.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
15
|
+
Validate client order ID format.
|
|
10
16
|
|
|
11
17
|
Args:
|
|
12
|
-
value: The
|
|
18
|
+
value: The cloid string to validate
|
|
13
19
|
|
|
14
20
|
Returns:
|
|
15
|
-
|
|
21
|
+
The validated cloid string (lowercase hex)
|
|
16
22
|
|
|
17
23
|
Raises:
|
|
18
|
-
ValueError: If the
|
|
24
|
+
ValueError: If the cloid format is invalid
|
|
19
25
|
"""
|
|
20
|
-
if
|
|
21
|
-
|
|
26
|
+
if value is None or value == "":
|
|
27
|
+
return None
|
|
22
28
|
|
|
23
|
-
if not
|
|
24
|
-
raise ValueError(
|
|
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
|
|
27
|
-
return
|
|
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(
|
|
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
|
-
@
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|