tpluspy 0.2.1__tar.gz → 0.2.4__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.1 → tpluspy-0.2.4}/PKG-INFO +23 -1
- {tpluspy-0.2.1 → tpluspy-0.2.4}/README.md +22 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/setup.py +1 -1
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/_context.py +80 -18
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/debug.py +48 -1
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/env.py +11 -1
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/markets.py +7 -2
- tpluspy-0.2.4/tplus/asset_metadata.py +116 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/__init__.py +2 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/base.py +2 -2
- tpluspy-0.2.4/tplus/client/blockchain.py +50 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/__init__.py +8 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/admin.py +52 -14
- tpluspy-0.2.4/tplus/client/clearingengine/cross_venue.py +36 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/market_data.py +39 -8
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/orderbook.py +164 -6
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/dev/env.py +25 -3
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/settle.py +156 -9
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/klines.py +37 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/order.py +1 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/position.py +1 -1
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/trades.py +2 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/user_margin.py +4 -0
- tpluspy-0.2.4/tplus/model/user_simulated_margin.py +73 -0
- tpluspy-0.2.4/tplus/utils/operator.py +26 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/PKG-INFO +23 -1
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/SOURCES.txt +5 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/pyproject.toml +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/setup.cfg +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/__main__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/accounts.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/assets.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/balance.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/decimals.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/deposit.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/orders.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/params.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/settle.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/sign.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/stream.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/trades.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/vaults.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/withdraw.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/withdrawal.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/cli_tools/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/cli_tools/options.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/cli_tools/output.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/auth.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/admin_settlement.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/assetregistry.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/base.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/decimal.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/vault.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/oms/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/oms/assetregistry.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/oms/oms_admin.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/withdrawal.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/constants.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/abi.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/address.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/constants.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/contracts.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/dev/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/dev/contracts.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/dev/token.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/exceptions.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/chaindata.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/credential_manager.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/deposit.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/evm.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/registry.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/vault.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/withdraw.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/utils.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/exceptions.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/logger.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/managers/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/managers/base.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/approval.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/asset_identifier.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/batch_order.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/cancel_order.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/chain_address.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/close_all_positions_preview.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/config.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/interest_rates.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/limit_order.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/market.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/market_order.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/multisig.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/order_trigger.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/orderbook.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/replace_order.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/risk_parameters.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/settlement.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/signed_message.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/types.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/user_event.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/user_inventory.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/user_solvency.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/withdrawal.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/py.typed +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/types.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/address.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/amount.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/bytes32.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/decimals.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/domain.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/hex.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/limit_order.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/market_order.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/replace_order.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/serializers.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/signing.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/timeout.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/__init__.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/decrypt.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/ed_keyfile.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/manager.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/model.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/token_cache.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/validate.py +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/dependency_links.txt +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/entry_points.txt +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/not-zip-safe +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/requires.txt +0 -0
- {tpluspy-0.2.1 → tpluspy-0.2.4}/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.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: tpluspy: Client utilities for interacting with tplus
|
|
5
5
|
Home-page: https://github.com/tpluslabs/tpluspy
|
|
6
6
|
Author: TPlus Labs
|
|
@@ -81,6 +81,28 @@ To install, use either `pip` or `uv pip`:
|
|
|
81
81
|
uv pip install -e .
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Onboarding with Codex CLI
|
|
85
|
+
|
|
86
|
+
This repo includes a Codex-discoverable onboarding skill at
|
|
87
|
+
`.agents/skills/onboard` for connecting an EVM wallet key to T+, checking whether
|
|
88
|
+
it already controls a T+ account, and guiding account creation when needed.
|
|
89
|
+
|
|
90
|
+
From the repo root:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
codex
|
|
94
|
+
/skills
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Choose `onboard`, or invoke it directly with:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
$onboard onboard me to T+
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The flow reads `TPLUS_PRIVATE_KEY` and `TPLUS_API_BASE_URL` from `.env`. Do not
|
|
104
|
+
paste private keys into chat.
|
|
105
|
+
|
|
84
106
|
## CLI
|
|
85
107
|
|
|
86
108
|
`tpluspy` installs a `tplus` command for interacting with T+ services from the
|
|
@@ -10,6 +10,28 @@ To install, use either `pip` or `uv pip`:
|
|
|
10
10
|
uv pip install -e .
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Onboarding with Codex CLI
|
|
14
|
+
|
|
15
|
+
This repo includes a Codex-discoverable onboarding skill at
|
|
16
|
+
`.agents/skills/onboard` for connecting an EVM wallet key to T+, checking whether
|
|
17
|
+
it already controls a T+ account, and guiding account creation when needed.
|
|
18
|
+
|
|
19
|
+
From the repo root:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
codex
|
|
23
|
+
/skills
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Choose `onboard`, or invoke it directly with:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
$onboard onboard me to T+
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The flow reads `TPLUS_PRIVATE_KEY` and `TPLUS_API_BASE_URL` from `.env`. Do not
|
|
33
|
+
paste private keys into chat.
|
|
34
|
+
|
|
13
35
|
## CLI
|
|
14
36
|
|
|
15
37
|
`tpluspy` installs a `tplus` command for interacting with T+ services from the
|
|
@@ -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.4",
|
|
12
12
|
description="""tpluspy: Client utilities for interacting with tplus""",
|
|
13
13
|
long_description=long_description,
|
|
14
14
|
long_description_content_type="text/markdown",
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from collections.abc import Callable
|
|
2
3
|
from functools import cached_property
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
6
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
5
7
|
|
|
6
8
|
import click
|
|
7
9
|
|
|
8
10
|
if TYPE_CHECKING:
|
|
11
|
+
from tplus.client.blockchain import BlockchainClient
|
|
9
12
|
from tplus.client.clearingengine import ClearingEngineClient
|
|
10
13
|
from tplus.client.market_data import MarketDataClient
|
|
11
14
|
from tplus.client.orderbook import OrderBookClient
|
|
@@ -17,15 +20,42 @@ if TYPE_CHECKING:
|
|
|
17
20
|
_AUTH_CACHE_DIR = Path.home() / ".tplus" / "auth"
|
|
18
21
|
|
|
19
22
|
|
|
23
|
+
def _derive_market_data_base_url(oms_base_url: str) -> str:
|
|
24
|
+
"""Derive the market-data-service URL from the OMS URL (swap the host label
|
|
25
|
+
``oms``->``mds``). Returns the input unchanged for non-oms hosts (e.g. localhost)."""
|
|
26
|
+
parsed = urlsplit(oms_base_url)
|
|
27
|
+
host = parsed.hostname
|
|
28
|
+
if host is None:
|
|
29
|
+
return oms_base_url
|
|
30
|
+
if host.startswith("oms."):
|
|
31
|
+
mds_host = f"mds.{host.removeprefix('oms.')}"
|
|
32
|
+
elif host.startswith("oms-"):
|
|
33
|
+
mds_host = f"mds-{host.removeprefix('oms-')}"
|
|
34
|
+
else:
|
|
35
|
+
return oms_base_url
|
|
36
|
+
userinfo = ""
|
|
37
|
+
if parsed.username is not None:
|
|
38
|
+
userinfo = parsed.username
|
|
39
|
+
if parsed.password is not None:
|
|
40
|
+
userinfo = f"{userinfo}:{parsed.password}"
|
|
41
|
+
userinfo = f"{userinfo}@"
|
|
42
|
+
port = f":{parsed.port}" if parsed.port is not None else ""
|
|
43
|
+
return urlunsplit(
|
|
44
|
+
(parsed.scheme, f"{userinfo}{mds_host}{port}", parsed.path, parsed.query, parsed.fragment)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
20
48
|
# ``dict`` base mirrors ``ape.cli.options.ApeCliContextObject`` so ape's
|
|
21
49
|
# ``network_option`` callback can do ``ctx.obj["network"] = value`` without
|
|
22
50
|
# importing ape here (which would slow down ``tplus --help``).
|
|
23
51
|
class CLIContext(dict):
|
|
24
52
|
def __init__(self):
|
|
25
53
|
super().__init__()
|
|
54
|
+
self.api_base_url: str | None = None
|
|
26
55
|
self.orderbook_base_url: str | None = None
|
|
27
56
|
self.clearing_base_url: str | None = None
|
|
28
57
|
self.market_data_base_url: str | None = None
|
|
58
|
+
self.blockchain_base_url: str | None = None
|
|
29
59
|
self.account: str | None = None
|
|
30
60
|
self.ignore_ssl: bool = False
|
|
31
61
|
|
|
@@ -49,22 +79,44 @@ class CLIContext(dict):
|
|
|
49
79
|
manager = self.user_manager
|
|
50
80
|
return manager._default_user or next(iter(manager.usernames), None)
|
|
51
81
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
82
|
+
def _api_base_url(self) -> str | None:
|
|
83
|
+
"""The single T+ base URL (the OMS). Per-service URLs derive from it; an
|
|
84
|
+
explicit per-service flag / env var overrides it."""
|
|
85
|
+
return self.api_base_url or os.environ.get("TPLUS_API_BASE_URL")
|
|
86
|
+
|
|
87
|
+
def _resolved_orderbook_url(self) -> str:
|
|
88
|
+
# The OMS host *is* the orderbook service, so TPLUS_API_BASE_URL is used directly.
|
|
89
|
+
url = self.orderbook_base_url or self._api_base_url()
|
|
90
|
+
if not url:
|
|
91
|
+
raise click.UsageError(
|
|
92
|
+
"No T+ base URL. Set TPLUS_API_BASE_URL (the OMS URL — the single T+ URL "
|
|
93
|
+
"everything derives from), or pass --orderbook-base-url / set "
|
|
94
|
+
"TPLUS_ORDERBOOK_BASE_URL to override."
|
|
95
|
+
)
|
|
96
|
+
return url
|
|
97
|
+
|
|
98
|
+
def _resolved_market_data_url(self) -> str:
|
|
99
|
+
if self.market_data_base_url:
|
|
100
|
+
return self.market_data_base_url
|
|
101
|
+
api = self._api_base_url()
|
|
102
|
+
if not api:
|
|
56
103
|
raise click.UsageError(
|
|
57
|
-
"No
|
|
58
|
-
"
|
|
104
|
+
"No T+ base URL. Set TPLUS_API_BASE_URL (the market-data URL is derived "
|
|
105
|
+
"by swapping the host label oms->mds), or pass --market-data-base-url / "
|
|
106
|
+
"set TPLUS_MARKET_DATA_BASE_URL to override."
|
|
59
107
|
)
|
|
108
|
+
return _derive_market_data_base_url(api)
|
|
60
109
|
|
|
110
|
+
def orderbook_client(
|
|
111
|
+
self, alias: str | None = None, *, anonymous: bool = False
|
|
112
|
+
) -> "OrderBookClient":
|
|
61
113
|
from tplus.client.auth import Auth
|
|
62
114
|
from tplus.client.orderbook import OrderBookClient
|
|
63
115
|
from tplus.utils.user.model import User
|
|
64
116
|
|
|
65
117
|
user = User() if anonymous else self.load_user(alias)
|
|
66
118
|
return OrderBookClient(
|
|
67
|
-
base_url=self.
|
|
119
|
+
base_url=self._resolved_orderbook_url(),
|
|
68
120
|
default_user=user,
|
|
69
121
|
auth=Auth(cache_dir=_AUTH_CACHE_DIR),
|
|
70
122
|
insecure_ssl=self.ignore_ssl,
|
|
@@ -90,12 +142,7 @@ class CLIContext(dict):
|
|
|
90
142
|
)
|
|
91
143
|
|
|
92
144
|
def _oms_base_url(self) -> str:
|
|
93
|
-
|
|
94
|
-
raise click.UsageError(
|
|
95
|
-
"No orderbook base URL specified. "
|
|
96
|
-
"Pass --orderbook-base-url or set TPLUS_ORDERBOOK_BASE_URL."
|
|
97
|
-
)
|
|
98
|
-
return self.orderbook_base_url
|
|
145
|
+
return self._resolved_orderbook_url()
|
|
99
146
|
|
|
100
147
|
def withdrawal_client(self, alias: str | None = None) -> "WithdrawalClient":
|
|
101
148
|
from tplus.client.withdrawal import WithdrawalClient
|
|
@@ -106,17 +153,22 @@ class CLIContext(dict):
|
|
|
106
153
|
insecure_ssl=self.ignore_ssl,
|
|
107
154
|
)
|
|
108
155
|
|
|
109
|
-
def
|
|
110
|
-
if not self.
|
|
156
|
+
def blockchain_client(self) -> "BlockchainClient":
|
|
157
|
+
if not self.blockchain_base_url:
|
|
111
158
|
raise click.UsageError(
|
|
112
|
-
"No
|
|
113
|
-
"Pass --
|
|
159
|
+
"No blockchain client base URL specified. "
|
|
160
|
+
"Pass --blockchain-base-url or set TPLUS_BLOCKCHAIN_BASE_URL."
|
|
114
161
|
)
|
|
115
162
|
|
|
163
|
+
from tplus.client.blockchain import BlockchainClient
|
|
164
|
+
|
|
165
|
+
return BlockchainClient(base_url=self.blockchain_base_url, insecure_ssl=self.ignore_ssl)
|
|
166
|
+
|
|
167
|
+
def market_data_client(self) -> "MarketDataClient":
|
|
116
168
|
from tplus.client.market_data import MarketDataClient
|
|
117
169
|
|
|
118
170
|
return MarketDataClient(
|
|
119
|
-
base_url=self.
|
|
171
|
+
base_url=self._resolved_market_data_url(),
|
|
120
172
|
insecure_ssl=self.ignore_ssl,
|
|
121
173
|
)
|
|
122
174
|
|
|
@@ -164,6 +216,16 @@ def market_data_url_option() -> Callable:
|
|
|
164
216
|
)
|
|
165
217
|
|
|
166
218
|
|
|
219
|
+
def blockchain_url_option() -> Callable:
|
|
220
|
+
return click.option(
|
|
221
|
+
"--blockchain-base-url",
|
|
222
|
+
envvar="TPLUS_BLOCKCHAIN_BASE_URL",
|
|
223
|
+
help="T+ blockchain client base URL.",
|
|
224
|
+
callback=_set_on_ctx("blockchain_base_url"),
|
|
225
|
+
expose_value=False,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
167
229
|
def tplus_account_option() -> Callable:
|
|
168
230
|
def callback(ctx: click.Context, _param, value):
|
|
169
231
|
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()
|
|
@@ -20,7 +20,17 @@ def env(output_format: str):
|
|
|
20
20
|
"""Show CLI-relevant environment variables and their current values."""
|
|
21
21
|
entries: list[tuple[str, str, str]] = [
|
|
22
22
|
("TPLUS_ACCOUNT", _default_account(), "Default local account alias."),
|
|
23
|
-
(
|
|
23
|
+
(
|
|
24
|
+
"TPLUS_API_BASE_URL",
|
|
25
|
+
"",
|
|
26
|
+
"Single T+ base URL (OMS); orderbook + market-data derive from it.",
|
|
27
|
+
),
|
|
28
|
+
("TPLUS_ORDERBOOK_BASE_URL", "", "Override: orderbook service base URL."),
|
|
29
|
+
(
|
|
30
|
+
"TPLUS_MARKET_DATA_BASE_URL",
|
|
31
|
+
"",
|
|
32
|
+
"Override: market-data URL (else derived from API URL, oms->mds).",
|
|
33
|
+
),
|
|
24
34
|
("TPLUS_CLEARING_BASE_URL", "", "Clearing engine base URL."),
|
|
25
35
|
("TPLUS_IGNORE_SSL", "false", "Skip TLS certificate verification."),
|
|
26
36
|
("TPLUS_OUTPUT_FORMAT", "table", "Default output format (table | json)."),
|
|
@@ -11,6 +11,7 @@ from tplus._cli._context import (
|
|
|
11
11
|
pass_cli_context,
|
|
12
12
|
tplus_account_option,
|
|
13
13
|
)
|
|
14
|
+
from tplus.asset_metadata import asset_metadata_dict
|
|
14
15
|
from tplus.cli_tools import (
|
|
15
16
|
echo_with_pager,
|
|
16
17
|
ignore_ssl_option,
|
|
@@ -71,9 +72,13 @@ def _list(cli_ctx: CLIContext, output_format: str, no_pager: bool):
|
|
|
71
72
|
records = []
|
|
72
73
|
for market in response or []:
|
|
73
74
|
fee_schedule = market.get("fee_schedule") or {}
|
|
75
|
+
asset_metadata = asset_metadata_dict(market.get("asset_id")) or {}
|
|
74
76
|
records.append(
|
|
75
77
|
{
|
|
76
78
|
"asset_id": market.get("asset_id"),
|
|
79
|
+
"symbol": asset_metadata.get("symbol"),
|
|
80
|
+
"asset_class": asset_metadata.get("asset_class"),
|
|
81
|
+
"representations": asset_metadata.get("representations"),
|
|
77
82
|
"price_decimals": market.get("book_price_decimals"),
|
|
78
83
|
"quantity_decimals": market.get("book_quantity_decimals"),
|
|
79
84
|
"max_leverage": market.get("max_leverage"),
|
|
@@ -136,9 +141,9 @@ def _klines(
|
|
|
136
141
|
)
|
|
137
142
|
if output_format == "raw":
|
|
138
143
|
echo_with_pager(
|
|
139
|
-
[kline.model_dump_json() for kline in klines],
|
|
144
|
+
[kline.model_dump_json() for kline in klines.items],
|
|
140
145
|
no_pager=no_pager,
|
|
141
146
|
)
|
|
142
147
|
return
|
|
143
148
|
|
|
144
|
-
render([kline.model_dump() for kline in klines], output_format, no_pager=no_pager)
|
|
149
|
+
render([kline.model_dump() for kline in klines.items], output_format, no_pager=no_pager)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Static T+ asset metadata used for display-only symbol resolution.
|
|
2
|
+
|
|
3
|
+
The protocol identifies assets by ``AssetIdentifier`` values, not symbols. This
|
|
4
|
+
module is a temporary local snapshot so CLI and onboarding output can show the
|
|
5
|
+
canonical symbol next to known production indexes and chain-address aliases.
|
|
6
|
+
|
|
7
|
+
Source snapshot: ``tplus-risk-params`` ``main:environments/prod/params.json``.
|
|
8
|
+
Do not use this for risk checks; read live registry/market endpoints for trading
|
|
9
|
+
parameters.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from tplus.model.asset_identifier import AssetIdentifier
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class AssetMetadata:
|
|
22
|
+
index: int
|
|
23
|
+
symbol: str
|
|
24
|
+
asset_class: str
|
|
25
|
+
representations: tuple[str, ...]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
CANONICAL_ASSETS: dict[int, AssetMetadata] = {
|
|
29
|
+
0: AssetMetadata(0, "USDC", "USD", ("USDC", "USDT")),
|
|
30
|
+
1: AssetMetadata(1, "ETH", "ETH", ("WETH",)),
|
|
31
|
+
2: AssetMetadata(2, "BTC", "BTC", ("WBTC",)),
|
|
32
|
+
3: AssetMetadata(3, "ARB", "ARB", ("ARB",)),
|
|
33
|
+
4: AssetMetadata(4, "MON", "MON", ("MON",)),
|
|
34
|
+
5: AssetMetadata(5, "GOLD", "GOLD", ("XAUT",)),
|
|
35
|
+
6: AssetMetadata(6, "CIGS", "CIGS", ("CIGS",)),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
CHAIN_ASSET_INDEXES: dict[str, int] = {
|
|
40
|
+
# USDC / USDT -> USD class.
|
|
41
|
+
"a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000@000000000000000001": 0,
|
|
42
|
+
"af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000@00000000000000a4b1": 0,
|
|
43
|
+
"754704bc059f8c67012fed69bc8a327a5aafb603000000000000000000000000@00000000000000008f": 0,
|
|
44
|
+
"dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000@000000000000000001": 0,
|
|
45
|
+
"833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000@000000000000002105": 0,
|
|
46
|
+
# ETH.
|
|
47
|
+
"82af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000@00000000000000a4b1": 1,
|
|
48
|
+
"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000@000000000000000001": 1,
|
|
49
|
+
"ee8c0e9f1bffb4eb878d8f15f368a02a35481242000000000000000000000000@00000000000000008f": 1,
|
|
50
|
+
"4200000000000000000000000000000000000006000000000000000000000000@000000000000002105": 1,
|
|
51
|
+
# BTC.
|
|
52
|
+
"2260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000@000000000000000001": 2,
|
|
53
|
+
"2f2a2543b76a4166549f7aab2e75bef0aefc5b0f000000000000000000000000@00000000000000a4b1": 2,
|
|
54
|
+
"0555e30da8f98308edb960aa94c0db47230d2b9c000000000000000000000000@00000000000000008f": 2,
|
|
55
|
+
# ARB.
|
|
56
|
+
"b50721bcf8d664c30412cfbc6cf7a15145234ad1000000000000000000000000@000000000000000001": 3,
|
|
57
|
+
"912ce59144191c1204e64559fe8253a0e49e6548000000000000000000000000@00000000000000a4b1": 3,
|
|
58
|
+
# MON.
|
|
59
|
+
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000@00000000000000008f": 4,
|
|
60
|
+
# GOLD / XAUT.
|
|
61
|
+
"68749665ff8d2d112fa859aa293f07a622782f38000000000000000000000000@000000000000000001": 5,
|
|
62
|
+
# CIGS.
|
|
63
|
+
"2e9126f66e386879f312287a419b0f70004c3f5b000000000000000000000000@000000000000002105": 6,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _asset_id_key(asset_id: Any) -> str:
|
|
68
|
+
if isinstance(asset_id, AssetIdentifier):
|
|
69
|
+
return str(asset_id)
|
|
70
|
+
if isinstance(asset_id, int):
|
|
71
|
+
return str(asset_id)
|
|
72
|
+
if isinstance(asset_id, dict):
|
|
73
|
+
return str(AssetIdentifier.model_validate(asset_id))
|
|
74
|
+
return str(asset_id).lower().removeprefix("0x")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def asset_index(asset_id: Any) -> int | None:
|
|
78
|
+
"""Return the canonical asset index for a known index/address identifier."""
|
|
79
|
+
key = _asset_id_key(asset_id)
|
|
80
|
+
if key.isnumeric():
|
|
81
|
+
return int(key)
|
|
82
|
+
if "@" in key:
|
|
83
|
+
try:
|
|
84
|
+
key = str(AssetIdentifier(key))
|
|
85
|
+
except ValueError:
|
|
86
|
+
pass
|
|
87
|
+
return CHAIN_ASSET_INDEXES.get(key)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_asset_metadata(asset_id: Any) -> AssetMetadata | None:
|
|
92
|
+
index = asset_index(asset_id)
|
|
93
|
+
if index is None:
|
|
94
|
+
return None
|
|
95
|
+
return CANONICAL_ASSETS.get(index)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def asset_label(asset_id: Any) -> str:
|
|
99
|
+
"""Human label for a known asset identifier, falling back without guessing."""
|
|
100
|
+
key = _asset_id_key(asset_id)
|
|
101
|
+
metadata = get_asset_metadata(key)
|
|
102
|
+
if metadata is None:
|
|
103
|
+
return f"asset {key}"
|
|
104
|
+
return f"{metadata.symbol} (asset {key})"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def asset_metadata_dict(asset_id: Any) -> dict[str, Any] | None:
|
|
108
|
+
metadata = get_asset_metadata(asset_id)
|
|
109
|
+
if metadata is None:
|
|
110
|
+
return None
|
|
111
|
+
return {
|
|
112
|
+
"index": metadata.index,
|
|
113
|
+
"symbol": metadata.symbol,
|
|
114
|
+
"asset_class": metadata.asset_class,
|
|
115
|
+
"representations": list(metadata.representations),
|
|
116
|
+
}
|
|
@@ -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
|
"""
|