tpluspy 0.2.0__tar.gz → 0.2.3__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 (132) hide show
  1. {tpluspy-0.2.0 → tpluspy-0.2.3}/PKG-INFO +1 -1
  2. {tpluspy-0.2.0 → tpluspy-0.2.3}/setup.py +1 -1
  3. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/_context.py +23 -0
  4. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/debug.py +48 -1
  5. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/markets.py +2 -2
  6. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/__init__.py +2 -0
  7. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/base.py +2 -2
  8. tpluspy-0.2.3/tplus/client/blockchain.py +50 -0
  9. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/__init__.py +8 -0
  10. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/admin.py +65 -14
  11. tpluspy-0.2.3/tplus/client/clearingengine/cross_venue.py +36 -0
  12. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/market_data.py +39 -8
  13. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/orderbook.py +174 -10
  14. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/dev/env.py +25 -3
  15. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/settle.py +156 -9
  16. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/klines.py +37 -0
  17. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/order.py +18 -0
  18. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/position.py +1 -1
  19. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/trades.py +15 -2
  20. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/user_margin.py +14 -0
  21. tpluspy-0.2.3/tplus/model/user_simulated_margin.py +73 -0
  22. tpluspy-0.2.3/tplus/utils/operator.py +26 -0
  23. {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/PKG-INFO +1 -1
  24. {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/SOURCES.txt +4 -0
  25. {tpluspy-0.2.0 → tpluspy-0.2.3}/README.md +0 -0
  26. {tpluspy-0.2.0 → tpluspy-0.2.3}/pyproject.toml +0 -0
  27. {tpluspy-0.2.0 → tpluspy-0.2.3}/setup.cfg +0 -0
  28. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/__init__.py +0 -0
  29. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/__init__.py +0 -0
  30. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/__main__.py +0 -0
  31. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/accounts.py +0 -0
  32. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/assets.py +0 -0
  33. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/balance.py +0 -0
  34. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/decimals.py +0 -0
  35. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/deposit.py +0 -0
  36. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/env.py +0 -0
  37. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/orders.py +0 -0
  38. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/params.py +0 -0
  39. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/settle.py +0 -0
  40. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/sign.py +0 -0
  41. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/stream.py +0 -0
  42. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/trades.py +0 -0
  43. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/vaults.py +0 -0
  44. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/withdraw.py +0 -0
  45. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/withdrawal.py +0 -0
  46. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/cli_tools/__init__.py +0 -0
  47. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/cli_tools/options.py +0 -0
  48. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/cli_tools/output.py +0 -0
  49. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/auth.py +0 -0
  50. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/admin_settlement.py +0 -0
  51. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/assetregistry.py +0 -0
  52. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/base.py +0 -0
  53. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/decimal.py +0 -0
  54. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/vault.py +0 -0
  55. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/oms/__init__.py +0 -0
  56. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/oms/assetregistry.py +0 -0
  57. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/oms/oms_admin.py +0 -0
  58. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/withdrawal.py +0 -0
  59. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/constants.py +0 -0
  60. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/__init__.py +0 -0
  61. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/abi.py +0 -0
  62. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/address.py +0 -0
  63. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/constants.py +0 -0
  64. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/contracts.py +0 -0
  65. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/dev/__init__.py +0 -0
  66. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/dev/contracts.py +0 -0
  67. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/dev/token.py +0 -0
  68. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/exceptions.py +0 -0
  69. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/__init__.py +0 -0
  70. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/chaindata.py +0 -0
  71. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/credential_manager.py +0 -0
  72. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/deposit.py +0 -0
  73. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/evm.py +0 -0
  74. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/registry.py +0 -0
  75. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/vault.py +0 -0
  76. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/withdraw.py +0 -0
  77. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/utils.py +0 -0
  78. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/exceptions.py +0 -0
  79. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/logger.py +0 -0
  80. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/managers/__init__.py +0 -0
  81. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/managers/base.py +0 -0
  82. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/__init__.py +0 -0
  83. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/approval.py +0 -0
  84. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/asset_identifier.py +0 -0
  85. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/batch_order.py +0 -0
  86. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/cancel_order.py +0 -0
  87. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/chain_address.py +0 -0
  88. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/close_all_positions_preview.py +0 -0
  89. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/config.py +0 -0
  90. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/interest_rates.py +0 -0
  91. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/limit_order.py +0 -0
  92. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/market.py +0 -0
  93. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/market_order.py +0 -0
  94. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/multisig.py +0 -0
  95. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/order_trigger.py +0 -0
  96. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/orderbook.py +0 -0
  97. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/replace_order.py +0 -0
  98. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/risk_parameters.py +0 -0
  99. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/settlement.py +0 -0
  100. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/signed_message.py +0 -0
  101. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/types.py +0 -0
  102. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/user_event.py +0 -0
  103. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/user_inventory.py +0 -0
  104. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/user_solvency.py +0 -0
  105. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/withdrawal.py +0 -0
  106. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/py.typed +0 -0
  107. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/types.py +0 -0
  108. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/__init__.py +0 -0
  109. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/address.py +0 -0
  110. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/amount.py +0 -0
  111. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/bytes32.py +0 -0
  112. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/decimals.py +0 -0
  113. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/domain.py +0 -0
  114. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/hex.py +0 -0
  115. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/limit_order.py +0 -0
  116. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/market_order.py +0 -0
  117. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/replace_order.py +0 -0
  118. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/serializers.py +0 -0
  119. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/signing.py +0 -0
  120. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/timeout.py +0 -0
  121. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/__init__.py +0 -0
  122. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/decrypt.py +0 -0
  123. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/ed_keyfile.py +0 -0
  124. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/manager.py +0 -0
  125. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/model.py +0 -0
  126. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/token_cache.py +0 -0
  127. {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/validate.py +0 -0
  128. {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/dependency_links.txt +0 -0
  129. {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/entry_points.txt +0 -0
  130. {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/not-zip-safe +0 -0
  131. {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/requires.txt +0 -0
  132. {tpluspy-0.2.0 → tpluspy-0.2.3}/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.2.0
3
+ Version: 0.2.3
4
4
  Summary: tpluspy: Client utilities for interacting with tplus
5
5
  Home-page: https://github.com/tpluslabs/tpluspy
6
6
  Author: TPlus Labs
@@ -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.2.0",
11
+ version="0.2.3",
12
12
  description="""tpluspy: Client utilities for interacting with tplus""",
13
13
  long_description=long_description,
14
14
  long_description_content_type="text/markdown",
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
6
6
  import click
7
7
 
8
8
  if TYPE_CHECKING:
9
+ from tplus.client.blockchain import BlockchainClient
9
10
  from tplus.client.clearingengine import ClearingEngineClient
10
11
  from tplus.client.market_data import MarketDataClient
11
12
  from tplus.client.orderbook import OrderBookClient
@@ -26,6 +27,7 @@ class CLIContext(dict):
26
27
  self.orderbook_base_url: str | None = None
27
28
  self.clearing_base_url: str | None = None
28
29
  self.market_data_base_url: str | None = None
30
+ self.blockchain_base_url: str | None = None
29
31
  self.account: str | None = None
30
32
  self.ignore_ssl: bool = False
31
33
 
@@ -106,6 +108,17 @@ class CLIContext(dict):
106
108
  insecure_ssl=self.ignore_ssl,
107
109
  )
108
110
 
111
+ def blockchain_client(self) -> "BlockchainClient":
112
+ if not self.blockchain_base_url:
113
+ raise click.UsageError(
114
+ "No blockchain client base URL specified. "
115
+ "Pass --blockchain-base-url or set TPLUS_BLOCKCHAIN_BASE_URL."
116
+ )
117
+
118
+ from tplus.client.blockchain import BlockchainClient
119
+
120
+ return BlockchainClient(base_url=self.blockchain_base_url, insecure_ssl=self.ignore_ssl)
121
+
109
122
  def market_data_client(self) -> "MarketDataClient":
110
123
  if not self.market_data_base_url:
111
124
  raise click.UsageError(
@@ -164,6 +177,16 @@ def market_data_url_option() -> Callable:
164
177
  )
165
178
 
166
179
 
180
+ def blockchain_url_option() -> Callable:
181
+ return click.option(
182
+ "--blockchain-base-url",
183
+ envvar="TPLUS_BLOCKCHAIN_BASE_URL",
184
+ help="T+ blockchain client base URL.",
185
+ callback=_set_on_ctx("blockchain_base_url"),
186
+ expose_value=False,
187
+ )
188
+
189
+
167
190
  def tplus_account_option() -> Callable:
168
191
  def callback(ctx: click.Context, _param, value):
169
192
  cli_ctx = ctx.ensure_object(CLIContext)
@@ -2,7 +2,12 @@ import asyncio
2
2
 
3
3
  import click
4
4
 
5
- from tplus._cli._context import CLIContext, clearing_url_option, pass_cli_context
5
+ from tplus._cli._context import (
6
+ CLIContext,
7
+ blockchain_url_option,
8
+ clearing_url_option,
9
+ pass_cli_context,
10
+ )
6
11
  from tplus.cli_tools import ignore_ssl_option
7
12
 
8
13
 
@@ -90,6 +95,48 @@ def _set_withdrawal_delay(
90
95
  click.echo("Withdrawal delay parameters updated.")
91
96
 
92
97
 
98
+ @debug.command("sync-vault-events")
99
+ @click.option("--from-block", "from_block", type=int, required=True)
100
+ @click.option("--to-block", "to_block", type=int, required=True)
101
+ @click.option("--address", "address", default=None, help="Contract address to filter on.")
102
+ @click.option(
103
+ "--event",
104
+ "events",
105
+ multiple=True,
106
+ help="Event topic-0 signature to filter on (repeatable).",
107
+ )
108
+ @click.option(
109
+ "--operator-secret",
110
+ "operator_secret",
111
+ envvar="TPLUS_OPERATOR_SECRET",
112
+ default=None,
113
+ help="Hex operator secret. Required by authenticated (threshold) clients.",
114
+ )
115
+ @blockchain_url_option()
116
+ @ignore_ssl_option()
117
+ @pass_cli_context
118
+ def _sync_vault_events(
119
+ cli_ctx: CLIContext,
120
+ from_block: int,
121
+ to_block: int,
122
+ address: str | None,
123
+ events: tuple[str, ...],
124
+ operator_secret: str | None,
125
+ ):
126
+ """Re-ingest a blockchain client's on-chain events in a block range."""
127
+ client = cli_ctx.blockchain_client()
128
+ asyncio.run(
129
+ client.sync_vault_events(
130
+ from_block,
131
+ to_block,
132
+ address=address,
133
+ events=list(events) or None,
134
+ operator_secret=operator_secret,
135
+ )
136
+ )
137
+ click.echo(f"Requested re-ingestion of blocks {from_block}-{to_block}.")
138
+
139
+
93
140
  @debug.command("reset-users")
94
141
  @clearing_url_option()
95
142
  @ignore_ssl_option()
@@ -136,9 +136,9 @@ def _klines(
136
136
  )
137
137
  if output_format == "raw":
138
138
  echo_with_pager(
139
- [kline.model_dump_json() for kline in klines],
139
+ [kline.model_dump_json() for kline in klines.items],
140
140
  no_pager=no_pager,
141
141
  )
142
142
  return
143
143
 
144
- render([kline.model_dump() for kline in klines], output_format, no_pager=no_pager)
144
+ render([kline.model_dump() for kline in klines.items], output_format, no_pager=no_pager)
@@ -1,3 +1,4 @@
1
+ from .blockchain import BlockchainClient
1
2
  from .clearingengine import ClearingEngineClient
2
3
  from .market_data import MarketDataClient
3
4
  from .oms import AssetRegistryClient
@@ -5,6 +6,7 @@ from .orderbook import OrderBookClient
5
6
  from .withdrawal import WithdrawalClient
6
7
 
7
8
  __all__ = (
9
+ "BlockchainClient",
8
10
  "ClearingEngineClient",
9
11
  "MarketDataClient",
10
12
  "OrderBookClient",
@@ -178,10 +178,9 @@ class BaseClient:
178
178
  user: "UserType | None" = None,
179
179
  request_timeout: float | None = None,
180
180
  ) -> dict[str, Any]:
181
- relative_url = endpoint if endpoint.startswith("/") else f"/{endpoint}"
182
181
  response = await self._send(
183
182
  method,
184
- relative_url,
183
+ endpoint,
185
184
  json_data=json_data,
186
185
  params=params,
187
186
  request_timeout=request_timeout,
@@ -201,6 +200,7 @@ class BaseClient:
201
200
  headers: dict[str, str] | None = None,
202
201
  request_timeout: float | None = None,
203
202
  ) -> httpx.Response:
203
+ relative_url = relative_url if relative_url.startswith("/") else f"/{relative_url}"
204
204
  if json_data or params:
205
205
  self.logger.debug(
206
206
  f"Request to {method} {relative_url} with payload: {json_data} params: {params}"
@@ -0,0 +1,50 @@
1
+ import time
2
+
3
+ from tplus.client.base import BaseClient
4
+ from tplus.utils.operator import load_operator_sk, sign_operator_payload
5
+
6
+
7
+ class BlockchainClient(BaseClient):
8
+ """Admin HTTP client for a blockchain client (threshold / anvil).
9
+
10
+ Authenticates with a secp256k1 operator signature rather than Ed25519 user signing.
11
+ ``base_url`` points at the blockchain client's health-API port.
12
+ """
13
+
14
+ async def sync_vault_events(
15
+ self,
16
+ from_block: int,
17
+ to_block: int,
18
+ *,
19
+ address: str | None = None,
20
+ events: list[str] | None = None,
21
+ operator_secret: str | None = None,
22
+ timestamp_ms: int | None = None,
23
+ ) -> None:
24
+ """Trigger re-ingestion of on-chain events in ``[from_block, to_block]``.
25
+
26
+ Fire-and-forget: the client re-queries logs and replays them over the overlay, so nothing
27
+ is returned here. Pass ``operator_secret`` for authenticated (threshold) clients; omit it
28
+ for anvil.
29
+ """
30
+ timestamp = int(time.time() * 1000) if timestamp_ms is None else timestamp_ms
31
+ events_str = ",".join(events) if events else ""
32
+ payload = f"{from_block}:{to_block}:{timestamp}:{address or ''}:{events_str}".encode()
33
+
34
+ signature = ""
35
+ if operator_secret is not None:
36
+ signature = sign_operator_payload(payload, load_operator_sk(operator_secret))
37
+
38
+ body: dict = {
39
+ "from_block": from_block,
40
+ "to_block": to_block,
41
+ "timestamp": timestamp,
42
+ "signature": signature,
43
+ }
44
+ if address is not None:
45
+ body["address"] = address
46
+ if events is not None:
47
+ body["events"] = events
48
+
49
+ # "historical_logs" is the blockchain client's existing route name.
50
+ await self._post("historical_logs", json_data=body, requires_auth=False)
@@ -5,6 +5,7 @@ from tplus.client.clearingengine.admin import AdminClient
5
5
  from tplus.client.clearingengine.admin_settlement import AdminSettlementClient
6
6
  from tplus.client.clearingengine.assetregistry import AdminAssetRegistryClient
7
7
  from tplus.client.clearingengine.base import BaseClearingEngineClient
8
+ from tplus.client.clearingengine.cross_venue import CrossVenueClient
8
9
  from tplus.client.clearingengine.decimal import DecimalClient
9
10
  from tplus.client.clearingengine.vault import VaultClient
10
11
 
@@ -50,6 +51,13 @@ class ClearingEngineClient(BaseClearingEngineClient):
50
51
  """
51
52
  return VaultClient.from_client(self)
52
53
 
54
+ @cached_property
55
+ def cross_venue(self) -> CrossVenueClient:
56
+ """
57
+ User-facing cross-venue (Hyperliquid) margin APIs.
58
+ """
59
+ return CrossVenueClient.from_client(self)
60
+
53
61
  @cached_property
54
62
  def admin(self) -> AdminClient:
55
63
  """
@@ -1,35 +1,25 @@
1
- import hashlib
2
1
  import time
3
2
  from typing import Any
4
3
 
5
- from cryptography.hazmat.primitives import hashes
6
4
  from cryptography.hazmat.primitives.asymmetric import ec
7
- from cryptography.hazmat.primitives.asymmetric.utils import Prehashed, decode_dss_signature
8
5
 
9
6
  from tplus.client.clearingengine.base import BaseClearingEngineClient
10
7
  from tplus.model.asset_identifier import AssetIdentifier
11
8
  from tplus.model.interest_rates import InterestRates
12
9
  from tplus.model.types import UserPublicKey
10
+ from tplus.utils.operator import load_operator_sk, sign_operator_payload
13
11
  from tplus.utils.user import User
14
12
 
15
13
 
16
14
  class AdminClient(BaseClearingEngineClient):
17
15
  @staticmethod
18
16
  def _load_operator_sk(operator_secret) -> ec.EllipticCurvePrivateKey:
19
- secret_bytes = bytes.fromhex(operator_secret)
20
- return ec.derive_private_key(int.from_bytes(secret_bytes, "big"), ec.SECP256K1())
17
+ return load_operator_sk(operator_secret)
21
18
 
22
19
  @staticmethod
23
20
  def _sign(payload: bytes, sk: ec.EllipticCurvePrivateKey) -> str:
24
21
  """SHA256 -> ECDSA sign -> low-S normalize -> compact r||s -> hex."""
25
- SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
26
- SECP256K1_HALF_ORDER = SECP256K1_ORDER // 2
27
- digest = hashlib.sha256(payload).digest()
28
- sig_der = sk.sign(digest, ec.ECDSA(Prehashed(hashes.SHA256())))
29
- r, s = decode_dss_signature(sig_der)
30
- if s > SECP256K1_HALF_ORDER:
31
- s = SECP256K1_ORDER - s
32
- return (r.to_bytes(32, "big") + s.to_bytes(32, "big")).hex()
22
+ return sign_operator_payload(payload, sk)
33
23
 
34
24
  async def get_verifying_key(self):
35
25
  """
@@ -222,6 +212,32 @@ class AdminClient(BaseClearingEngineClient):
222
212
 
223
213
  await self._post("admin/last-trade-prices/modify", json_data={"prices": prices})
224
214
 
215
+ async def set_impact_prices(
216
+ self,
217
+ asset_id: AssetIdentifier,
218
+ buy_impact: str | None,
219
+ sell_impact: str | None,
220
+ decimals: int = 0,
221
+ ):
222
+ """Override the buy/sell impact prices for an asset on the CE (debug only).
223
+
224
+ The CE applies the override locally and propagates it to the OMS. Pass
225
+ ``buy_impact=None`` and ``sell_impact=None`` (the default) to clear the
226
+ override and set the asset's impact price back to ``None``.
227
+ """
228
+ prices: dict[str, Any]
229
+ if buy_impact is None and sell_impact is None:
230
+ prices = {str(asset_id): None}
231
+ else:
232
+ override: dict[str, Any] = {}
233
+ if buy_impact is not None:
234
+ override["buy_impact"] = {"price": buy_impact, "decimals": decimals}
235
+ if sell_impact is not None:
236
+ override["sell_impact"] = {"price": sell_impact, "decimals": decimals}
237
+ prices = {str(asset_id): override}
238
+
239
+ await self._post("admin/impact-prices/modify", json_data={"prices": prices})
240
+
225
241
  async def set_book_decimals(
226
242
  self,
227
243
  asset_id: AssetIdentifier,
@@ -243,12 +259,20 @@ class AdminClient(BaseClearingEngineClient):
243
259
  is_mm: bool,
244
260
  operator_secret: str,
245
261
  timestamp_ns: int | None = None,
262
+ nonce: int | None = None,
246
263
  ):
247
264
  ts = time.time_ns() if timestamp_ns is None else timestamp_ns
265
+ request_nonce = time.time_ns() if nonce is None else nonce
248
266
 
249
267
  sk = AdminClient._load_operator_sk(operator_secret=operator_secret)
250
268
  user_pubkey = bytes(user.public_key_vec)
251
- payload = ts.to_bytes(8, "big") + user_pubkey + b"\x01"
269
+ payload = (
270
+ b"ce.admin.status.modify.v1"
271
+ + request_nonce.to_bytes(8, "big")
272
+ + ts.to_bytes(8, "big")
273
+ + user_pubkey
274
+ + (b"\x01" if is_mm else b"\x00")
275
+ )
252
276
  sig = AdminClient._sign(payload, sk)
253
277
 
254
278
  await self._post(
@@ -258,6 +282,7 @@ class AdminClient(BaseClearingEngineClient):
258
282
  "user": user.public_key,
259
283
  "is_mm": is_mm,
260
284
  "timestamp_ns": ts,
285
+ "nonce": request_nonce,
261
286
  },
262
287
  "signature": sig,
263
288
  },
@@ -275,6 +300,19 @@ class AdminClient(BaseClearingEngineClient):
275
300
  json_data={"fee_account": user},
276
301
  )
277
302
 
303
+ async def set_avs_backstop_target(
304
+ self, user: UserPublicKey | None, account_index: int | None = None
305
+ ):
306
+ """Configure the AVS backstop target, or clear it (disabling the
307
+ post-liquidation transfer) when ``user`` is ``None``."""
308
+ await self._post(
309
+ "admin/avs-backstop-target/modify",
310
+ json_data={"user": user, "account_index": account_index},
311
+ )
312
+
313
+ async def clear_avs_backstop_target(self):
314
+ await self.set_avs_backstop_target(None)
315
+
278
316
  async def reset_users(self):
279
317
  await self._post(
280
318
  "admin/users/reset",
@@ -287,6 +325,19 @@ class AdminClient(BaseClearingEngineClient):
287
325
  json_data={},
288
326
  )
289
327
 
328
+ async def expire_xm_venue_locks(self):
329
+ """Force-expire every cross-venue (Hyperliquid) state-lock now (debug only).
330
+
331
+ Simulates the inaccessible-/downtime-timeout cliff: each locked venue
332
+ balance is zeroed and the (now smaller) credit line is gossiped to the
333
+ OMS. Lets e2e tests observe the post-timeout solvency drop without
334
+ waiting the real 7-day ``VENUE_INACCESSIBLE_TIMEOUT_NS``.
335
+ """
336
+ await self._post(
337
+ "admin/xm/expire-locks",
338
+ json_data={},
339
+ )
340
+
290
341
 
291
342
  def _asset_id_to_index(
292
343
  asset_id: int | AssetIdentifier | str,
@@ -0,0 +1,36 @@
1
+ import time
2
+ from typing import TYPE_CHECKING
3
+
4
+ from tplus.client.clearingengine.base import BaseClearingEngineClient
5
+
6
+ if TYPE_CHECKING:
7
+ from tplus.utils.user import User
8
+
9
+
10
+ class CrossVenueClient(BaseClearingEngineClient):
11
+ """User-facing cross-venue (e.g. Hyperliquid) margin APIs on the clearing engine."""
12
+
13
+ async def get_venue_state(self, user: "User", venue: int) -> dict:
14
+ """Read the caller's cross-venue ``(user, venue)`` state.
15
+
16
+ User-authenticated: the query is signed with ``user``'s key, so a user
17
+ can only read their own venue state. Returns
18
+ ``{venue_present, allocation_bps, locked, usd_balance}`` — ``venue_present``
19
+ flips true once the adapter binding reaches the CE, and ``allocation_bps``
20
+ is non-zero once the credit line is applied.
21
+ """
22
+ ts = time.time_ns()
23
+ sep = b"\x1f"
24
+ # Must match `xm_venue_state_signing_payload` in the CE creditline route.
25
+ payload = (
26
+ b"xm_venue_state_query_v1"
27
+ + sep
28
+ + bytes.fromhex(user.public_key)
29
+ + sep
30
+ + venue.to_bytes(8, "big")
31
+ + sep
32
+ + ts.to_bytes(8, "big")
33
+ )
34
+ signature = user.sk.sign(payload).hex()
35
+ endpoint = f"xm/venue/{user.public_key}/{venue}?timestamp_ns={ts}&signature={signature}"
36
+ return await self._get(endpoint, requires_auth=False)
@@ -1,11 +1,15 @@
1
- """Read-only client for the `market-data-service` (public market data; no auth)."""
1
+ """Client for the `market-data-service` (public market data + per-user endpoints)."""
2
2
 
3
3
  from collections.abc import AsyncIterator
4
- from typing import Any
4
+ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from tplus.client.base import BaseClient
7
7
  from tplus.model.asset_identifier import AssetIdentifier
8
- from tplus.model.klines import KlineUpdate, parse_kline_update
8
+
9
+ if TYPE_CHECKING:
10
+ from tplus.client.auth import AuthenticatedClient
11
+ from tplus.types import UserType
12
+ from tplus.model.klines import KlinesPage, KlineUpdate, parse_kline_update, parse_klines_page
9
13
  from tplus.model.orderbook import OrderBook, OrderBookDiff
10
14
  from tplus.model.trades import (
11
15
  Trade,
@@ -15,6 +19,8 @@ from tplus.model.trades import (
15
19
  parse_trades,
16
20
  )
17
21
 
22
+ DEFAULT_BASE_URL = "http://localhost:8011"
23
+
18
24
 
19
25
  def _pagination(page: int | None, limit: int | None) -> dict[str, Any]:
20
26
  params: dict[str, Any] = {}
@@ -30,8 +36,33 @@ def _pagination(page: int | None, limit: int | None) -> dict[str, Any]:
30
36
  class MarketDataClient(BaseClient):
31
37
  """Klines, order-book depth, public trades and 24h tickers (REST + WS streams)."""
32
38
 
33
- def __init__(self, base_url: str = "http://localhost:8011", **kwargs: Any) -> None:
39
+ def __init__(
40
+ self,
41
+ base_url: str = DEFAULT_BASE_URL,
42
+ *,
43
+ auth_client: "AuthenticatedClient | None" = None,
44
+ **kwargs: Any,
45
+ ) -> None:
34
46
  super().__init__(base_url, **kwargs)
47
+ # Per-user endpoints borrow the OMS bearer token from this client.
48
+ self._auth_client = auth_client
49
+
50
+ async def _authed_get(
51
+ self,
52
+ endpoint: str,
53
+ *,
54
+ user: "UserType | None" = None,
55
+ params: dict[str, Any] | None = None,
56
+ ) -> Any:
57
+ """GET an MDS endpoint with the OMS bearer token attached."""
58
+ if self._auth_client is None:
59
+ raise ValueError("per-user market-data endpoints require an `auth_client`")
60
+
61
+ signer = user if not isinstance(user, str) else None
62
+ await self._auth_client._ensure_auth(user=signer)
63
+ headers = self._auth_client._get_auth_headers(user=user)
64
+ response = await self._send("GET", endpoint, params=params, headers=headers)
65
+ return self._handle_response(response)
35
66
 
36
67
  async def get_orderbook_snapshot(self, asset_id: AssetIdentifier) -> OrderBook:
37
68
  """Current order-book snapshot for `asset_id`."""
@@ -50,8 +81,8 @@ class MarketDataClient(BaseClient):
50
81
  page: int | None = None,
51
82
  limit: int | None = None,
52
83
  end_timestamp_ns: int | None = None,
53
- ) -> list[KlineUpdate]:
54
- """K-line (candlestick) data for `asset_id`."""
84
+ ) -> KlinesPage:
85
+ """A page of k-line (candlestick) data for `asset_id`, with pagination metadata."""
55
86
  params = _pagination(page, limit)
56
87
  if end_timestamp_ns:
57
88
  params["end_timestamp_ns"] = end_timestamp_ns
@@ -59,10 +90,10 @@ class MarketDataClient(BaseClient):
59
90
  response = await self._request(
60
91
  "GET", f"/klines/{asset_id}", params=params, requires_auth=False
61
92
  )
62
- if not isinstance(response, list):
93
+ if not isinstance(response, dict | list):
63
94
  raise ValueError(f"Invalid response from get_klines: {response}")
64
95
 
65
- return parse_kline_update(response)
96
+ return parse_klines_page(response)
66
97
 
67
98
  async def get_ticker(self, asset_id: AssetIdentifier) -> dict[str, Any]:
68
99
  """24h ticker for `asset_id`."""