tpluspy 0.3.2__tar.gz → 0.5.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 (162) hide show
  1. {tpluspy-0.3.2 → tpluspy-0.5.0}/PKG-INFO +95 -50
  2. {tpluspy-0.3.2 → tpluspy-0.5.0}/README.md +84 -47
  3. {tpluspy-0.3.2 → tpluspy-0.5.0}/setup.py +16 -4
  4. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/_context.py +1 -0
  5. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/markets.py +71 -18
  6. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/orders.py +1 -1
  7. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/settle.py +1 -1
  8. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/vaults.py +77 -1
  9. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/withdraw.py +1 -1
  10. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/api.py +9 -6
  11. tpluspy-0.5.0/tplus/client/auth.py +455 -0
  12. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/base.py +151 -10
  13. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/clearingengine/__init__.py +2 -2
  14. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/clearingengine/admin.py +59 -24
  15. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/clearingengine/cross_venue.py +3 -2
  16. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/market_data.py +149 -30
  17. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/oms/assetregistry.py +15 -0
  18. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/oms/oms_admin.py +6 -0
  19. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/orderbook.py +411 -100
  20. tpluspy-0.5.0/tplus/client/websocket.py +58 -0
  21. tpluspy-0.5.0/tplus/evm/__init__.py +19 -0
  22. tpluspy-0.5.0/tplus/evm/_manifest.py +45 -0
  23. tpluspy-0.5.0/tplus/evm/abi.py +117 -0
  24. tpluspy-0.5.0/tplus/evm/backends/__init__.py +125 -0
  25. tpluspy-0.5.0/tplus/evm/backends/_ape_project.py +78 -0
  26. tpluspy-0.5.0/tplus/evm/backends/ape.py +224 -0
  27. tpluspy-0.5.0/tplus/evm/backends/base.py +196 -0
  28. tpluspy-0.5.0/tplus/evm/backends/web3.py +476 -0
  29. tpluspy-0.5.0/tplus/evm/constants.py +2 -0
  30. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/contracts.py +217 -249
  31. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/dev/env.py +23 -20
  32. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/exceptions.py +20 -0
  33. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/managers/chaindata.py +9 -4
  34. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/managers/credential_manager.py +31 -22
  35. tpluspy-0.5.0/tplus/evm/managers/deposit.py +60 -0
  36. tpluspy-0.5.0/tplus/evm/managers/evm.py +103 -0
  37. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/managers/registry.py +13 -12
  38. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/managers/settle.py +82 -32
  39. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/managers/vault.py +21 -31
  40. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/managers/withdraw.py +29 -27
  41. tpluspy-0.5.0/tplus/evm/manifests/tplus-contracts.json +1 -0
  42. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/exceptions.py +8 -1
  43. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/asset_identifier.py +5 -1
  44. tpluspy-0.5.0/tplus/model/auth.py +19 -0
  45. tpluspy-0.5.0/tplus/model/batch_order.py +63 -0
  46. tpluspy-0.5.0/tplus/model/control.py +49 -0
  47. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/klines.py +3 -1
  48. tpluspy-0.5.0/tplus/model/market.py +93 -0
  49. tpluspy-0.5.0/tplus/model/open_interest.py +27 -0
  50. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/order.py +62 -1
  51. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/position.py +10 -0
  52. tpluspy-0.5.0/tplus/model/position_basis.py +26 -0
  53. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/settlement.py +32 -15
  54. tpluspy-0.5.0/tplus/model/sub_account.py +16 -0
  55. tpluspy-0.5.0/tplus/model/ticker.py +51 -0
  56. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/trades.py +33 -1
  57. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/user_event.py +1 -1
  58. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/user_margin.py +38 -2
  59. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/withdrawal.py +16 -12
  60. tpluspy-0.5.0/tplus/types.py +9 -0
  61. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/limit_order.py +7 -2
  62. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/market_order.py +7 -2
  63. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/replace_order.py +18 -4
  64. tpluspy-0.5.0/tplus/utils/serializers.py +39 -0
  65. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/signing.py +7 -2
  66. tpluspy-0.5.0/tplus/utils/user/__init__.py +128 -0
  67. tpluspy-0.5.0/tplus/utils/user/evm.py +145 -0
  68. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/user/manager.py +80 -2
  69. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/user/model.py +183 -9
  70. {tpluspy-0.3.2 → tpluspy-0.5.0}/tpluspy.egg-info/PKG-INFO +95 -50
  71. {tpluspy-0.3.2 → tpluspy-0.5.0}/tpluspy.egg-info/SOURCES.txt +15 -0
  72. {tpluspy-0.3.2 → tpluspy-0.5.0}/tpluspy.egg-info/requires.txt +11 -2
  73. tpluspy-0.3.2/tplus/client/auth.py +0 -296
  74. tpluspy-0.3.2/tplus/evm/__init__.py +0 -1
  75. tpluspy-0.3.2/tplus/evm/abi.py +0 -21
  76. tpluspy-0.3.2/tplus/evm/constants.py +0 -2
  77. tpluspy-0.3.2/tplus/evm/managers/deposit.py +0 -46
  78. tpluspy-0.3.2/tplus/evm/managers/evm.py +0 -12
  79. tpluspy-0.3.2/tplus/model/batch_order.py +0 -31
  80. tpluspy-0.3.2/tplus/model/market.py +0 -17
  81. tpluspy-0.3.2/tplus/types.py +0 -4
  82. tpluspy-0.3.2/tplus/utils/serializers.py +0 -15
  83. tpluspy-0.3.2/tplus/utils/user/__init__.py +0 -32
  84. {tpluspy-0.3.2 → tpluspy-0.5.0}/pyproject.toml +0 -0
  85. {tpluspy-0.3.2 → tpluspy-0.5.0}/setup.cfg +0 -0
  86. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/__init__.py +0 -0
  87. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/__init__.py +0 -0
  88. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/__main__.py +0 -0
  89. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/accounts.py +0 -0
  90. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/assets.py +0 -0
  91. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/balance.py +0 -0
  92. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/debug.py +0 -0
  93. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/decimals.py +0 -0
  94. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/deposit.py +0 -0
  95. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/env.py +0 -0
  96. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/params.py +0 -0
  97. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/sign.py +0 -0
  98. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/stream.py +0 -0
  99. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/trades.py +0 -0
  100. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/_cli/withdrawal.py +0 -0
  101. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/asset_metadata.py +0 -0
  102. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/cli_tools/__init__.py +0 -0
  103. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/cli_tools/options.py +0 -0
  104. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/cli_tools/output.py +0 -0
  105. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/__init__.py +0 -0
  106. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/blockchain.py +0 -0
  107. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/clearingengine/admin_settlement.py +0 -0
  108. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/clearingengine/assetregistry.py +0 -0
  109. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/clearingengine/base.py +0 -0
  110. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/clearingengine/decimal.py +0 -0
  111. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/clearingengine/vault.py +0 -0
  112. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/oms/__init__.py +0 -0
  113. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/client/withdrawal.py +0 -0
  114. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/constants.py +0 -0
  115. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/address.py +0 -0
  116. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/dev/__init__.py +0 -0
  117. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/dev/contracts.py +0 -0
  118. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/dev/token.py +0 -0
  119. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/managers/__init__.py +0 -0
  120. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/evm/utils.py +0 -0
  121. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/logger.py +0 -0
  122. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/managers/__init__.py +0 -0
  123. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/managers/base.py +0 -0
  124. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/__init__.py +0 -0
  125. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/approval.py +0 -0
  126. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/cancel_order.py +0 -0
  127. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/chain_address.py +0 -0
  128. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/close_all_positions_preview.py +0 -0
  129. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/config.py +0 -0
  130. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/interest_rates.py +0 -0
  131. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/limit_order.py +0 -0
  132. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/market_order.py +0 -0
  133. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/multisig.py +0 -0
  134. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/order_id.py +0 -0
  135. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/order_trigger.py +0 -0
  136. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/orderbook.py +0 -0
  137. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/pagination.py +0 -0
  138. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/replace_order.py +0 -0
  139. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/risk_parameters.py +0 -0
  140. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/signed_message.py +0 -0
  141. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/types.py +0 -0
  142. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/user_inventory.py +0 -0
  143. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/user_simulated_margin.py +0 -0
  144. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/model/user_solvency.py +0 -0
  145. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/py.typed +0 -0
  146. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/__init__.py +0 -0
  147. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/address.py +0 -0
  148. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/amount.py +0 -0
  149. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/bytes32.py +0 -0
  150. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/decimals.py +0 -0
  151. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/domain.py +0 -0
  152. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/hex.py +0 -0
  153. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/operator.py +0 -0
  154. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/timeout.py +0 -0
  155. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/user/decrypt.py +0 -0
  156. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/user/ed_keyfile.py +0 -0
  157. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/user/token_cache.py +0 -0
  158. {tpluspy-0.3.2 → tpluspy-0.5.0}/tplus/utils/user/validate.py +0 -0
  159. {tpluspy-0.3.2 → tpluspy-0.5.0}/tpluspy.egg-info/dependency_links.txt +0 -0
  160. {tpluspy-0.3.2 → tpluspy-0.5.0}/tpluspy.egg-info/entry_points.txt +0 -0
  161. {tpluspy-0.3.2 → tpluspy-0.5.0}/tpluspy.egg-info/not-zip-safe +0 -0
  162. {tpluspy-0.3.2 → tpluspy-0.5.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.5.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"
@@ -36,6 +37,7 @@ Requires-Dist: pytest-asyncio; extra == "test"
36
37
  Requires-Dist: anyio>=4; extra == "test"
37
38
  Requires-Dist: pytest-anyio>=0.0.0; extra == "test"
38
39
  Requires-Dist: trio>=0.24; extra == "test"
40
+ Requires-Dist: eth-tester[py-evm]>=0.12.0b1; extra == "test"
39
41
  Provides-Extra: lint
40
42
  Requires-Dist: ruff<0.16,>=0.11.7; extra == "lint"
41
43
  Requires-Dist: mypy<2,>=1.18.2; extra == "lint"
@@ -44,11 +46,17 @@ Requires-Dist: setuptools>=75.6.0; extra == "release"
44
46
  Requires-Dist: wheel; extra == "release"
45
47
  Requires-Dist: twine; extra == "release"
46
48
  Provides-Extra: evm
47
- Requires-Dist: ape-tokens; extra == "evm"
48
49
  Requires-Dist: click; extra == "evm"
49
50
  Requires-Dist: eip712>=0.3.1; extra == "evm"
50
- Requires-Dist: eth-ape<0.9,>=0.8.49; extra == "evm"
51
+ Requires-Dist: eth-account<0.14,>=0.13.0; extra == "evm"
52
+ Requires-Dist: eth-keys<0.8,>=0.6; extra == "evm"
51
53
  Requires-Dist: hexbytes<2,>=1.3.1; extra == "evm"
54
+ Requires-Dist: pyyaml<7,>=6; extra == "evm"
55
+ Requires-Dist: web3<8,>=7.0; extra == "evm"
56
+ Provides-Extra: evm-ape
57
+ Requires-Dist: tpluspy[evm]; extra == "evm-ape"
58
+ Requires-Dist: ape-tokens; extra == "evm-ape"
59
+ Requires-Dist: eth-ape<0.9,>=0.8.49; extra == "evm-ape"
52
60
  Provides-Extra: docs
53
61
  Requires-Dist: Sphinx<9,>=7.0; extra == "docs"
54
62
  Requires-Dist: myst-parser<5,>=2.0; extra == "docs"
@@ -142,6 +150,41 @@ async def run_client():
142
150
  asyncio.run(run_client())
143
151
  ```
144
152
 
153
+ #### Use EVM accounts as T+ accounts
154
+
155
+ 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).
156
+
157
+ Pass the account itself anywhere a `User` is accepted, whether that is `default_user=`, a per-call `user=`, or a `signer=`:
158
+
159
+ ```python
160
+ from ape import accounts
161
+
162
+ account = accounts.load("me")
163
+
164
+ async with OrderBookClient(API_BASE_URL, default_user=account) as client:
165
+ await client.get_user_inventory() # signs as the T+ user behind the account
166
+ await client.create_limit_order(..., user=account) # per-call override
167
+ ```
168
+
169
+ An `eth_account` signer works the same way, with no `[evm]` extra:
170
+
171
+ ```python
172
+ from eth_account import Account
173
+
174
+ client = OrderBookClient(API_BASE_URL, default_user=Account.from_key("0x..."))
175
+ ```
176
+
177
+ 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:
178
+
179
+ ```python
180
+ from tplus.utils.user import load_user_from_ape_account
181
+
182
+ user = load_user_from_ape_account("me") # by Ape alias (needs the `evm` extra)
183
+ user = load_user_from_ape_account(account) # an already-loaded Ape account
184
+ ```
185
+
186
+ The result is a normal T+ `User` and signs every request the same way.
187
+
145
188
  #### REST API Usage
146
189
 
147
190
  The client offers async methods for common REST endpoints:
@@ -225,13 +268,28 @@ print(f"Cancel Order Response: {cancel_response}")
225
268
  # Replace an Order
226
269
  # Original Order ID should be from an existing, open order.
227
270
  original_order_id_to_replace = "actual-original-order-id" # Replace with a real order ID
228
- replace_response = await client.replace_order(
271
+ replace_response, revision = await client.replace_order(
229
272
  original_order_id=original_order_id_to_replace,
230
273
  asset_id=example_asset,
231
- new_quantity=6, # Optional: New integer quantity
232
- new_price=1050, # Optional: New integer price
274
+ new_quantity=6, # Effective lifetime-total quantity
275
+ new_price=1050, # Effective limit price
233
276
  )
234
277
  print(f"Replace Order Response: {replace_response}")
278
+ # `revision` is the replacement's timestamp_ns; a later amend_order must quote it as
279
+ # expected_authorization_revision.
280
+
281
+ # Replace several open orders in one request (up to 50). Each item is signed on its own;
282
+ # the OMS sends one message per market and returns one status per item, in order.
283
+ batch_replace_response = await client.replace_multiple_orders(
284
+ [
285
+ await client.prepare_replace_order_request(
286
+ original_order_id=oid, asset_id=example_asset, new_quantity=6, new_price=price
287
+ )
288
+ for oid, price in [("order-a", 1050), ("order-b", 1040)]
289
+ ]
290
+ )
291
+ for status in batch_replace_response.batch_order_status:
292
+ print(f"{status.order_id}: {status.status} {status.reason or ''}")
235
293
  ```
236
294
 
237
295
  See `examples/rest_usage.py` for a runnable demonstration.
@@ -269,68 +327,55 @@ async for trade in client.stream_finalized_trades():
269
327
 
270
328
  See `examples/websocket_usage.py` for a runnable demonstration using `asyncio.gather` to run multiple streams concurrently.
271
329
 
330
+ #### Token Recovery
331
+
332
+ 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.
333
+
272
334
  ### Contracts
273
335
 
274
- To interact with the contracts or sign T+ settlement messages, ensure you have installed the `evm` extra:
336
+ To interact with the on-chain T+ contracts (or sign T+ settlement messages),
337
+ install one of the EVM extras:
275
338
 
276
339
  ```shell
277
- pip install tpluspy[evm]
340
+ pip install "tpluspy[evm]" # web3.py backend (no Ape)
341
+ pip install "tpluspy[evm-ape]" # adds the Ape backend on top of [evm]
278
342
  ```
279
343
 
280
- Use the `tplusp.contracts` module to read data from t+ contracts.
281
- For example, launch a Sepolia-connected Ape console:
344
+ The Python API is identical either way. With the **web3.py** backend, point it
345
+ at a node via `WEB3_PROVIDER_URI` (read by `web3.auto`) — or call
346
+ `tplus.evm.use_web3(rpc_url=...)`:
282
347
 
283
348
  ```shell
284
- ape console --network ethereum:sepolia:alchemy
349
+ export WEB3_PROVIDER_URI="https://arb1.arbitrum.io/rpc"
350
+ ```
351
+
352
+ ```python
353
+ from tplus.evm.contracts import registry, vault
354
+
355
+ registry.admin()
356
+ vault.getApprovedSettlers()
285
357
  ```
286
358
 
287
- **Note**: You can use any provider you want or a RPC directly, it doesn't have to be Alchemy.
359
+ With the **Ape** backend, just use `ape console` / `ape run` as usual; it's
360
+ auto-detected whenever Ape is connected to a network:
288
361
 
289
- Then, once in the console, you will already have access to contracts that you can call methods on:
362
+ ```shell
363
+ ape console --network ethereum:sepolia:alchemy
364
+ ```
290
365
 
291
366
  ```python
292
- In[1]: registry.getAssets()
293
- Out[1]: [
367
+ In [1]: from tplus.evm.contracts import registry
368
+ In [2]: registry.getAssets()
369
+ Out[2]: [
294
370
  getAssets_return(
295
371
  assetAddress=HexBytes("0x000000000000000000000000f08a50178dfcde18524640ea6618a1f965821715"),
296
372
  chainId=11155111,
297
373
  maxDeposits=100,
298
374
  )
299
375
  ]
300
- In[2]: registry.admin()
301
- Out[2]: "0x467a95fC5359edE5d5dDc4f10A1F4B680694858E"
376
+ In [3]: registry.admin()
377
+ Out[3]: '0x467a95fC5359edE5d5dDc4f10A1F4B680694858E'
302
378
  ```
303
379
 
304
- #### Settlement signatures
305
-
306
- Sign settlement messages using the structured `Order` type from `tplus.utils.domain`.
307
-
308
- ```python
309
- from ape import accounts, convert, chain
310
- from tplus.utils.domain import Order
311
- from tplus.evm.contracts import vault
312
- from tplus.utils.user import UserManager
313
-
314
- # Load your Ethereum account for t+.
315
- tplus_user = accounts.load("tplus-account")
316
-
317
- # Load your t+ user (public key).
318
- user_id = UserManager.load("my_user").public_key
319
-
320
- # Get the nonce from t+ or the contracts directly.
321
- nonce = vault.getDepositNonce(tplus_user)
322
-
323
- order = Order(
324
- tokenOut="0x62622E77D1349Face943C6e7D5c01C61465FE1dc",
325
- amountOut=convert("1 ether", int),
326
- tokenIn="0x58372ab62269A52fA636aD7F200d93999595DCAF",
327
- amountIn=convert("1 ether", int),
328
- userId=user_id,
329
- nonce=nonce,
330
- validUntil=chain.pending_timestamp,
331
- )
332
-
333
- # Use this signature for the settlement.
334
- signature = tplus_user.sign_message(order).encode_rsv()
335
- print(signature)
336
- ```
380
+ See [`docs/userguides/contracts.md`](docs/userguides/contracts.md) for the full
381
+ guide, including accounts, settlements, and the higher-level managers.
@@ -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,28 @@ 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.
201
+
202
+ # Replace several open orders in one request (up to 50). Each item is signed on its own;
203
+ # the OMS sends one message per market and returns one status per item, in order.
204
+ batch_replace_response = await client.replace_multiple_orders(
205
+ [
206
+ await client.prepare_replace_order_request(
207
+ original_order_id=oid, asset_id=example_asset, new_quantity=6, new_price=price
208
+ )
209
+ for oid, price in [("order-a", 1050), ("order-b", 1040)]
210
+ ]
211
+ )
212
+ for status in batch_replace_response.batch_order_status:
213
+ print(f"{status.order_id}: {status.status} {status.reason or ''}")
164
214
  ```
165
215
 
166
216
  See `examples/rest_usage.py` for a runnable demonstration.
@@ -198,68 +248,55 @@ async for trade in client.stream_finalized_trades():
198
248
 
199
249
  See `examples/websocket_usage.py` for a runnable demonstration using `asyncio.gather` to run multiple streams concurrently.
200
250
 
251
+ #### Token Recovery
252
+
253
+ 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.
254
+
201
255
  ### Contracts
202
256
 
203
- To interact with the contracts or sign T+ settlement messages, ensure you have installed the `evm` extra:
257
+ To interact with the on-chain T+ contracts (or sign T+ settlement messages),
258
+ install one of the EVM extras:
204
259
 
205
260
  ```shell
206
- pip install tpluspy[evm]
261
+ pip install "tpluspy[evm]" # web3.py backend (no Ape)
262
+ pip install "tpluspy[evm-ape]" # adds the Ape backend on top of [evm]
207
263
  ```
208
264
 
209
- Use the `tplusp.contracts` module to read data from t+ contracts.
210
- For example, launch a Sepolia-connected Ape console:
265
+ The Python API is identical either way. With the **web3.py** backend, point it
266
+ at a node via `WEB3_PROVIDER_URI` (read by `web3.auto`) — or call
267
+ `tplus.evm.use_web3(rpc_url=...)`:
211
268
 
212
269
  ```shell
213
- ape console --network ethereum:sepolia:alchemy
270
+ export WEB3_PROVIDER_URI="https://arb1.arbitrum.io/rpc"
271
+ ```
272
+
273
+ ```python
274
+ from tplus.evm.contracts import registry, vault
275
+
276
+ registry.admin()
277
+ vault.getApprovedSettlers()
214
278
  ```
215
279
 
216
- **Note**: You can use any provider you want or a RPC directly, it doesn't have to be Alchemy.
280
+ With the **Ape** backend, just use `ape console` / `ape run` as usual; it's
281
+ auto-detected whenever Ape is connected to a network:
217
282
 
218
- Then, once in the console, you will already have access to contracts that you can call methods on:
283
+ ```shell
284
+ ape console --network ethereum:sepolia:alchemy
285
+ ```
219
286
 
220
287
  ```python
221
- In[1]: registry.getAssets()
222
- Out[1]: [
288
+ In [1]: from tplus.evm.contracts import registry
289
+ In [2]: registry.getAssets()
290
+ Out[2]: [
223
291
  getAssets_return(
224
292
  assetAddress=HexBytes("0x000000000000000000000000f08a50178dfcde18524640ea6618a1f965821715"),
225
293
  chainId=11155111,
226
294
  maxDeposits=100,
227
295
  )
228
296
  ]
229
- In[2]: registry.admin()
230
- Out[2]: "0x467a95fC5359edE5d5dDc4f10A1F4B680694858E"
297
+ In [3]: registry.admin()
298
+ Out[3]: '0x467a95fC5359edE5d5dDc4f10A1F4B680694858E'
231
299
  ```
232
300
 
233
- #### Settlement signatures
234
-
235
- Sign settlement messages using the structured `Order` type from `tplus.utils.domain`.
236
-
237
- ```python
238
- from ape import accounts, convert, chain
239
- from tplus.utils.domain import Order
240
- from tplus.evm.contracts import vault
241
- from tplus.utils.user import UserManager
242
-
243
- # Load your Ethereum account for t+.
244
- tplus_user = accounts.load("tplus-account")
245
-
246
- # Load your t+ user (public key).
247
- user_id = UserManager.load("my_user").public_key
248
-
249
- # Get the nonce from t+ or the contracts directly.
250
- nonce = vault.getDepositNonce(tplus_user)
251
-
252
- order = Order(
253
- tokenOut="0x62622E77D1349Face943C6e7D5c01C61465FE1dc",
254
- amountOut=convert("1 ether", int),
255
- tokenIn="0x58372ab62269A52fA636aD7F200d93999595DCAF",
256
- amountIn=convert("1 ether", int),
257
- userId=user_id,
258
- nonce=nonce,
259
- validUntil=chain.pending_timestamp,
260
- )
261
-
262
- # Use this signature for the settlement.
263
- signature = tplus_user.sign_message(order).encode_rsv()
264
- print(signature)
265
- ```
301
+ See [`docs/userguides/contracts.md`](docs/userguides/contracts.md) for the full
302
+ guide, including accounts, settlements, and the higher-level managers.
@@ -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.5.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",
@@ -41,6 +42,10 @@ setup(
41
42
  "anyio>=4",
42
43
  "pytest-anyio>=0.0.0",
43
44
  "trio>=0.24",
45
+ # In-process EVM backend used by the web3-backend tests. ``py-evm``
46
+ # is what actually executes the EVM; ``eth-tester`` only ships
47
+ # pre-releases on PyPI so allow them.
48
+ "eth-tester[py-evm]>=0.12.0b1",
44
49
  ],
45
50
  "lint": [
46
51
  "ruff>=0.11.7,<0.16",
@@ -52,11 +57,18 @@ setup(
52
57
  "twine",
53
58
  ],
54
59
  "evm": [
55
- "ape-tokens",
56
60
  "click",
57
61
  "eip712>=0.3.1",
58
- "eth-ape>=0.8.49,<0.9",
62
+ "eth-account>=0.13.0,<0.14",
63
+ "eth-keys>=0.6,<0.8",
59
64
  "hexbytes>=1.3.1,<2",
65
+ "pyyaml>=6,<7",
66
+ "web3>=7.0,<8",
67
+ ],
68
+ "evm-ape": [
69
+ "tpluspy[evm]",
70
+ "ape-tokens",
71
+ "eth-ape>=0.8.49,<0.9",
60
72
  ],
61
73
  "docs": [
62
74
  "Sphinx>=7.0,<9",
@@ -72,7 +84,7 @@ setup(
72
84
  zip_safe=False,
73
85
  keywords="ethereum",
74
86
  packages=find_packages(exclude=["tests", "tests.*"]),
75
- package_data={"tpluspy": ["py.typed", "evm/manifests/*"]},
87
+ package_data={"tplus": ["py.typed", "evm/manifests/*"]},
76
88
  classifiers=[
77
89
  "Development Status :: 5 - Production/Stable",
78
90
  "Intended Audience :: Developers",
@@ -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),
@@ -78,7 +78,7 @@ def _build_manager(
78
78
  vault = cast("DepositVault", DepositVault.at(vault_address)) if vault_address else None
79
79
  return SettlementManager(
80
80
  default_user=cli_ctx.load_user(),
81
- ape_account=signer,
81
+ account=signer,
82
82
  oms_client=cli_ctx.orderbook_client(),
83
83
  vault=vault,
84
84
  )