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.
Files changed (150) hide show
  1. {tpluspy-0.3.2 → tpluspy-0.4.0}/PKG-INFO +47 -4
  2. {tpluspy-0.3.2 → tpluspy-0.4.0}/README.md +44 -3
  3. {tpluspy-0.3.2 → tpluspy-0.4.0}/setup.py +3 -1
  4. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/_context.py +1 -0
  5. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/markets.py +71 -18
  6. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/orders.py +1 -1
  7. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/vaults.py +77 -1
  8. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/api.py +9 -6
  9. tpluspy-0.4.0/tplus/client/auth.py +455 -0
  10. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/base.py +151 -10
  11. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/__init__.py +2 -2
  12. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/admin.py +30 -18
  13. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/cross_venue.py +3 -2
  14. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/market_data.py +102 -23
  15. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/orderbook.py +171 -35
  16. tpluspy-0.4.0/tplus/client/websocket.py +58 -0
  17. tpluspy-0.4.0/tplus/evm/constants.py +2 -0
  18. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/contracts.py +1 -1
  19. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/dev/env.py +23 -20
  20. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/chaindata.py +4 -3
  21. tpluspy-0.4.0/tplus/evm/managers/deposit.py +66 -0
  22. tpluspy-0.4.0/tplus/evm/managers/evm.py +93 -0
  23. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/settle.py +42 -17
  24. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/withdraw.py +18 -11
  25. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/exceptions.py +8 -1
  26. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/asset_identifier.py +5 -1
  27. tpluspy-0.4.0/tplus/model/auth.py +19 -0
  28. tpluspy-0.4.0/tplus/model/market.py +93 -0
  29. tpluspy-0.4.0/tplus/model/open_interest.py +27 -0
  30. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/order.py +62 -1
  31. tpluspy-0.4.0/tplus/model/position_basis.py +26 -0
  32. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/settlement.py +32 -15
  33. tpluspy-0.4.0/tplus/model/sub_account.py +16 -0
  34. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/trades.py +33 -1
  35. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_event.py +1 -1
  36. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/withdrawal.py +16 -12
  37. tpluspy-0.4.0/tplus/types.py +9 -0
  38. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/limit_order.py +7 -2
  39. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/market_order.py +7 -2
  40. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/replace_order.py +18 -4
  41. tpluspy-0.4.0/tplus/utils/serializers.py +39 -0
  42. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/signing.py +7 -2
  43. tpluspy-0.4.0/tplus/utils/user/__init__.py +128 -0
  44. tpluspy-0.4.0/tplus/utils/user/evm.py +145 -0
  45. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/manager.py +80 -2
  46. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/model.py +183 -9
  47. {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/PKG-INFO +47 -4
  48. {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/SOURCES.txt +6 -0
  49. {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/requires.txt +2 -0
  50. tpluspy-0.3.2/tplus/client/auth.py +0 -296
  51. tpluspy-0.3.2/tplus/evm/constants.py +0 -2
  52. tpluspy-0.3.2/tplus/evm/managers/deposit.py +0 -46
  53. tpluspy-0.3.2/tplus/evm/managers/evm.py +0 -12
  54. tpluspy-0.3.2/tplus/model/market.py +0 -17
  55. tpluspy-0.3.2/tplus/types.py +0 -4
  56. tpluspy-0.3.2/tplus/utils/serializers.py +0 -15
  57. tpluspy-0.3.2/tplus/utils/user/__init__.py +0 -32
  58. {tpluspy-0.3.2 → tpluspy-0.4.0}/pyproject.toml +0 -0
  59. {tpluspy-0.3.2 → tpluspy-0.4.0}/setup.cfg +0 -0
  60. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/__init__.py +0 -0
  61. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/__init__.py +0 -0
  62. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/__main__.py +0 -0
  63. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/accounts.py +0 -0
  64. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/assets.py +0 -0
  65. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/balance.py +0 -0
  66. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/debug.py +0 -0
  67. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/decimals.py +0 -0
  68. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/deposit.py +0 -0
  69. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/env.py +0 -0
  70. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/params.py +0 -0
  71. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/settle.py +0 -0
  72. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/sign.py +0 -0
  73. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/stream.py +0 -0
  74. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/trades.py +0 -0
  75. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/withdraw.py +0 -0
  76. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/_cli/withdrawal.py +0 -0
  77. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/asset_metadata.py +0 -0
  78. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/cli_tools/__init__.py +0 -0
  79. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/cli_tools/options.py +0 -0
  80. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/cli_tools/output.py +0 -0
  81. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/__init__.py +0 -0
  82. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/blockchain.py +0 -0
  83. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/admin_settlement.py +0 -0
  84. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/assetregistry.py +0 -0
  85. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/base.py +0 -0
  86. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/decimal.py +0 -0
  87. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/clearingengine/vault.py +0 -0
  88. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/oms/__init__.py +0 -0
  89. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/oms/assetregistry.py +0 -0
  90. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/oms/oms_admin.py +0 -0
  91. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/client/withdrawal.py +0 -0
  92. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/constants.py +0 -0
  93. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/__init__.py +0 -0
  94. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/abi.py +0 -0
  95. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/address.py +0 -0
  96. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/dev/__init__.py +0 -0
  97. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/dev/contracts.py +0 -0
  98. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/dev/token.py +0 -0
  99. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/exceptions.py +0 -0
  100. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/__init__.py +0 -0
  101. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/credential_manager.py +0 -0
  102. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/registry.py +0 -0
  103. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/managers/vault.py +0 -0
  104. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/evm/utils.py +0 -0
  105. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/logger.py +0 -0
  106. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/managers/__init__.py +0 -0
  107. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/managers/base.py +0 -0
  108. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/__init__.py +0 -0
  109. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/approval.py +0 -0
  110. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/batch_order.py +0 -0
  111. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/cancel_order.py +0 -0
  112. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/chain_address.py +0 -0
  113. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/close_all_positions_preview.py +0 -0
  114. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/config.py +0 -0
  115. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/interest_rates.py +0 -0
  116. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/klines.py +0 -0
  117. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/limit_order.py +0 -0
  118. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/market_order.py +0 -0
  119. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/multisig.py +0 -0
  120. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/order_id.py +0 -0
  121. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/order_trigger.py +0 -0
  122. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/orderbook.py +0 -0
  123. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/pagination.py +0 -0
  124. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/position.py +0 -0
  125. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/replace_order.py +0 -0
  126. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/risk_parameters.py +0 -0
  127. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/signed_message.py +0 -0
  128. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/types.py +0 -0
  129. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_inventory.py +0 -0
  130. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_margin.py +0 -0
  131. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_simulated_margin.py +0 -0
  132. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/model/user_solvency.py +0 -0
  133. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/py.typed +0 -0
  134. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/__init__.py +0 -0
  135. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/address.py +0 -0
  136. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/amount.py +0 -0
  137. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/bytes32.py +0 -0
  138. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/decimals.py +0 -0
  139. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/domain.py +0 -0
  140. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/hex.py +0 -0
  141. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/operator.py +0 -0
  142. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/timeout.py +0 -0
  143. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/decrypt.py +0 -0
  144. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/ed_keyfile.py +0 -0
  145. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/token_cache.py +0 -0
  146. {tpluspy-0.3.2 → tpluspy-0.4.0}/tplus/utils/user/validate.py +0 -0
  147. {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/dependency_links.txt +0 -0
  148. {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/entry_points.txt +0 -0
  149. {tpluspy-0.3.2 → tpluspy-0.4.0}/tpluspy.egg-info/not-zip-safe +0 -0
  150. {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.2
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, # Optional: New integer quantity
232
- new_price=1050, # Optional: New integer price
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, # Optional: New integer quantity
161
- new_price=1050, # Optional: New integer price
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.3.2",
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
  ],
@@ -73,6 +73,7 @@ class CLIContext(dict):
73
73
  "Pass --tplus-account, set TPLUS_ACCOUNT, "
74
74
  "or create one with `tplus accounts add`."
75
75
  )
76
+
76
77
  return self.user_manager.load(name)
77
78
 
78
79
  def _resolve_default_account(self) -> str | None:
@@ -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(cli_ctx: CLIContext, output_format: str, no_pager: bool):
65
- """List all markets."""
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
- response = cast("list[dict[str, Any]]", asyncio.run(client._request("GET", "/markets")))
91
+ markets = asyncio.run(_collect_markets(client, page_number, limit))
68
92
  if output_format == "raw":
69
- click.echo(json.dumps(response, indent=2, default=str))
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 response or []:
74
- fee_schedule = market.get("fee_schedule") or {}
75
- asset_metadata = asset_metadata_dict(market.get("asset_id")) or {}
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.get("asset_id"),
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.get("book_price_decimals"),
83
- "quantity_decimals": market.get("book_quantity_decimals"),
84
- "max_leverage": market.get("max_leverage"),
85
- "isolated_only": market.get("isolated_only"),
86
- "tick_size": market.get("tick_size"),
87
- "min_order_size": market.get("min_order_size"),
88
- "fee_account": fee_schedule.get("fee_account"),
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.get('global') or [])} global / "
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, signers=[signer], credential_manager=credman, clearing_engine=ce
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
- """Single-token client for the T+ API gateway.
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 one auth token, so a single nonce/token handshake authenticates
17
- every endpoint. This client owns the shared httpx connection and ``Auth``;
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(self)
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