tpluspy 0.3.2__tar.gz → 0.4.0__tar.gz
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.
- {tpluspy-0.3.2 → tpluspy-0.4.0}/PKG-INFO +47 -4
- {tpluspy-0.3.2 → tpluspy-0.4.0}/README.md +44 -3
- {tpluspy-0.3.2 → tpluspy-0.4.0}/setup.py +3 -1
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/_context.py +1 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/markets.py +71 -18
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/orders.py +1 -1
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/vaults.py +77 -1
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/api.py +9 -6
- tpluspy-0.4.0/tplus/client/auth.py +455 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/base.py +151 -10
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/__init__.py +2 -2
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/admin.py +30 -18
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/cross_venue.py +3 -2
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/market_data.py +102 -23
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/orderbook.py +171 -35
- tpluspy-0.4.0/tplus/client/websocket.py +58 -0
- tpluspy-0.4.0/tplus/evm/constants.py +2 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/contracts.py +1 -1
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/dev/env.py +23 -20
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/chaindata.py +4 -3
- tpluspy-0.4.0/tplus/evm/managers/deposit.py +66 -0
- tpluspy-0.4.0/tplus/evm/managers/evm.py +93 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/settle.py +42 -17
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/withdraw.py +18 -11
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/exceptions.py +8 -1
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/asset_identifier.py +5 -1
- tpluspy-0.4.0/tplus/model/auth.py +19 -0
- tpluspy-0.4.0/tplus/model/market.py +93 -0
- tpluspy-0.4.0/tplus/model/open_interest.py +27 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/order.py +62 -1
- tpluspy-0.4.0/tplus/model/position_basis.py +26 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/settlement.py +32 -15
- tpluspy-0.4.0/tplus/model/sub_account.py +16 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/trades.py +33 -1
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_event.py +1 -1
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/withdrawal.py +16 -12
- tpluspy-0.4.0/tplus/types.py +9 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/limit_order.py +7 -2
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/market_order.py +7 -2
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/replace_order.py +18 -4
- tpluspy-0.4.0/tplus/utils/serializers.py +39 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/signing.py +7 -2
- tpluspy-0.4.0/tplus/utils/user/__init__.py +128 -0
- tpluspy-0.4.0/tplus/utils/user/evm.py +145 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/manager.py +80 -2
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/model.py +183 -9
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/PKG-INFO +47 -4
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/SOURCES.txt +6 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/requires.txt +2 -0
- tpluspy-0.3.2/tplus/client/auth.py +0 -296
- tpluspy-0.3.2/tplus/evm/constants.py +0 -2
- tpluspy-0.3.2/tplus/evm/managers/deposit.py +0 -46
- tpluspy-0.3.2/tplus/evm/managers/evm.py +0 -12
- tpluspy-0.3.2/tplus/model/market.py +0 -17
- tpluspy-0.3.2/tplus/types.py +0 -4
- tpluspy-0.3.2/tplus/utils/serializers.py +0 -15
- tpluspy-0.3.2/tplus/utils/user/__init__.py +0 -32
- {tpluspy-0.3.2 → tpluspy-0.4.0}/pyproject.toml +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/setup.cfg +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/__main__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/accounts.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/assets.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/balance.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/debug.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/decimals.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/deposit.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/env.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/params.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/settle.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/sign.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/stream.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/trades.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/withdraw.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/withdrawal.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/asset_metadata.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/cli_tools/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/cli_tools/options.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/cli_tools/output.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/blockchain.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/admin_settlement.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/assetregistry.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/base.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/decimal.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/vault.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/oms/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/oms/assetregistry.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/oms/oms_admin.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/withdrawal.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/constants.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/abi.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/address.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/dev/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/dev/contracts.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/dev/token.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/exceptions.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/credential_manager.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/registry.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/vault.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/utils.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/logger.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/managers/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/managers/base.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/approval.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/batch_order.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/cancel_order.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/chain_address.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/close_all_positions_preview.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/config.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/interest_rates.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/klines.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/limit_order.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/market_order.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/multisig.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/order_id.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/order_trigger.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/orderbook.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/pagination.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/position.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/replace_order.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/risk_parameters.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/signed_message.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/types.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_inventory.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_margin.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_simulated_margin.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_solvency.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/py.typed +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/__init__.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/address.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/amount.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/bytes32.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/decimals.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/domain.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/hex.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/operator.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/timeout.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/decrypt.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/ed_keyfile.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/token_cache.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/validate.py +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/dependency_links.txt +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/entry_points.txt +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/not-zip-safe +0 -0
- {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tpluspy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: tpluspy: Client utilities for interacting with tplus
|
|
5
5
|
Home-page: https://github.com/tpluslabs/tpluspy
|
|
6
6
|
Author: TPlus Labs
|
|
@@ -29,6 +29,7 @@ Requires-Dist: pycryptodome>=3.17.1
|
|
|
29
29
|
Requires-Dist: pydantic<3,>=2.10.4
|
|
30
30
|
Requires-Dist: websockets<14,>=13.1
|
|
31
31
|
Provides-Extra: test
|
|
32
|
+
Requires-Dist: eth-account<0.14,>=0.13.0; extra == "test"
|
|
32
33
|
Requires-Dist: pytest>=8.0; extra == "test"
|
|
33
34
|
Requires-Dist: pytest-timeout<3,>=2.2.0; extra == "test"
|
|
34
35
|
Requires-Dist: pytest-mock; extra == "test"
|
|
@@ -47,6 +48,7 @@ Provides-Extra: evm
|
|
|
47
48
|
Requires-Dist: ape-tokens; extra == "evm"
|
|
48
49
|
Requires-Dist: click; extra == "evm"
|
|
49
50
|
Requires-Dist: eip712>=0.3.1; extra == "evm"
|
|
51
|
+
Requires-Dist: eth-account<0.14,>=0.13.0; extra == "evm"
|
|
50
52
|
Requires-Dist: eth-ape<0.9,>=0.8.49; extra == "evm"
|
|
51
53
|
Requires-Dist: hexbytes<2,>=1.3.1; extra == "evm"
|
|
52
54
|
Provides-Extra: docs
|
|
@@ -142,6 +144,41 @@ async def run_client():
|
|
|
142
144
|
asyncio.run(run_client())
|
|
143
145
|
```
|
|
144
146
|
|
|
147
|
+
#### Use EVM accounts as T+ accounts
|
|
148
|
+
|
|
149
|
+
An EVM account can back a T+ account: it signs one fixed message and the T+ identity is derived from that signature, so the same wallet always maps to the same T+ account (the one the T+ frontend derives on wallet login).
|
|
150
|
+
|
|
151
|
+
Pass the account itself anywhere a `User` is accepted, whether that is `default_user=`, a per-call `user=`, or a `signer=`:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from ape import accounts
|
|
155
|
+
|
|
156
|
+
account = accounts.load("me")
|
|
157
|
+
|
|
158
|
+
async with OrderBookClient(API_BASE_URL, default_user=account) as client:
|
|
159
|
+
await client.get_user_inventory() # signs as the T+ user behind the account
|
|
160
|
+
await client.create_limit_order(..., user=account) # per-call override
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
An `eth_account` signer works the same way, with no `[evm]` extra:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from eth_account import Account
|
|
167
|
+
|
|
168
|
+
client = OrderBookClient(API_BASE_URL, default_user=Account.from_key("0x..."))
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Use `load_user_from_ape_account` to hold the `User` itself, for example to read its public key or to pick a sub-account. `load_user` loads stored keyfiles by name, so Ape aliases and keyfile names occupy separate namespaces and cannot collide:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from tplus.utils.user import load_user_from_ape_account
|
|
175
|
+
|
|
176
|
+
user = load_user_from_ape_account("me") # by Ape alias (needs the `evm` extra)
|
|
177
|
+
user = load_user_from_ape_account(account) # an already-loaded Ape account
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The result is a normal T+ `User` and signs every request the same way.
|
|
181
|
+
|
|
145
182
|
#### REST API Usage
|
|
146
183
|
|
|
147
184
|
The client offers async methods for common REST endpoints:
|
|
@@ -225,13 +262,15 @@ print(f"Cancel Order Response: {cancel_response}")
|
|
|
225
262
|
# Replace an Order
|
|
226
263
|
# Original Order ID should be from an existing, open order.
|
|
227
264
|
original_order_id_to_replace = "actual-original-order-id" # Replace with a real order ID
|
|
228
|
-
replace_response = await client.replace_order(
|
|
265
|
+
replace_response, revision = await client.replace_order(
|
|
229
266
|
original_order_id=original_order_id_to_replace,
|
|
230
267
|
asset_id=example_asset,
|
|
231
|
-
new_quantity=6, #
|
|
232
|
-
new_price=1050, #
|
|
268
|
+
new_quantity=6, # Effective lifetime-total quantity
|
|
269
|
+
new_price=1050, # Effective limit price
|
|
233
270
|
)
|
|
234
271
|
print(f"Replace Order Response: {replace_response}")
|
|
272
|
+
# `revision` is the replacement's timestamp_ns; a later amend_order must quote it as
|
|
273
|
+
# expected_authorization_revision.
|
|
235
274
|
```
|
|
236
275
|
|
|
237
276
|
See `examples/rest_usage.py` for a runnable demonstration.
|
|
@@ -269,6 +308,10 @@ async for trade in client.stream_finalized_trades():
|
|
|
269
308
|
|
|
270
309
|
See `examples/websocket_usage.py` for a runnable demonstration using `asyncio.gather` to run multiple streams concurrently.
|
|
271
310
|
|
|
311
|
+
#### Token Recovery
|
|
312
|
+
|
|
313
|
+
When a bearer token stops working before it actually expires (e.g. from a service restart), both transports recover on their own. A REST response or WebSocket handshake rejected with `401` or `403` re-authenticates and retries once; a second rejection is raised to the caller. Requests rejected concurrently share a single re-authentication.
|
|
314
|
+
|
|
272
315
|
### Contracts
|
|
273
316
|
|
|
274
317
|
To interact with the contracts or sign T+ settlement messages, ensure you have installed the `evm` extra:
|
|
@@ -71,6 +71,41 @@ async def run_client():
|
|
|
71
71
|
asyncio.run(run_client())
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
#### Use EVM accounts as T+ accounts
|
|
75
|
+
|
|
76
|
+
An EVM account can back a T+ account: it signs one fixed message and the T+ identity is derived from that signature, so the same wallet always maps to the same T+ account (the one the T+ frontend derives on wallet login).
|
|
77
|
+
|
|
78
|
+
Pass the account itself anywhere a `User` is accepted, whether that is `default_user=`, a per-call `user=`, or a `signer=`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from ape import accounts
|
|
82
|
+
|
|
83
|
+
account = accounts.load("me")
|
|
84
|
+
|
|
85
|
+
async with OrderBookClient(API_BASE_URL, default_user=account) as client:
|
|
86
|
+
await client.get_user_inventory() # signs as the T+ user behind the account
|
|
87
|
+
await client.create_limit_order(..., user=account) # per-call override
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
An `eth_account` signer works the same way, with no `[evm]` extra:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from eth_account import Account
|
|
94
|
+
|
|
95
|
+
client = OrderBookClient(API_BASE_URL, default_user=Account.from_key("0x..."))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Use `load_user_from_ape_account` to hold the `User` itself, for example to read its public key or to pick a sub-account. `load_user` loads stored keyfiles by name, so Ape aliases and keyfile names occupy separate namespaces and cannot collide:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from tplus.utils.user import load_user_from_ape_account
|
|
102
|
+
|
|
103
|
+
user = load_user_from_ape_account("me") # by Ape alias (needs the `evm` extra)
|
|
104
|
+
user = load_user_from_ape_account(account) # an already-loaded Ape account
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The result is a normal T+ `User` and signs every request the same way.
|
|
108
|
+
|
|
74
109
|
#### REST API Usage
|
|
75
110
|
|
|
76
111
|
The client offers async methods for common REST endpoints:
|
|
@@ -154,13 +189,15 @@ print(f"Cancel Order Response: {cancel_response}")
|
|
|
154
189
|
# Replace an Order
|
|
155
190
|
# Original Order ID should be from an existing, open order.
|
|
156
191
|
original_order_id_to_replace = "actual-original-order-id" # Replace with a real order ID
|
|
157
|
-
replace_response = await client.replace_order(
|
|
192
|
+
replace_response, revision = await client.replace_order(
|
|
158
193
|
original_order_id=original_order_id_to_replace,
|
|
159
194
|
asset_id=example_asset,
|
|
160
|
-
new_quantity=6, #
|
|
161
|
-
new_price=1050, #
|
|
195
|
+
new_quantity=6, # Effective lifetime-total quantity
|
|
196
|
+
new_price=1050, # Effective limit price
|
|
162
197
|
)
|
|
163
198
|
print(f"Replace Order Response: {replace_response}")
|
|
199
|
+
# `revision` is the replacement's timestamp_ns; a later amend_order must quote it as
|
|
200
|
+
# expected_authorization_revision.
|
|
164
201
|
```
|
|
165
202
|
|
|
166
203
|
See `examples/rest_usage.py` for a runnable demonstration.
|
|
@@ -198,6 +235,10 @@ async for trade in client.stream_finalized_trades():
|
|
|
198
235
|
|
|
199
236
|
See `examples/websocket_usage.py` for a runnable demonstration using `asyncio.gather` to run multiple streams concurrently.
|
|
200
237
|
|
|
238
|
+
#### Token Recovery
|
|
239
|
+
|
|
240
|
+
When a bearer token stops working before it actually expires (e.g. from a service restart), both transports recover on their own. A REST response or WebSocket handshake rejected with `401` or `403` re-authenticates and retries once; a second rejection is raised to the caller. Requests rejected concurrently share a single re-authentication.
|
|
241
|
+
|
|
201
242
|
### Contracts
|
|
202
243
|
|
|
203
244
|
To interact with the contracts or sign T+ settlement messages, ensure you have installed the `evm` extra:
|
|
@@ -8,7 +8,7 @@ long_description = (here / "README.md").read_text(encoding="utf-8")
|
|
|
8
8
|
|
|
9
9
|
setup(
|
|
10
10
|
name="tpluspy",
|
|
11
|
-
version="0.
|
|
11
|
+
version="0.4.0",
|
|
12
12
|
description="""tpluspy: Client utilities for interacting with tplus""",
|
|
13
13
|
long_description=long_description,
|
|
14
14
|
long_description_content_type="text/markdown",
|
|
@@ -34,6 +34,7 @@ setup(
|
|
|
34
34
|
python_requires=">=3.10,<4",
|
|
35
35
|
extras_require={
|
|
36
36
|
"test": [
|
|
37
|
+
"eth-account>=0.13.0,<0.14",
|
|
37
38
|
"pytest>=8.0",
|
|
38
39
|
"pytest-timeout>=2.2.0,<3",
|
|
39
40
|
"pytest-mock",
|
|
@@ -55,6 +56,7 @@ setup(
|
|
|
55
56
|
"ape-tokens",
|
|
56
57
|
"click",
|
|
57
58
|
"eip712>=0.3.1",
|
|
59
|
+
"eth-account>=0.13.0,<0.14",
|
|
58
60
|
"eth-ape>=0.8.49,<0.9",
|
|
59
61
|
"hexbytes>=1.3.1,<2",
|
|
60
62
|
],
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
-
from typing import Any, cast
|
|
4
3
|
|
|
5
4
|
import click
|
|
6
5
|
|
|
@@ -19,6 +18,23 @@ from tplus.cli_tools import (
|
|
|
19
18
|
output_format_option,
|
|
20
19
|
render,
|
|
21
20
|
)
|
|
21
|
+
from tplus.model.market import MarketResponse
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def _collect_markets(
|
|
25
|
+
client, page_number: int | None, limit: int | None
|
|
26
|
+
) -> list[MarketResponse]:
|
|
27
|
+
"""The requested page, or every page when the caller did not pick one."""
|
|
28
|
+
page = await client.get_markets(page=page_number, limit=limit)
|
|
29
|
+
if page_number is not None:
|
|
30
|
+
return page.markets
|
|
31
|
+
|
|
32
|
+
markets = list(page.markets)
|
|
33
|
+
while page.next_page is not None:
|
|
34
|
+
page = await client.get_markets(page=page.next_page, limit=limit)
|
|
35
|
+
markets.extend(page.markets)
|
|
36
|
+
|
|
37
|
+
return markets
|
|
22
38
|
|
|
23
39
|
|
|
24
40
|
@click.group()
|
|
@@ -60,35 +76,43 @@ def _get(cli_ctx: CLIContext, asset_id: str):
|
|
|
60
76
|
@tplus_account_option()
|
|
61
77
|
@output_format_option()
|
|
62
78
|
@no_pager_option()
|
|
79
|
+
@click.option("--page", "page_number", type=int)
|
|
80
|
+
@click.option("--limit", type=int)
|
|
63
81
|
@pass_cli_context
|
|
64
|
-
def _list(
|
|
65
|
-
|
|
82
|
+
def _list(
|
|
83
|
+
cli_ctx: CLIContext,
|
|
84
|
+
output_format: str,
|
|
85
|
+
no_pager: bool,
|
|
86
|
+
page_number: int | None,
|
|
87
|
+
limit: int | None,
|
|
88
|
+
):
|
|
89
|
+
"""List all markets, following pagination unless `--page` picks one."""
|
|
66
90
|
client = cli_ctx.orderbook_client()
|
|
67
|
-
|
|
91
|
+
markets = asyncio.run(_collect_markets(client, page_number, limit))
|
|
68
92
|
if output_format == "raw":
|
|
69
|
-
|
|
93
|
+
raw = [market.model_dump(by_alias=True) for market in markets]
|
|
94
|
+
click.echo(json.dumps(raw, indent=2, default=str))
|
|
70
95
|
return
|
|
71
96
|
|
|
72
97
|
records = []
|
|
73
|
-
for market in
|
|
74
|
-
fee_schedule = market.
|
|
75
|
-
asset_metadata = asset_metadata_dict(market.
|
|
98
|
+
for market in markets:
|
|
99
|
+
fee_schedule = market.fee_schedule
|
|
100
|
+
asset_metadata = asset_metadata_dict(str(market.asset_id)) or {}
|
|
76
101
|
records.append(
|
|
77
102
|
{
|
|
78
|
-
"asset_id": market.
|
|
103
|
+
"asset_id": str(market.asset_id),
|
|
79
104
|
"symbol": asset_metadata.get("symbol"),
|
|
80
105
|
"asset_class": asset_metadata.get("asset_class"),
|
|
81
106
|
"representations": asset_metadata.get("representations"),
|
|
82
|
-
"price_decimals": market.
|
|
83
|
-
"quantity_decimals": market.
|
|
84
|
-
"max_leverage": market.
|
|
85
|
-
"isolated_only": market.
|
|
86
|
-
"tick_size": market.
|
|
87
|
-
"min_order_size": market.
|
|
88
|
-
"fee_account": fee_schedule.
|
|
107
|
+
"price_decimals": market.book_price_decimals,
|
|
108
|
+
"quantity_decimals": market.book_quantity_decimals,
|
|
109
|
+
"max_leverage": market.max_leverage,
|
|
110
|
+
"isolated_only": market.isolated_only,
|
|
111
|
+
"tick_size": market.tick_size,
|
|
112
|
+
"min_order_size": market.min_order_size,
|
|
113
|
+
"fee_account": fee_schedule.fee_account if fee_schedule else None,
|
|
89
114
|
"fee_tiers": (
|
|
90
|
-
f"{len(fee_schedule.
|
|
91
|
-
f"{len(fee_schedule.get('per_asset') or [])} per-asset"
|
|
115
|
+
f"{len(fee_schedule.global_)} global / {len(fee_schedule.per_asset)} per-asset"
|
|
92
116
|
if fee_schedule
|
|
93
117
|
else None
|
|
94
118
|
),
|
|
@@ -97,6 +121,35 @@ def _list(cli_ctx: CLIContext, output_format: str, no_pager: bool):
|
|
|
97
121
|
render(records, output_format, no_pager=no_pager)
|
|
98
122
|
|
|
99
123
|
|
|
124
|
+
@markets.command("symbol-map")
|
|
125
|
+
@orderbook_url_option()
|
|
126
|
+
@ignore_ssl_option()
|
|
127
|
+
@tplus_account_option()
|
|
128
|
+
@output_format_option()
|
|
129
|
+
@no_pager_option()
|
|
130
|
+
@pass_cli_context
|
|
131
|
+
def _symbol_map(cli_ctx: CLIContext, output_format: str, no_pager: bool):
|
|
132
|
+
"""Show the canonical asset table served by the OMS."""
|
|
133
|
+
client = cli_ctx.orderbook_client()
|
|
134
|
+
symbol_map = asyncio.run(client.get_markets(include_symbol_map=True)).symbol_map or {}
|
|
135
|
+
if output_format == "raw":
|
|
136
|
+
raw = {str(index): asset.model_dump() for index, asset in symbol_map.items()}
|
|
137
|
+
click.echo(json.dumps(raw, indent=2))
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
records = [
|
|
141
|
+
{
|
|
142
|
+
"index": index,
|
|
143
|
+
"symbol": asset.symbol,
|
|
144
|
+
"name": asset.name,
|
|
145
|
+
"asset_class": asset.asset_class,
|
|
146
|
+
"representations": ", ".join(asset.representations),
|
|
147
|
+
}
|
|
148
|
+
for index, asset in sorted(symbol_map.items())
|
|
149
|
+
]
|
|
150
|
+
render(records, output_format, no_pager=no_pager)
|
|
151
|
+
|
|
152
|
+
|
|
100
153
|
@markets.command("depth")
|
|
101
154
|
@market_data_url_option()
|
|
102
155
|
@ignore_ssl_option()
|
|
@@ -117,7 +117,7 @@ def _replace(
|
|
|
117
117
|
from tplus.model.asset_identifier import AssetIdentifier
|
|
118
118
|
|
|
119
119
|
client = cli_ctx.orderbook_client()
|
|
120
|
-
response = asyncio.run(
|
|
120
|
+
response, _revision = asyncio.run(
|
|
121
121
|
client.replace_order(
|
|
122
122
|
original_order_id=order_id,
|
|
123
123
|
asset_id=AssetIdentifier(asset_id),
|
|
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
_EVM_AVAILABLE = importlib.util.find_spec("ape") is not None
|
|
31
|
+
PROFILE_WINDOW_SECONDS = 7 * 24 * 60 * 60
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
@click.group()
|
|
@@ -180,6 +181,43 @@ if _EVM_AVAILABLE:
|
|
|
180
181
|
show_default=True,
|
|
181
182
|
help="Automata verifier contract address.",
|
|
182
183
|
)
|
|
184
|
+
@click.option(
|
|
185
|
+
"--attestation-test-mode",
|
|
186
|
+
is_flag=True,
|
|
187
|
+
help="Accept unattested credentials. Dev only.",
|
|
188
|
+
)
|
|
189
|
+
@click.option(
|
|
190
|
+
"--council-member",
|
|
191
|
+
"security_council",
|
|
192
|
+
multiple=True,
|
|
193
|
+
help="Security council member address. May be repeated. Required off dev networks.",
|
|
194
|
+
)
|
|
195
|
+
@click.option(
|
|
196
|
+
"--council-threshold",
|
|
197
|
+
type=int,
|
|
198
|
+
help="Security council threshold. Defaults to a strict majority of the council.",
|
|
199
|
+
)
|
|
200
|
+
@click.option(
|
|
201
|
+
"--manage-profile-window",
|
|
202
|
+
type=int,
|
|
203
|
+
default=PROFILE_WINDOW_SECONDS,
|
|
204
|
+
show_default=True,
|
|
205
|
+
help="Seconds a measurement-profile change stays pending.",
|
|
206
|
+
)
|
|
207
|
+
@click.option(
|
|
208
|
+
"--signer-rotation-window",
|
|
209
|
+
type=int,
|
|
210
|
+
default=PROFILE_WINDOW_SECONDS,
|
|
211
|
+
show_default=True,
|
|
212
|
+
help="Seconds a signer rotation stays pending.",
|
|
213
|
+
)
|
|
214
|
+
@click.option(
|
|
215
|
+
"--withdrawal-quorum",
|
|
216
|
+
type=int,
|
|
217
|
+
default=1,
|
|
218
|
+
show_default=True,
|
|
219
|
+
help="Approvals required to release a withdrawal.",
|
|
220
|
+
)
|
|
183
221
|
@click.option(
|
|
184
222
|
"--skip-if-deployed",
|
|
185
223
|
is_flag=True,
|
|
@@ -192,9 +230,17 @@ if _EVM_AVAILABLE:
|
|
|
192
230
|
quorum: int | None,
|
|
193
231
|
measurements: tuple[str, ...],
|
|
194
232
|
automata_verifier: str,
|
|
233
|
+
attestation_test_mode: bool,
|
|
234
|
+
security_council: tuple[str, ...],
|
|
235
|
+
council_threshold: int | None,
|
|
236
|
+
manage_profile_window: int,
|
|
237
|
+
signer_rotation_window: int,
|
|
238
|
+
withdrawal_quorum: int,
|
|
195
239
|
skip_if_deployed: bool,
|
|
196
240
|
):
|
|
197
241
|
"""Deploy a new CredentialManager pointing at REGISTRY_ADDRESS."""
|
|
242
|
+
from ape import chain
|
|
243
|
+
|
|
198
244
|
from tplus.evm.contracts import CredentialManager
|
|
199
245
|
|
|
200
246
|
if skip_if_deployed and (adopted := CredentialManager()._adopt_ce_deployment()):
|
|
@@ -204,6 +250,19 @@ if _EVM_AVAILABLE:
|
|
|
204
250
|
op_list = list(operators) or [account.address]
|
|
205
251
|
threshold = quorum if quorum is not None else len(op_list)
|
|
206
252
|
meas = [bytes.fromhex(m.removeprefix("0x")) for m in measurements]
|
|
253
|
+
|
|
254
|
+
council = list(security_council)
|
|
255
|
+
if not council:
|
|
256
|
+
# Defaulting the veto council to the operators would collapse the independent
|
|
257
|
+
# veto, so only a dev chain may fall back to it.
|
|
258
|
+
if not chain.provider.network.is_dev:
|
|
259
|
+
raise click.UsageError("--council-member is required off dev networks.")
|
|
260
|
+
|
|
261
|
+
council = op_list
|
|
262
|
+
|
|
263
|
+
# Strict majority, matching the contract's own 2k>n check.
|
|
264
|
+
council_threshold = council_threshold or (len(council) // 2 + 1)
|
|
265
|
+
|
|
207
266
|
instance = CredentialManager.deploy(
|
|
208
267
|
op_list,
|
|
209
268
|
threshold,
|
|
@@ -211,6 +270,12 @@ if _EVM_AVAILABLE:
|
|
|
211
270
|
registry_address,
|
|
212
271
|
meas,
|
|
213
272
|
automata_verifier,
|
|
273
|
+
attestation_test_mode,
|
|
274
|
+
council,
|
|
275
|
+
council_threshold,
|
|
276
|
+
manage_profile_window,
|
|
277
|
+
signer_rotation_window,
|
|
278
|
+
withdrawal_quorum,
|
|
214
279
|
sender=account,
|
|
215
280
|
)
|
|
216
281
|
click.echo(f"deployed: {instance.address}")
|
|
@@ -226,18 +291,29 @@ if _EVM_AVAILABLE:
|
|
|
226
291
|
except click.UsageError:
|
|
227
292
|
ce = None
|
|
228
293
|
|
|
294
|
+
# Waiting on vault registration reads the vault list back from the OMS.
|
|
295
|
+
try:
|
|
296
|
+
assets = cli_ctx.orderbook_client(anonymous=True).assets
|
|
297
|
+
except click.UsageError:
|
|
298
|
+
assets = None
|
|
299
|
+
|
|
229
300
|
credman = (
|
|
230
301
|
cast("CredentialManager", CredentialManager.at(credential_manager_address))
|
|
231
302
|
if credential_manager_address
|
|
232
303
|
else None
|
|
233
304
|
)
|
|
234
305
|
return CredentialManagerOwner(
|
|
235
|
-
admin=signer,
|
|
306
|
+
admin=signer,
|
|
307
|
+
signers=[signer],
|
|
308
|
+
credential_manager=credman,
|
|
309
|
+
clearing_engine=ce,
|
|
310
|
+
asset_registry_client=assets,
|
|
236
311
|
)
|
|
237
312
|
|
|
238
313
|
@vaults.command("register", cls=ConnectedProviderCommand)
|
|
239
314
|
@tplus_network_option()
|
|
240
315
|
@clearing_url_option()
|
|
316
|
+
@orderbook_url_option()
|
|
241
317
|
@ignore_ssl_option()
|
|
242
318
|
@credential_manager_address_option()
|
|
243
319
|
@tplus_account_option()
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from tplus.client.auth import AuthenticatedClient
|
|
5
|
+
from tplus.client.auth import Auth, AuthenticatedClient
|
|
6
6
|
from tplus.client.market_data import MarketDataClient
|
|
7
7
|
from tplus.client.orderbook import OrderBookClient
|
|
8
8
|
|
|
@@ -10,12 +10,11 @@ DEFAULT_BASE_URL = "http://localhost:8080"
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TplusApiClient(AuthenticatedClient):
|
|
13
|
-
"""
|
|
13
|
+
"""Client for the T+ API gateway.
|
|
14
14
|
|
|
15
15
|
The gateway is a reverse proxy exposing both the OMS and the MDS behind one
|
|
16
|
-
origin and
|
|
17
|
-
|
|
18
|
-
the two service namespaces are composed off it so they reuse both:
|
|
16
|
+
origin. OMS and MDS are separate token authorities, so the service namespaces
|
|
17
|
+
share one HTTP connection but maintain independent auth sessions:
|
|
19
18
|
|
|
20
19
|
- ``.oms`` — :class:`OrderBookClient` (orders, inventory, positions, margin).
|
|
21
20
|
- ``.mds`` — :class:`MarketDataClient` (klines, depth, tickers, user trades).
|
|
@@ -33,7 +32,11 @@ class TplusApiClient(AuthenticatedClient):
|
|
|
33
32
|
def __init__(self, base_url: str = DEFAULT_BASE_URL, **kwargs) -> None:
|
|
34
33
|
super().__init__(base_url, **kwargs)
|
|
35
34
|
self.oms = OrderBookClient.from_client(self)
|
|
36
|
-
self.mds = MarketDataClient.from_client(
|
|
35
|
+
self.mds = MarketDataClient.from_client(
|
|
36
|
+
self,
|
|
37
|
+
auth=Auth(cache_dir=self._auth.cache_dir),
|
|
38
|
+
auth_path_prefix="/market-data",
|
|
39
|
+
)
|
|
37
40
|
|
|
38
41
|
def __getattr__(self, name: str) -> Any:
|
|
39
42
|
# Only reached when normal lookup fails, so real inherited attributes
|