tpluspy 0.1.0a0__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.
- tplus/__init__.py +0 -0
- tplus/client/__init__.py +4 -0
- tplus/client/base.py +291 -0
- tplus/client/clearingengine/__init__.py +66 -0
- tplus/client/clearingengine/admin.py +30 -0
- tplus/client/clearingengine/assetregistry.py +37 -0
- tplus/client/clearingengine/base.py +5 -0
- tplus/client/clearingengine/decimal.py +47 -0
- tplus/client/clearingengine/deposit.py +19 -0
- tplus/client/clearingengine/settlement.py +84 -0
- tplus/client/clearingengine/vault.py +34 -0
- tplus/client/clearingengine/withdrawal.py +57 -0
- tplus/client/orderbook.py +395 -0
- tplus/client.py +437 -0
- tplus/constants.py +4 -0
- tplus/evm/__init__.py +1 -0
- tplus/evm/abi.py +21 -0
- tplus/evm/contracts.py +324 -0
- tplus/evm/eip712.py +16 -0
- tplus/evm/exceptions.py +4 -0
- tplus/evm/utils.py +0 -0
- tplus/logger.py +10 -0
- tplus/model/__init__.py +0 -0
- tplus/model/asset_identifier.py +124 -0
- tplus/model/cancel_order.py +20 -0
- tplus/model/klines.py +43 -0
- tplus/model/limit_order.py +74 -0
- tplus/model/market.py +17 -0
- tplus/model/market_order.py +60 -0
- tplus/model/order.py +219 -0
- tplus/model/order_trigger.py +25 -0
- tplus/model/orderbook.py +43 -0
- tplus/model/replace_order.py +49 -0
- tplus/model/settlement.py +200 -0
- tplus/model/signed_message.py +38 -0
- tplus/model/trades.py +158 -0
- tplus/model/types.py +87 -0
- tplus/model/withdrawal.py +57 -0
- tplus/py.typed +0 -0
- tplus/utils/__init__.py +0 -0
- tplus/utils/bytes32.py +23 -0
- tplus/utils/decimals.py +45 -0
- tplus/utils/hex.py +78 -0
- tplus/utils/limit_order.py +54 -0
- tplus/utils/market_order.py +50 -0
- tplus/utils/replace_order.py +58 -0
- tplus/utils/serializers.py +15 -0
- tplus/utils/signing.py +30 -0
- tplus/utils/user/__init__.py +10 -0
- tplus/utils/user/ed_keyfile.py +97 -0
- tplus/utils/user/manager.py +86 -0
- tplus/utils/user/model.py +45 -0
- tplus/utils/user/validate.py +9 -0
- tplus/utils/user.py +32 -0
- tplus/version.py +16 -0
- tpluspy-0.1.0a0.dist-info/METADATA +266 -0
- tpluspy-0.1.0a0.dist-info/RECORD +59 -0
- tpluspy-0.1.0a0.dist-info/WHEEL +5 -0
- tpluspy-0.1.0a0.dist-info/top_level.txt +2 -0
tplus/__init__.py
ADDED
|
File without changes
|
tplus/client/__init__.py
ADDED
tplus/client/base.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import ssl
|
|
4
|
+
from collections.abc import AsyncIterator, Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import websockets
|
|
10
|
+
|
|
11
|
+
from tplus.logger import get_logger
|
|
12
|
+
from tplus.utils.user import User
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseClient:
|
|
16
|
+
"""
|
|
17
|
+
Base client to use across T+ services.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
DEFAULT_TIMEOUT = 10.0
|
|
21
|
+
AUTH = True
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
user: User,
|
|
26
|
+
base_url: str,
|
|
27
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
28
|
+
client: httpx.Client | None = None,
|
|
29
|
+
websocket_kwargs: dict[str, Any] | None = None,
|
|
30
|
+
log_level: int = logging.INFO,
|
|
31
|
+
):
|
|
32
|
+
self.user = user
|
|
33
|
+
self.base_url = base_url.rstrip("/")
|
|
34
|
+
self._parsed_base_url = urlparse(self.base_url)
|
|
35
|
+
self._client = client or httpx.AsyncClient(
|
|
36
|
+
base_url=self.base_url,
|
|
37
|
+
timeout=timeout,
|
|
38
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
39
|
+
verify=False, # TODO remove that for production
|
|
40
|
+
)
|
|
41
|
+
self._ws_kwargs: dict[str, Any] = websocket_kwargs or {}
|
|
42
|
+
|
|
43
|
+
import asyncio
|
|
44
|
+
|
|
45
|
+
self._auth_lock: asyncio.Lock = asyncio.Lock()
|
|
46
|
+
self._auth_token: str | None = None
|
|
47
|
+
self._auth_expiry_ns: int = 0
|
|
48
|
+
self.logger = get_logger(log_level=log_level)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_client(cls, client: "BaseClient") -> "BaseClient":
|
|
52
|
+
"""
|
|
53
|
+
Easy way to clone clients without initializing multiple AsyncClients.
|
|
54
|
+
"""
|
|
55
|
+
return cls(client.user, client.base_url, client=client._client)
|
|
56
|
+
|
|
57
|
+
async def _get(self, endpoint: str, json_data: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
58
|
+
return await self._request("GET", endpoint, json_data=json_data)
|
|
59
|
+
|
|
60
|
+
async def _post(self, endpoint: str, json_data: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
61
|
+
return await self._request("POST", endpoint, json_data=json_data)
|
|
62
|
+
|
|
63
|
+
async def _request(
|
|
64
|
+
self, method: str, endpoint: str, json_data: dict[str, Any] | None = None
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
relative_url = endpoint if endpoint.startswith("/") else f"/{endpoint}"
|
|
67
|
+
|
|
68
|
+
if self.AUTH and (
|
|
69
|
+
not relative_url.startswith("/nonce") and not relative_url.startswith("/auth")
|
|
70
|
+
):
|
|
71
|
+
await self._ensure_auth()
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
if json_data:
|
|
75
|
+
self.logger.debug(f"Request to {method} {relative_url} with payload: {json_data}")
|
|
76
|
+
request_headers = self._get_auth_headers()
|
|
77
|
+
if request_headers:
|
|
78
|
+
merged_headers = {**self._client.headers, **request_headers}
|
|
79
|
+
else:
|
|
80
|
+
merged_headers = None
|
|
81
|
+
|
|
82
|
+
response = await self._client.request(
|
|
83
|
+
method=method,
|
|
84
|
+
url=relative_url,
|
|
85
|
+
json=json_data,
|
|
86
|
+
headers=merged_headers,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# If we receive an HTTP 401/403, the auth token may have expired. Refresh the
|
|
90
|
+
# credentials **once** and retry the request automatically. This keeps the
|
|
91
|
+
# higher-level client APIs unaware of token lifetimes and greatly simplifies
|
|
92
|
+
# consumer code.
|
|
93
|
+
if response.status_code in {401, 403} and not relative_url.startswith("/auth"):
|
|
94
|
+
self.logger.info(
|
|
95
|
+
"Received %s for %s – refreshing auth token and retrying once.",
|
|
96
|
+
response.status_code,
|
|
97
|
+
relative_url,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Force re-authentication and rebuild the auth headers (inside the same
|
|
101
|
+
# lock to avoid a thundering herd when many coroutines hit expiry at the
|
|
102
|
+
# same time).
|
|
103
|
+
await self._authenticate()
|
|
104
|
+
retry_headers = {**self._client.headers, **self._get_auth_headers()}
|
|
105
|
+
|
|
106
|
+
response = await self._client.request(
|
|
107
|
+
method=method,
|
|
108
|
+
url=relative_url,
|
|
109
|
+
json=json_data,
|
|
110
|
+
headers=retry_headers,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if response.status_code == 204:
|
|
114
|
+
return {}
|
|
115
|
+
if not response.content:
|
|
116
|
+
return {}
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
json_response = response.json()
|
|
120
|
+
if json_response is None:
|
|
121
|
+
self.logger.warning(
|
|
122
|
+
f"API endpoint {response.request.url!r} returned JSON null. Treating as empty dictionary."
|
|
123
|
+
)
|
|
124
|
+
return {}
|
|
125
|
+
|
|
126
|
+
return json_response
|
|
127
|
+
|
|
128
|
+
except Exception:
|
|
129
|
+
raise Exception(
|
|
130
|
+
f"Invalid response from server - status_code={response.status_code}."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
except httpx.TimeoutException as e:
|
|
134
|
+
self.logger.error(f"Request timed out to {e.request.url!r}: {e}")
|
|
135
|
+
raise
|
|
136
|
+
except httpx.RequestError as e:
|
|
137
|
+
self.logger.error(
|
|
138
|
+
f"An error occurred while requesting {e.request.url!r}: {type(e).__name__} - {e}"
|
|
139
|
+
)
|
|
140
|
+
raise
|
|
141
|
+
except httpx.HTTPStatusError as e:
|
|
142
|
+
self.logger.error(
|
|
143
|
+
f"HTTP error {e.response.status_code} while requesting {e.request.url!r}: {e.response.text}"
|
|
144
|
+
)
|
|
145
|
+
raise
|
|
146
|
+
except json.JSONDecodeError as e:
|
|
147
|
+
self.logger.error(
|
|
148
|
+
f"Failed to decode JSON response from {response.request.url!r}. Status: {response.status_code}. Content: {response.text[:100]}..."
|
|
149
|
+
)
|
|
150
|
+
raise ValueError(f"Invalid JSON received from API: {e}") from e
|
|
151
|
+
|
|
152
|
+
async def _ensure_auth(self) -> None:
|
|
153
|
+
import time
|
|
154
|
+
|
|
155
|
+
safety_margin_ns = 60 * 1_000_000_000
|
|
156
|
+
if self._auth_token and (time.time_ns() + safety_margin_ns) < self._auth_expiry_ns:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
async with self._auth_lock:
|
|
160
|
+
if self._auth_token and (time.time_ns() + safety_margin_ns) < self._auth_expiry_ns:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
await self._authenticate()
|
|
164
|
+
|
|
165
|
+
def _get_auth_headers(self) -> dict[str, str]:
|
|
166
|
+
if not self._auth_token:
|
|
167
|
+
return {}
|
|
168
|
+
return {
|
|
169
|
+
"Authorization": f"Bearer {self._auth_token}",
|
|
170
|
+
"User-Id": self.user.public_key,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async def _authenticate(self) -> None:
|
|
174
|
+
nonce_endpoint = f"/nonce/{self.user.public_key}"
|
|
175
|
+
nonce_resp = await self._client.get(nonce_endpoint)
|
|
176
|
+
nonce_resp.raise_for_status()
|
|
177
|
+
nonce_data = nonce_resp.json() if hasattr(nonce_resp, "json") else nonce_resp
|
|
178
|
+
nonce_value = nonce_data["value"] if isinstance(nonce_data, dict) else nonce_data
|
|
179
|
+
|
|
180
|
+
signature_bytes = self.user.sign(nonce_value)
|
|
181
|
+
signature_array = list(signature_bytes)
|
|
182
|
+
|
|
183
|
+
self.logger.debug(f"AUTH DEBUG: nonce={nonce_value} (len={len(nonce_value)})")
|
|
184
|
+
self.logger.debug(
|
|
185
|
+
f"AUTH DEBUG: signature={signature_array[:8]}... (len={len(signature_array)})"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
auth_payload = {
|
|
189
|
+
"user_id": self.user.public_key,
|
|
190
|
+
"nonce": nonce_value,
|
|
191
|
+
"signature": signature_array,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
token_resp = await self._client.post("/auth", json=auth_payload)
|
|
195
|
+
token_resp.raise_for_status()
|
|
196
|
+
token_json = token_resp.json() if hasattr(token_resp, "json") else token_resp
|
|
197
|
+
|
|
198
|
+
self.logger.info(f"Full authentication response from server: {token_json}")
|
|
199
|
+
|
|
200
|
+
self.logger.debug(
|
|
201
|
+
f"AUTH DEBUG: token={token_json.get('token')} expires={token_json.get('expiry_ns')}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
self._auth_token = token_json["token"]
|
|
205
|
+
self._auth_expiry_ns = int(token_json["expiry_ns"])
|
|
206
|
+
|
|
207
|
+
async def _ws_auth_headers(self) -> dict[str, str]:
|
|
208
|
+
if self.AUTH:
|
|
209
|
+
await self._ensure_auth()
|
|
210
|
+
|
|
211
|
+
return self._get_auth_headers()
|
|
212
|
+
|
|
213
|
+
def _get_websocket_url(self, path: str) -> str:
|
|
214
|
+
from urllib.parse import urlunparse
|
|
215
|
+
|
|
216
|
+
scheme = "wss" if self._parsed_base_url.scheme == "https" else "ws"
|
|
217
|
+
netloc = self._parsed_base_url.netloc
|
|
218
|
+
ws_path = path if path.startswith("/") else f"/{path}"
|
|
219
|
+
return urlunparse((scheme, netloc, ws_path, "", "", ""))
|
|
220
|
+
|
|
221
|
+
CONTROL_MESSAGE_TYPES: set[str] = {
|
|
222
|
+
"subscriptions",
|
|
223
|
+
"ping",
|
|
224
|
+
"pong",
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async def _stream_ws(
|
|
228
|
+
self,
|
|
229
|
+
path: str,
|
|
230
|
+
parser: Callable[[Any], Any],
|
|
231
|
+
*,
|
|
232
|
+
control_handler: Callable[[dict[str, Any]], None] | None = None,
|
|
233
|
+
) -> AsyncIterator[Any]:
|
|
234
|
+
ws_url = self._get_websocket_url(path)
|
|
235
|
+
self.logger.debug("Connecting to %s stream: %s", path, ws_url)
|
|
236
|
+
auth_headers = await self._ws_auth_headers()
|
|
237
|
+
ws_kwargs = dict(self._ws_kwargs)
|
|
238
|
+
if "extra_headers" in ws_kwargs and ws_kwargs["extra_headers"]:
|
|
239
|
+
caller_headers = ws_kwargs.pop("extra_headers")
|
|
240
|
+
if isinstance(caller_headers, dict):
|
|
241
|
+
caller_headers.update(auth_headers)
|
|
242
|
+
ws_kwargs["extra_headers"] = caller_headers
|
|
243
|
+
else:
|
|
244
|
+
ws_kwargs["extra_headers"] = list(auth_headers.items()) + list(caller_headers)
|
|
245
|
+
else:
|
|
246
|
+
ws_kwargs["extra_headers"] = auth_headers
|
|
247
|
+
|
|
248
|
+
# TODO Remove this when we have a real SSL certificate (or make it configurable)
|
|
249
|
+
ssl_context = ssl.create_default_context()
|
|
250
|
+
ssl_context.check_hostname = False
|
|
251
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
252
|
+
|
|
253
|
+
async with websockets.connect(ws_url, **ws_kwargs, ssl=ssl_context) as websocket:
|
|
254
|
+
async for message in websocket:
|
|
255
|
+
try:
|
|
256
|
+
data = json.loads(message)
|
|
257
|
+
if isinstance(data, dict) and data.get("type") in self.CONTROL_MESSAGE_TYPES:
|
|
258
|
+
if control_handler is not None:
|
|
259
|
+
control_handler(data)
|
|
260
|
+
continue
|
|
261
|
+
yield parser(data)
|
|
262
|
+
except json.JSONDecodeError:
|
|
263
|
+
self.logger.warning(
|
|
264
|
+
"Received non-JSON message on %s stream: %s…", path, message[:100]
|
|
265
|
+
)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
self.logger.error(
|
|
268
|
+
"Error processing message from %s stream: %s. Message: %s…",
|
|
269
|
+
path,
|
|
270
|
+
e,
|
|
271
|
+
message[:100],
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
async def close(self) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Closes the underlying httpx async client.
|
|
277
|
+
"""
|
|
278
|
+
self.logger.debug("Closing async HTTP client.")
|
|
279
|
+
await self._client.aclose()
|
|
280
|
+
|
|
281
|
+
async def __aenter__(self):
|
|
282
|
+
"""
|
|
283
|
+
Async context manager entry.
|
|
284
|
+
"""
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
288
|
+
"""
|
|
289
|
+
Async context manager exit.
|
|
290
|
+
"""
|
|
291
|
+
await self.close()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
|
|
3
|
+
from tplus.client.clearingengine.admin import AdminClient
|
|
4
|
+
from tplus.client.clearingengine.assetregistry import AssetRegistryClient
|
|
5
|
+
from tplus.client.clearingengine.base import BaseClearingEngineClient
|
|
6
|
+
from tplus.client.clearingengine.decimal import DecimalClient
|
|
7
|
+
from tplus.client.clearingengine.deposit import DepositClient
|
|
8
|
+
from tplus.client.clearingengine.settlement import SettlementClient
|
|
9
|
+
from tplus.client.clearingengine.vault import VaultClient
|
|
10
|
+
from tplus.client.clearingengine.withdrawal import WithdrawalClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClearingEngineClient(BaseClearingEngineClient):
|
|
14
|
+
"""
|
|
15
|
+
APIs targeting the clearing engine ("CE") directly. Most of the APIs are
|
|
16
|
+
permission-less; however some require signing, such as settlements and withdrawal flows.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@cached_property
|
|
20
|
+
def settlements(self) -> SettlementClient:
|
|
21
|
+
"""
|
|
22
|
+
APIs related to settlements.
|
|
23
|
+
"""
|
|
24
|
+
return SettlementClient.from_client(self)
|
|
25
|
+
|
|
26
|
+
@cached_property
|
|
27
|
+
def assets(self) -> AssetRegistryClient:
|
|
28
|
+
"""
|
|
29
|
+
APIs related to registered assets.
|
|
30
|
+
"""
|
|
31
|
+
return AssetRegistryClient.from_client(self)
|
|
32
|
+
|
|
33
|
+
@cached_property
|
|
34
|
+
def decimals(self) -> DecimalClient:
|
|
35
|
+
"""
|
|
36
|
+
APIs related to decimals.
|
|
37
|
+
"""
|
|
38
|
+
return DecimalClient.from_client(self)
|
|
39
|
+
|
|
40
|
+
@cached_property
|
|
41
|
+
def deposits(self) -> DepositClient:
|
|
42
|
+
"""
|
|
43
|
+
APIs related to deposits.
|
|
44
|
+
"""
|
|
45
|
+
return DepositClient.from_client(self)
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def withdrawals(self) -> WithdrawalClient:
|
|
49
|
+
"""
|
|
50
|
+
APIs related to withdrawals.
|
|
51
|
+
"""
|
|
52
|
+
return WithdrawalClient.from_client(self)
|
|
53
|
+
|
|
54
|
+
@cached_property
|
|
55
|
+
def vaults(self) -> VaultClient:
|
|
56
|
+
"""
|
|
57
|
+
APIs related to vaults.
|
|
58
|
+
"""
|
|
59
|
+
return VaultClient.from_client(self)
|
|
60
|
+
|
|
61
|
+
@cached_property
|
|
62
|
+
def admin(self) -> AdminClient:
|
|
63
|
+
"""
|
|
64
|
+
APIs related to the admin clearing-engine.
|
|
65
|
+
"""
|
|
66
|
+
return AdminClient.from_client(self)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from tplus.client.clearingengine.base import BaseClearingEngineClient
|
|
2
|
+
from tplus.model.asset_identifier import AssetIdentifier
|
|
3
|
+
from tplus.model.types import UserPublicKey
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AdminClient(BaseClearingEngineClient):
|
|
7
|
+
async def get_verifying_key(self):
|
|
8
|
+
"""
|
|
9
|
+
Get a clearing-engine's verifying key.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
str | None
|
|
13
|
+
"""
|
|
14
|
+
return await self._get("admin/verifying-key")
|
|
15
|
+
|
|
16
|
+
async def modify_user_inventory(
|
|
17
|
+
self, user: "UserPublicKey", asset: "AssetIdentifier", balance: dict
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Admin-only API for testing.
|
|
21
|
+
"""
|
|
22
|
+
if not isinstance(user, UserPublicKey):
|
|
23
|
+
user = UserPublicKey.__validate_user__(user)
|
|
24
|
+
if not isinstance(asset, AssetIdentifier):
|
|
25
|
+
asset = AssetIdentifier.model_validate(asset)
|
|
26
|
+
|
|
27
|
+
asset = asset.model_dump()
|
|
28
|
+
await self._post(
|
|
29
|
+
"admin/inventory/modify", json_data={"user": user, "asset": asset, "balance": balance}
|
|
30
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from tplus.client.clearingengine.base import BaseClearingEngineClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AssetRegistryClient(BaseClearingEngineClient):
|
|
5
|
+
"""
|
|
6
|
+
Clearing engine APIs related to assets.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
async def get(self) -> dict:
|
|
10
|
+
"""
|
|
11
|
+
Get all registered assets in the CE.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
dict: A mapping of stringified asset index (base 10) to chain ID to asset information.
|
|
15
|
+
"""
|
|
16
|
+
return await self._get("assets")
|
|
17
|
+
|
|
18
|
+
async def get_risk_parameters(self):
|
|
19
|
+
"""
|
|
20
|
+
Get all registered risk parameters in the CE.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
dict: A mapping of asset identifiers to their respective risk parameters.
|
|
24
|
+
"""
|
|
25
|
+
return await self._get("params")
|
|
26
|
+
|
|
27
|
+
async def update(self):
|
|
28
|
+
"""
|
|
29
|
+
Request that the clearing engine updates its registered assets for the given registry chain.
|
|
30
|
+
"""
|
|
31
|
+
await self._post("assets/update")
|
|
32
|
+
|
|
33
|
+
async def update_risk_parameters(self):
|
|
34
|
+
"""
|
|
35
|
+
Request that the clearing engine updates its registered risk parameters.
|
|
36
|
+
"""
|
|
37
|
+
await self._post("params/update")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from tplus.client.clearingengine.base import BaseClearingEngineClient
|
|
2
|
+
from tplus.model.asset_identifier import AssetIdentifier
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _prep_request(
|
|
6
|
+
asset_ids: list[str | AssetIdentifier] | str | AssetIdentifier, chains: list[int] | int
|
|
7
|
+
) -> dict:
|
|
8
|
+
if not isinstance(asset_ids, list):
|
|
9
|
+
asset_ids = [asset_ids]
|
|
10
|
+
if not isinstance(chains, list):
|
|
11
|
+
chains = [chains]
|
|
12
|
+
|
|
13
|
+
assets = []
|
|
14
|
+
for asset in asset_ids:
|
|
15
|
+
if not isinstance(asset, AssetIdentifier):
|
|
16
|
+
asset = AssetIdentifier.model_validate(asset)
|
|
17
|
+
|
|
18
|
+
assets.append(asset.model_dump())
|
|
19
|
+
|
|
20
|
+
return {"assets": assets, "chains": chains}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DecimalClient(BaseClearingEngineClient):
|
|
24
|
+
"""
|
|
25
|
+
APIs related to decimals.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
async def get(self, asset_id: list[str | AssetIdentifier], chains: list[int]) -> dict:
|
|
29
|
+
"""
|
|
30
|
+
Get CE cached decimals for the given assets and chains.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
asset_id (list[str | AssetIdentifier]): Asset identifiers.
|
|
34
|
+
chains (list[int]): Chains identifiers.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A mapping of asset Ids => chains => decimals.
|
|
38
|
+
"""
|
|
39
|
+
request = _prep_request(asset_id, chains)
|
|
40
|
+
return await self._get("decimals", json_data=request)
|
|
41
|
+
|
|
42
|
+
async def update(self, asset_id: list[str | AssetIdentifier], chains: list[int]):
|
|
43
|
+
"""
|
|
44
|
+
Request that the CE update cache decimals for the given assets and chains.
|
|
45
|
+
"""
|
|
46
|
+
request = _prep_request(asset_id, chains)
|
|
47
|
+
await self._post("decimals/update", json_data=request)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from tplus.client.clearingengine.base import BaseClearingEngineClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DepositClient(BaseClearingEngineClient):
|
|
5
|
+
"""
|
|
6
|
+
APIs related to deposits.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
async def update(self, user: str, chain_id: int):
|
|
10
|
+
"""
|
|
11
|
+
Request that the CE check the deposit vault for new deposits for
|
|
12
|
+
the given user.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
user (str): The user pubkey key ID.
|
|
16
|
+
chain_id (int): The chain ID to check.
|
|
17
|
+
"""
|
|
18
|
+
request = {"user": user, "chain_id": chain_id}
|
|
19
|
+
await self._post("deposits/update", json_data=request)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from tplus.client.clearingengine.base import BaseClearingEngineClient
|
|
2
|
+
from tplus.model.asset_identifier import ChainAddress
|
|
3
|
+
from tplus.model.settlement import BatchSettlementRequest, TxSettlementRequest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SettlementClient(BaseClearingEngineClient):
|
|
7
|
+
"""
|
|
8
|
+
Clearing engine APIs related to settlements.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
async def init_settlement(self, request: dict | TxSettlementRequest):
|
|
12
|
+
"""
|
|
13
|
+
Initialize a transaction (atomic) based settlement. This begins the process
|
|
14
|
+
of settling. Use ``get_signatures()`` to retrieve successful signatures.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
request (dict | TxSettlementRequest): transaction request.
|
|
18
|
+
"""
|
|
19
|
+
if isinstance(request, dict):
|
|
20
|
+
# Validate.
|
|
21
|
+
request = TxSettlementRequest.model_validate(request)
|
|
22
|
+
|
|
23
|
+
data = request.model_dump(mode="json")
|
|
24
|
+
await self._post("settlement/init", json_data=data)
|
|
25
|
+
|
|
26
|
+
async def get_signatures(self, user: str) -> dict:
|
|
27
|
+
"""
|
|
28
|
+
Get CE approved signatures for the given user for settlement. This happens
|
|
29
|
+
after settlement initialization.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
user (str): The settler.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A list of signatures (rust int arrays).
|
|
36
|
+
"""
|
|
37
|
+
return await self._get(f"settlement/signatures/{user}")
|
|
38
|
+
|
|
39
|
+
async def init_batch_settlement(self, request: dict | BatchSettlementRequest):
|
|
40
|
+
"""
|
|
41
|
+
Initialize a bundle-based settlement.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
request (dict | BatchSettlementRequest): The transaction request.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(request, dict):
|
|
47
|
+
# Validate.
|
|
48
|
+
request = BatchSettlementRequest.model_validate(request)
|
|
49
|
+
|
|
50
|
+
json_data = request.model_dump(mode="json")
|
|
51
|
+
await self._post("settlement/batch", json_data=json_data)
|
|
52
|
+
|
|
53
|
+
async def update(self, user: str, chain_id: int):
|
|
54
|
+
"""
|
|
55
|
+
Request that the CE check the deposit vault for new settlements for
|
|
56
|
+
the given user.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
user (str): The user pubkey key ID.
|
|
60
|
+
chain_id (int): The chain ID to check.
|
|
61
|
+
"""
|
|
62
|
+
request = {"user": user, "chain_id": chain_id}
|
|
63
|
+
await self._post("settlement/update", json_data=request)
|
|
64
|
+
|
|
65
|
+
async def update_approved_settlers(self, chain_id: int, vault_address: str):
|
|
66
|
+
"""
|
|
67
|
+
Request that the CE check the deposit vault for new approved settlers.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
chain_id (int): The chain ID to check.
|
|
71
|
+
vault_address (str): The vault address to check.
|
|
72
|
+
"""
|
|
73
|
+
request = ChainAddress(f"{vault_address}@{chain_id}")
|
|
74
|
+
json_data = request.model_dump(mode="json")
|
|
75
|
+
await self._post("settlers/update", json_data=json_data)
|
|
76
|
+
|
|
77
|
+
async def get_approved_settlers(self, chain_id: int) -> list[str]:
|
|
78
|
+
"""
|
|
79
|
+
Request that the CE check the deposit vault for new approved settlers.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
chain_id (int): The chain ID to check.
|
|
83
|
+
"""
|
|
84
|
+
return await self._get(f"settlers/{chain_id}")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from tplus.client.clearingengine.base import BaseClearingEngineClient
|
|
2
|
+
from tplus.model.asset_identifier import AssetIdentifier, ChainAddress
|
|
3
|
+
from tplus.model.types import ChainID
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VaultClient(BaseClearingEngineClient):
|
|
7
|
+
"""
|
|
8
|
+
APIs related to vaults.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
async def update(self):
|
|
12
|
+
"""
|
|
13
|
+
Request that the CE check the registry contract for new registered vaults.
|
|
14
|
+
"""
|
|
15
|
+
await self._post("vaults/update")
|
|
16
|
+
|
|
17
|
+
async def update_balance(self, asset_id: AssetIdentifier | str, chain_id: ChainID):
|
|
18
|
+
"""
|
|
19
|
+
Request that the CE check the deposit vault for new deposits for
|
|
20
|
+
the given user.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
asset_id (AssetIdentifier | str): The asset identifier.
|
|
24
|
+
chain_id (:class:`~tplus.models.types.ChainID`): The chain ID to check.
|
|
25
|
+
"""
|
|
26
|
+
request = {"asset_id": asset_id, "chain_id": chain_id}
|
|
27
|
+
await self._post("vault/balance/update", json_data=request)
|
|
28
|
+
|
|
29
|
+
async def get(self) -> list[ChainAddress]:
|
|
30
|
+
"""
|
|
31
|
+
Get all registered vaults.
|
|
32
|
+
"""
|
|
33
|
+
result = await self._get("vaults") or []
|
|
34
|
+
return [ChainAddress.model_validate(a) for a in result]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from tplus.client.clearingengine.base import BaseClearingEngineClient
|
|
2
|
+
from tplus.model.withdrawal import WithdrawalRequest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WithdrawalClient(BaseClearingEngineClient):
|
|
6
|
+
"""
|
|
7
|
+
APIs related to withdrawal.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
async def init_withdrawal(self, withdrawal: dict | WithdrawalRequest):
|
|
11
|
+
"""
|
|
12
|
+
Begin the steps of initializing a withdrawal. Once successful, can
|
|
13
|
+
use ``.get_signatures()`` to fetch the resulting signatures for
|
|
14
|
+
completing the withdrawal.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
withdrawal (dict | WithdrawalRequest): The withdrawal data
|
|
18
|
+
"""
|
|
19
|
+
if isinstance(withdrawal, dict):
|
|
20
|
+
# Validate.
|
|
21
|
+
withdrawal = WithdrawalRequest.model_validate(withdrawal)
|
|
22
|
+
|
|
23
|
+
json_data = withdrawal.model_dump(mode="json")
|
|
24
|
+
await self._post("withdrawal/init", json_data=json_data)
|
|
25
|
+
|
|
26
|
+
async def get_signatures(self, user: str) -> dict:
|
|
27
|
+
"""
|
|
28
|
+
Get CE approved signatures for the given user for withdrawal. This happens
|
|
29
|
+
after withdrawal initialization.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
user (str): The user withdrawing.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A list of signatures (rust int arrays).
|
|
36
|
+
"""
|
|
37
|
+
return await self._get(f"withdrawal/signatures/{user}")
|
|
38
|
+
|
|
39
|
+
async def update(self, user: str, chain_id: int):
|
|
40
|
+
"""
|
|
41
|
+
Request the CE check for new completed deposits for the given user on
|
|
42
|
+
the given chain.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
user (str): The user withdrawing.
|
|
46
|
+
chain_id (int): The chain to request withdrawals for.
|
|
47
|
+
"""
|
|
48
|
+
await self._post("withdrawal/update", json_data={"user": user, "chain_id": chain_id})
|
|
49
|
+
|
|
50
|
+
async def get_queued(self, user: str) -> list[WithdrawalRequest]:
|
|
51
|
+
"""
|
|
52
|
+
Get a user's queued withdrawals.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
user (str): The user withdrawing.
|
|
56
|
+
"""
|
|
57
|
+
return await self._get(f"withdrawal/queue/{user}")
|