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.
Files changed (59) hide show
  1. tplus/__init__.py +0 -0
  2. tplus/client/__init__.py +4 -0
  3. tplus/client/base.py +291 -0
  4. tplus/client/clearingengine/__init__.py +66 -0
  5. tplus/client/clearingengine/admin.py +30 -0
  6. tplus/client/clearingengine/assetregistry.py +37 -0
  7. tplus/client/clearingengine/base.py +5 -0
  8. tplus/client/clearingengine/decimal.py +47 -0
  9. tplus/client/clearingengine/deposit.py +19 -0
  10. tplus/client/clearingengine/settlement.py +84 -0
  11. tplus/client/clearingengine/vault.py +34 -0
  12. tplus/client/clearingengine/withdrawal.py +57 -0
  13. tplus/client/orderbook.py +395 -0
  14. tplus/client.py +437 -0
  15. tplus/constants.py +4 -0
  16. tplus/evm/__init__.py +1 -0
  17. tplus/evm/abi.py +21 -0
  18. tplus/evm/contracts.py +324 -0
  19. tplus/evm/eip712.py +16 -0
  20. tplus/evm/exceptions.py +4 -0
  21. tplus/evm/utils.py +0 -0
  22. tplus/logger.py +10 -0
  23. tplus/model/__init__.py +0 -0
  24. tplus/model/asset_identifier.py +124 -0
  25. tplus/model/cancel_order.py +20 -0
  26. tplus/model/klines.py +43 -0
  27. tplus/model/limit_order.py +74 -0
  28. tplus/model/market.py +17 -0
  29. tplus/model/market_order.py +60 -0
  30. tplus/model/order.py +219 -0
  31. tplus/model/order_trigger.py +25 -0
  32. tplus/model/orderbook.py +43 -0
  33. tplus/model/replace_order.py +49 -0
  34. tplus/model/settlement.py +200 -0
  35. tplus/model/signed_message.py +38 -0
  36. tplus/model/trades.py +158 -0
  37. tplus/model/types.py +87 -0
  38. tplus/model/withdrawal.py +57 -0
  39. tplus/py.typed +0 -0
  40. tplus/utils/__init__.py +0 -0
  41. tplus/utils/bytes32.py +23 -0
  42. tplus/utils/decimals.py +45 -0
  43. tplus/utils/hex.py +78 -0
  44. tplus/utils/limit_order.py +54 -0
  45. tplus/utils/market_order.py +50 -0
  46. tplus/utils/replace_order.py +58 -0
  47. tplus/utils/serializers.py +15 -0
  48. tplus/utils/signing.py +30 -0
  49. tplus/utils/user/__init__.py +10 -0
  50. tplus/utils/user/ed_keyfile.py +97 -0
  51. tplus/utils/user/manager.py +86 -0
  52. tplus/utils/user/model.py +45 -0
  53. tplus/utils/user/validate.py +9 -0
  54. tplus/utils/user.py +32 -0
  55. tplus/version.py +16 -0
  56. tpluspy-0.1.0a0.dist-info/METADATA +266 -0
  57. tpluspy-0.1.0a0.dist-info/RECORD +59 -0
  58. tpluspy-0.1.0a0.dist-info/WHEEL +5 -0
  59. tpluspy-0.1.0a0.dist-info/top_level.txt +2 -0
tplus/__init__.py ADDED
File without changes
@@ -0,0 +1,4 @@
1
+ from .clearingengine import ClearingEngineClient
2
+ from .orderbook import OrderBookClient
3
+
4
+ __all__ = ("ClearingEngineClient", "OrderBookClient")
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,5 @@
1
+ from tplus.client.base import BaseClient
2
+
3
+
4
+ class BaseClearingEngineClient(BaseClient):
5
+ AUTH = False
@@ -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}")