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.
Files changed (133) hide show
  1. {tpluspy-0.2.1 → tpluspy-0.2.4}/PKG-INFO +23 -1
  2. {tpluspy-0.2.1 → tpluspy-0.2.4}/README.md +22 -0
  3. {tpluspy-0.2.1 → tpluspy-0.2.4}/setup.py +1 -1
  4. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/_context.py +80 -18
  5. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/debug.py +48 -1
  6. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/env.py +11 -1
  7. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/markets.py +7 -2
  8. tpluspy-0.2.4/tplus/asset_metadata.py +116 -0
  9. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/__init__.py +2 -0
  10. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/base.py +2 -2
  11. tpluspy-0.2.4/tplus/client/blockchain.py +50 -0
  12. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/__init__.py +8 -0
  13. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/admin.py +52 -14
  14. tpluspy-0.2.4/tplus/client/clearingengine/cross_venue.py +36 -0
  15. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/market_data.py +39 -8
  16. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/orderbook.py +164 -6
  17. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/dev/env.py +25 -3
  18. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/settle.py +156 -9
  19. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/klines.py +37 -0
  20. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/order.py +1 -0
  21. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/position.py +1 -1
  22. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/trades.py +2 -0
  23. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/user_margin.py +4 -0
  24. tpluspy-0.2.4/tplus/model/user_simulated_margin.py +73 -0
  25. tpluspy-0.2.4/tplus/utils/operator.py +26 -0
  26. {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/PKG-INFO +23 -1
  27. {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/SOURCES.txt +5 -0
  28. {tpluspy-0.2.1 → tpluspy-0.2.4}/pyproject.toml +0 -0
  29. {tpluspy-0.2.1 → tpluspy-0.2.4}/setup.cfg +0 -0
  30. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/__init__.py +0 -0
  31. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/__init__.py +0 -0
  32. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/__main__.py +0 -0
  33. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/accounts.py +0 -0
  34. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/assets.py +0 -0
  35. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/balance.py +0 -0
  36. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/decimals.py +0 -0
  37. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/deposit.py +0 -0
  38. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/orders.py +0 -0
  39. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/params.py +0 -0
  40. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/settle.py +0 -0
  41. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/sign.py +0 -0
  42. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/stream.py +0 -0
  43. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/trades.py +0 -0
  44. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/vaults.py +0 -0
  45. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/withdraw.py +0 -0
  46. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/_cli/withdrawal.py +0 -0
  47. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/cli_tools/__init__.py +0 -0
  48. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/cli_tools/options.py +0 -0
  49. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/cli_tools/output.py +0 -0
  50. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/auth.py +0 -0
  51. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/admin_settlement.py +0 -0
  52. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/assetregistry.py +0 -0
  53. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/base.py +0 -0
  54. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/decimal.py +0 -0
  55. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/clearingengine/vault.py +0 -0
  56. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/oms/__init__.py +0 -0
  57. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/oms/assetregistry.py +0 -0
  58. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/oms/oms_admin.py +0 -0
  59. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/client/withdrawal.py +0 -0
  60. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/constants.py +0 -0
  61. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/__init__.py +0 -0
  62. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/abi.py +0 -0
  63. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/address.py +0 -0
  64. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/constants.py +0 -0
  65. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/contracts.py +0 -0
  66. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/dev/__init__.py +0 -0
  67. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/dev/contracts.py +0 -0
  68. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/dev/token.py +0 -0
  69. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/exceptions.py +0 -0
  70. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/__init__.py +0 -0
  71. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/chaindata.py +0 -0
  72. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/credential_manager.py +0 -0
  73. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/deposit.py +0 -0
  74. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/evm.py +0 -0
  75. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/registry.py +0 -0
  76. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/vault.py +0 -0
  77. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/managers/withdraw.py +0 -0
  78. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/evm/utils.py +0 -0
  79. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/exceptions.py +0 -0
  80. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/logger.py +0 -0
  81. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/managers/__init__.py +0 -0
  82. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/managers/base.py +0 -0
  83. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/__init__.py +0 -0
  84. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/approval.py +0 -0
  85. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/asset_identifier.py +0 -0
  86. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/batch_order.py +0 -0
  87. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/cancel_order.py +0 -0
  88. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/chain_address.py +0 -0
  89. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/close_all_positions_preview.py +0 -0
  90. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/config.py +0 -0
  91. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/interest_rates.py +0 -0
  92. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/limit_order.py +0 -0
  93. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/market.py +0 -0
  94. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/market_order.py +0 -0
  95. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/multisig.py +0 -0
  96. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/order_trigger.py +0 -0
  97. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/orderbook.py +0 -0
  98. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/replace_order.py +0 -0
  99. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/risk_parameters.py +0 -0
  100. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/settlement.py +0 -0
  101. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/signed_message.py +0 -0
  102. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/types.py +0 -0
  103. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/user_event.py +0 -0
  104. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/user_inventory.py +0 -0
  105. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/user_solvency.py +0 -0
  106. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/model/withdrawal.py +0 -0
  107. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/py.typed +0 -0
  108. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/types.py +0 -0
  109. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/__init__.py +0 -0
  110. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/address.py +0 -0
  111. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/amount.py +0 -0
  112. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/bytes32.py +0 -0
  113. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/decimals.py +0 -0
  114. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/domain.py +0 -0
  115. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/hex.py +0 -0
  116. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/limit_order.py +0 -0
  117. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/market_order.py +0 -0
  118. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/replace_order.py +0 -0
  119. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/serializers.py +0 -0
  120. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/signing.py +0 -0
  121. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/timeout.py +0 -0
  122. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/__init__.py +0 -0
  123. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/decrypt.py +0 -0
  124. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/ed_keyfile.py +0 -0
  125. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/manager.py +0 -0
  126. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/model.py +0 -0
  127. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/token_cache.py +0 -0
  128. {tpluspy-0.2.1 → tpluspy-0.2.4}/tplus/utils/user/validate.py +0 -0
  129. {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/dependency_links.txt +0 -0
  130. {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/entry_points.txt +0 -0
  131. {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/not-zip-safe +0 -0
  132. {tpluspy-0.2.1 → tpluspy-0.2.4}/tpluspy.egg-info/requires.txt +0 -0
  133. {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.1
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.1",
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 orderbook_client(
53
- self, alias: str | None = None, *, anonymous: bool = False
54
- ) -> "OrderBookClient":
55
- if not self.orderbook_base_url:
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 orderbook base URL specified. "
58
- "Pass --orderbook-base-url or set TPLUS_ORDERBOOK_BASE_URL."
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.orderbook_base_url,
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
- if not self.orderbook_base_url:
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 market_data_client(self) -> "MarketDataClient":
110
- if not self.market_data_base_url:
156
+ def blockchain_client(self) -> "BlockchainClient":
157
+ if not self.blockchain_base_url:
111
158
  raise click.UsageError(
112
- "No market-data base URL specified. "
113
- "Pass --market-data-base-url or set TPLUS_MARKET_DATA_BASE_URL."
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.market_data_base_url,
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 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()
@@ -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
- ("TPLUS_ORDERBOOK_BASE_URL", "", "Orderbook service base URL."),
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
- 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
  """