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.
- {tpluspy-0.2.0 → tpluspy-0.2.3}/PKG-INFO +1 -1
- {tpluspy-0.2.0 → tpluspy-0.2.3}/setup.py +1 -1
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/_context.py +23 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/debug.py +48 -1
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/markets.py +2 -2
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/__init__.py +2 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/base.py +2 -2
- tpluspy-0.2.3/tplus/client/blockchain.py +50 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/__init__.py +8 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/admin.py +65 -14
- tpluspy-0.2.3/tplus/client/clearingengine/cross_venue.py +36 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/market_data.py +39 -8
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/orderbook.py +174 -10
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/dev/env.py +25 -3
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/settle.py +156 -9
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/klines.py +37 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/order.py +18 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/position.py +1 -1
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/trades.py +15 -2
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/user_margin.py +14 -0
- tpluspy-0.2.3/tplus/model/user_simulated_margin.py +73 -0
- tpluspy-0.2.3/tplus/utils/operator.py +26 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/PKG-INFO +1 -1
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/SOURCES.txt +4 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/README.md +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/pyproject.toml +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/setup.cfg +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/__main__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/accounts.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/assets.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/balance.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/decimals.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/deposit.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/env.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/orders.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/params.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/settle.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/sign.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/stream.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/trades.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/vaults.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/withdraw.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/_cli/withdrawal.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/cli_tools/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/cli_tools/options.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/cli_tools/output.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/auth.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/admin_settlement.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/assetregistry.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/base.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/decimal.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/clearingengine/vault.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/oms/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/oms/assetregistry.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/oms/oms_admin.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/client/withdrawal.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/constants.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/abi.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/address.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/constants.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/contracts.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/dev/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/dev/contracts.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/dev/token.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/exceptions.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/chaindata.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/credential_manager.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/deposit.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/evm.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/registry.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/vault.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/managers/withdraw.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/evm/utils.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/exceptions.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/logger.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/managers/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/managers/base.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/approval.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/asset_identifier.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/batch_order.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/cancel_order.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/chain_address.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/close_all_positions_preview.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/config.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/interest_rates.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/limit_order.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/market.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/market_order.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/multisig.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/order_trigger.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/orderbook.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/replace_order.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/risk_parameters.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/settlement.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/signed_message.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/types.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/user_event.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/user_inventory.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/user_solvency.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/model/withdrawal.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/py.typed +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/types.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/address.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/amount.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/bytes32.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/decimals.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/domain.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/hex.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/limit_order.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/market_order.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/replace_order.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/serializers.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/signing.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/timeout.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/__init__.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/decrypt.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/ed_keyfile.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/manager.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/model.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/token_cache.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tplus/utils/user/validate.py +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/dependency_links.txt +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/entry_points.txt +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/not-zip-safe +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/requires.txt +0 -0
- {tpluspy-0.2.0 → tpluspy-0.2.3}/tpluspy.egg-info/top_level.txt +0 -0
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
"""
|
|
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
|
-
|
|
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__(
|
|
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
|
-
) ->
|
|
54
|
-
"""
|
|
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
|
|
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`."""
|