typed-grvt 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. typed_grvt-0.1.0/LICENSE +21 -0
  2. typed_grvt-0.1.0/PKG-INFO +111 -0
  3. typed_grvt-0.1.0/README.md +88 -0
  4. typed_grvt-0.1.0/pyproject.toml +32 -0
  5. typed_grvt-0.1.0/setup.cfg +4 -0
  6. typed_grvt-0.1.0/src/typed_grvt/__init__.py +15 -0
  7. typed_grvt-0.1.0/src/typed_grvt/__init__.pyi +23 -0
  8. typed_grvt-0.1.0/src/typed_grvt/core/__init__.py +5 -0
  9. typed_grvt-0.1.0/src/typed_grvt/core/__init__.pyi +73 -0
  10. typed_grvt-0.1.0/src/typed_grvt/core/auth.py +283 -0
  11. typed_grvt-0.1.0/src/typed_grvt/core/base.py +164 -0
  12. typed_grvt-0.1.0/src/typed_grvt/core/endpoint/__init__.py +1 -0
  13. typed_grvt-0.1.0/src/typed_grvt/core/endpoint/edge.py +93 -0
  14. typed_grvt-0.1.0/src/typed_grvt/core/endpoint/gateway.py +188 -0
  15. typed_grvt-0.1.0/src/typed_grvt/core/env.py +57 -0
  16. typed_grvt-0.1.0/src/typed_grvt/core/envelope.py +245 -0
  17. typed_grvt-0.1.0/src/typed_grvt/core/exc.py +23 -0
  18. typed_grvt-0.1.0/src/typed_grvt/core/locks.py +37 -0
  19. typed_grvt-0.1.0/src/typed_grvt/core/lookup.py +146 -0
  20. typed_grvt-0.1.0/src/typed_grvt/core/owners.py +121 -0
  21. typed_grvt-0.1.0/src/typed_grvt/core/signing.py +494 -0
  22. typed_grvt-0.1.0/src/typed_grvt/core/transport/__init__.py +1 -0
  23. typed_grvt-0.1.0/src/typed_grvt/core/transport/edge.py +119 -0
  24. typed_grvt-0.1.0/src/typed_grvt/core/transport/gateway.py +536 -0
  25. typed_grvt-0.1.0/src/typed_grvt/core/transport/ws.py +375 -0
  26. typed_grvt-0.1.0/src/typed_grvt/core/types.py +62 -0
  27. typed_grvt-0.1.0/src/typed_grvt/core/wire.py +247 -0
  28. typed_grvt-0.1.0/src/typed_grvt/edge/__init__.py +32 -0
  29. typed_grvt-0.1.0/src/typed_grvt/edge/bridge_withdrawal_info.py +58 -0
  30. typed_grvt-0.1.0/src/typed_grvt/edge/bridge_withdrawal_quote.py +82 -0
  31. typed_grvt-0.1.0/src/typed_grvt/edge/bridge_withdrawal_status.py +61 -0
  32. typed_grvt-0.1.0/src/typed_grvt/edge/deposit_addresses.py +40 -0
  33. typed_grvt-0.1.0/src/typed_grvt/edge/generate_deposit_address.py +40 -0
  34. typed_grvt-0.1.0/src/typed_grvt/edge/indirect_referral_data.py +146 -0
  35. typed_grvt-0.1.0/src/typed_grvt/edge/investment_strategies.py +101 -0
  36. typed_grvt-0.1.0/src/typed_grvt/edge/referral_data.py +150 -0
  37. typed_grvt-0.1.0/src/typed_grvt/edge/referral_epochs.py +48 -0
  38. typed_grvt-0.1.0/src/typed_grvt/edge/referral_points.py +57 -0
  39. typed_grvt-0.1.0/src/typed_grvt/edge/rhino_chain_id.py +50 -0
  40. typed_grvt-0.1.0/src/typed_grvt/edge/schemas.py +55 -0
  41. typed_grvt-0.1.0/src/typed_grvt/main.py +61 -0
  42. typed_grvt-0.1.0/src/typed_grvt/market/__init__.py +40 -0
  43. typed_grvt-0.1.0/src/typed_grvt/market/all_instruments.py +55 -0
  44. typed_grvt-0.1.0/src/typed_grvt/market/candlesticks.py +183 -0
  45. typed_grvt-0.1.0/src/typed_grvt/market/collateral_asset_info.py +63 -0
  46. typed_grvt-0.1.0/src/typed_grvt/market/currency.py +53 -0
  47. typed_grvt-0.1.0/src/typed_grvt/market/funding_rate.py +148 -0
  48. typed_grvt-0.1.0/src/typed_grvt/market/instrument.py +43 -0
  49. typed_grvt-0.1.0/src/typed_grvt/market/instruments.py +73 -0
  50. typed_grvt-0.1.0/src/typed_grvt/market/margin_rules.py +73 -0
  51. typed_grvt-0.1.0/src/typed_grvt/market/mini_ticker.py +43 -0
  52. typed_grvt-0.1.0/src/typed_grvt/market/orderbook.py +47 -0
  53. typed_grvt-0.1.0/src/typed_grvt/market/supported_assets.py +58 -0
  54. typed_grvt-0.1.0/src/typed_grvt/market/ticker.py +43 -0
  55. typed_grvt-0.1.0/src/typed_grvt/market/time.py +37 -0
  56. typed_grvt-0.1.0/src/typed_grvt/market/trade_history.py +121 -0
  57. typed_grvt-0.1.0/src/typed_grvt/market/trades.py +50 -0
  58. typed_grvt-0.1.0/src/typed_grvt/market_streams/__init__.py +26 -0
  59. typed_grvt-0.1.0/src/typed_grvt/market_streams/candlesticks.py +41 -0
  60. typed_grvt-0.1.0/src/typed_grvt/market_streams/mini_ticker.py +34 -0
  61. typed_grvt-0.1.0/src/typed_grvt/market_streams/mini_ticker_delta.py +34 -0
  62. typed_grvt-0.1.0/src/typed_grvt/market_streams/orderbook.py +36 -0
  63. typed_grvt-0.1.0/src/typed_grvt/market_streams/orderbook_delta.py +34 -0
  64. typed_grvt-0.1.0/src/typed_grvt/market_streams/ticker.py +34 -0
  65. typed_grvt-0.1.0/src/typed_grvt/market_streams/ticker_delta.py +34 -0
  66. typed_grvt-0.1.0/src/typed_grvt/market_streams/trades.py +48 -0
  67. typed_grvt-0.1.0/src/typed_grvt/py.typed +0 -0
  68. typed_grvt-0.1.0/src/typed_grvt/schemas.py +1024 -0
  69. typed_grvt-0.1.0/src/typed_grvt/trading/__init__.py +88 -0
  70. typed_grvt-0.1.0/src/typed_grvt/trading/account_summary.py +94 -0
  71. typed_grvt-0.1.0/src/typed_grvt/trading/aggregated_account_summary.py +62 -0
  72. typed_grvt-0.1.0/src/typed_grvt/trading/all_initial_leverage.py +69 -0
  73. typed_grvt-0.1.0/src/typed_grvt/trading/authorized_builders.py +56 -0
  74. typed_grvt-0.1.0/src/typed_grvt/trading/builder_fill_history.py +141 -0
  75. typed_grvt-0.1.0/src/typed_grvt/trading/bulk_orders.py +107 -0
  76. typed_grvt-0.1.0/src/typed_grvt/trading/cancel_all_orders.py +64 -0
  77. typed_grvt-0.1.0/src/typed_grvt/trading/cancel_on_disconnect.py +52 -0
  78. typed_grvt-0.1.0/src/typed_grvt/trading/cancel_order.py +63 -0
  79. typed_grvt-0.1.0/src/typed_grvt/trading/cancel_rfq.py +50 -0
  80. typed_grvt-0.1.0/src/typed_grvt/trading/collateral_preference.py +63 -0
  81. typed_grvt-0.1.0/src/typed_grvt/trading/create_order.py +43 -0
  82. typed_grvt-0.1.0/src/typed_grvt/trading/create_rfq.py +50 -0
  83. typed_grvt-0.1.0/src/typed_grvt/trading/deposit_history.py +153 -0
  84. typed_grvt-0.1.0/src/typed_grvt/trading/ecn_from_broker.py +74 -0
  85. typed_grvt-0.1.0/src/typed_grvt/trading/fill_history.py +150 -0
  86. typed_grvt-0.1.0/src/typed_grvt/trading/funding_account_summary.py +47 -0
  87. typed_grvt-0.1.0/src/typed_grvt/trading/funding_payment_history.py +177 -0
  88. typed_grvt-0.1.0/src/typed_grvt/trading/interest_payment_history.py +149 -0
  89. typed_grvt-0.1.0/src/typed_grvt/trading/open_orders.py +67 -0
  90. typed_grvt-0.1.0/src/typed_grvt/trading/order.py +59 -0
  91. typed_grvt-0.1.0/src/typed_grvt/trading/order_history.py +150 -0
  92. typed_grvt-0.1.0/src/typed_grvt/trading/position_history.py +240 -0
  93. typed_grvt-0.1.0/src/typed_grvt/trading/position_margin_limits.py +61 -0
  94. typed_grvt-0.1.0/src/typed_grvt/trading/positions.py +67 -0
  95. typed_grvt-0.1.0/src/typed_grvt/trading/set_derisk_mm_ratio.py +64 -0
  96. typed_grvt-0.1.0/src/typed_grvt/trading/set_indicative_prices.py +59 -0
  97. typed_grvt-0.1.0/src/typed_grvt/trading/set_initial_leverage.py +61 -0
  98. typed_grvt-0.1.0/src/typed_grvt/trading/spot_account_summary.py +60 -0
  99. typed_grvt-0.1.0/src/typed_grvt/trading/sub_accounts.py +44 -0
  100. typed_grvt-0.1.0/src/typed_grvt/trading/transfer.py +107 -0
  101. typed_grvt-0.1.0/src/typed_grvt/trading/transfer_history.py +150 -0
  102. typed_grvt-0.1.0/src/typed_grvt/trading/vault_investor_summary.py +72 -0
  103. typed_grvt-0.1.0/src/typed_grvt/trading/vault_manager_investor_history.py +87 -0
  104. typed_grvt-0.1.0/src/typed_grvt/trading/vault_redeem_cancel.py +43 -0
  105. typed_grvt-0.1.0/src/typed_grvt/trading/vault_redemption_queue.py +61 -0
  106. typed_grvt-0.1.0/src/typed_grvt/trading/withdrawal.py +68 -0
  107. typed_grvt-0.1.0/src/typed_grvt/trading/withdrawal_fee.py +52 -0
  108. typed_grvt-0.1.0/src/typed_grvt/trading/withdrawal_history.py +156 -0
  109. typed_grvt-0.1.0/src/typed_grvt/trading_streams/__init__.py +32 -0
  110. typed_grvt-0.1.0/src/typed_grvt/trading_streams/cancels.py +61 -0
  111. typed_grvt-0.1.0/src/typed_grvt/trading_streams/deposits.py +46 -0
  112. typed_grvt-0.1.0/src/typed_grvt/trading_streams/ecn_to_broker.py +80 -0
  113. typed_grvt-0.1.0/src/typed_grvt/trading_streams/fills.py +50 -0
  114. typed_grvt-0.1.0/src/typed_grvt/trading_streams/order_states.py +61 -0
  115. typed_grvt-0.1.0/src/typed_grvt/trading_streams/orders.py +50 -0
  116. typed_grvt-0.1.0/src/typed_grvt/trading_streams/positions.py +50 -0
  117. typed_grvt-0.1.0/src/typed_grvt/trading_streams/rfq_quotes.py +66 -0
  118. typed_grvt-0.1.0/src/typed_grvt/trading_streams/rfqs.py +70 -0
  119. typed_grvt-0.1.0/src/typed_grvt/trading_streams/transfers.py +48 -0
  120. typed_grvt-0.1.0/src/typed_grvt/trading_streams/withdrawals.py +48 -0
  121. typed_grvt-0.1.0/src/typed_grvt.egg-info/PKG-INFO +111 -0
  122. typed_grvt-0.1.0/src/typed_grvt.egg-info/SOURCES.txt +123 -0
  123. typed_grvt-0.1.0/src/typed_grvt.egg-info/dependency_links.txt +1 -0
  124. typed_grvt-0.1.0/src/typed_grvt.egg-info/requires.txt +5 -0
  125. typed_grvt-0.1.0/src/typed_grvt.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tribulnation Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: typed-grvt
3
+ Version: 0.1.0
4
+ Summary: A fully typed, validated async client for the GRVT API.
5
+ Author-email: Marcel Claramunt <marcel@tribulnation.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/tribulnation/typed
8
+ Project-URL: Documentation, https://tribulnation.com/typed/grvt
9
+ Project-URL: Website, https://tribulnation.com/typed
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: typed-core>=0.13
18
+ Requires-Dist: lazy-loader
19
+ Requires-Dist: eth-account>=0.13
20
+ Requires-Dist: httpx>=0.26
21
+ Requires-Dist: pydantic>=2.7
22
+ Dynamic: license-file
23
+
24
+ # Typed GRVT
25
+
26
+ <p align="center">
27
+ <em>A fully typed, validated async client for the GRVT API.</em>
28
+ </p>
29
+
30
+ <p align="center">
31
+ <a href="https://pypi.org/project/typed-grvt/">
32
+ <img src="https://img.shields.io/pypi/v/typed-grvt.svg" alt="PyPI version">
33
+ </a>
34
+ <a href="https://pypi.org/project/typed-grvt/">
35
+ <img src="https://img.shields.io/pypi/pyversions/typed-grvt.svg" alt="Python versions">
36
+ </a>
37
+ <a href="https://tribulnation.com/typed/grvt">
38
+ <img src="https://img.shields.io/badge/docs-live-black" alt="Docs">
39
+ </a>
40
+ <a href="LICENSE">
41
+ <img src="https://img.shields.io/pypi/l/typed-grvt.svg" alt="License">
42
+ </a>
43
+ </p>
44
+
45
+ ---
46
+
47
+ - **Documentation**: [https://tribulnation.com/typed/grvt](https://tribulnation.com/typed/grvt)
48
+ - **Source Code**: [https://github.com/tribulnation/typed/tree/main/packages/grvt](https://github.com/tribulnation/typed/tree/main/packages/grvt)
49
+
50
+ ---
51
+
52
+ ```python
53
+ from typed_grvt import Grvt
54
+
55
+ async with Grvt.new(public=True) as client:
56
+ ticker = await client.market.ticker('BTC_USDT_Perp')
57
+ print(ticker.get('mark_price'))
58
+ ```
59
+
60
+ `Grvt.new()` connects to GRVT production. Pass `env='testnet'` (or `env='staging'`) to use a
61
+ test deployment instead.
62
+
63
+ ## Why Typed GRVT?
64
+
65
+ - **🎯 Precise Types**: Typed orders, fills, positions and market data across perpetuals, futures, options, spot swaps and stable perpetuals, not `dict`/`Any`.
66
+ - **✅ Runtime Validation**: Every HTTP reply and WebSocket feed message is validated by default, with a per-call `validate` override.
67
+ - **⚡ Async First**: Async HTTP and JSON-RPC over WebSocket, with a per-call `transport` choice and live streams, built for concurrent workflows.
68
+ - **📚 Full Surface**: Market data, trading, account history, transfers, withdrawals, deposits, RFQs and vault summaries, with session login and EIP-712 signing handled for you.
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ pip install typed-grvt
74
+ ```
75
+
76
+ ## Surfaces
77
+
78
+ | Attribute | What it covers | Credentials |
79
+ | --- | --- | --- |
80
+ | `client.market` | Instruments, currencies, tickers, order books, trades, candles, funding rates | none |
81
+ | `client.market_streams` | Live tickers, order books, trades and candles | none |
82
+ | `client.trading` | Orders, fills, positions, account summaries, transfers, withdrawals, leverage, RFQs, vault summaries, redemption queues and redemption cancels | API key (+ signer key for `create_order`, `bulk_orders`, `transfer`, `withdrawal` and `set_derisk_mm_ratio`) |
83
+ | `client.trading_streams` | Your orders, fills, positions, transfers, deposits and withdrawals, live | API key |
84
+ | `client.edge` | Deposit addresses, bridge withdrawal quotes and status, referrals, strategies | API key (none for `investment_strategies` and `rhino_chain_id`) |
85
+
86
+ ## How To
87
+
88
+ - [Fetch Market Data](https://tribulnation.com/typed/grvt/how-to/fetch-market-data)
89
+ - [Fetch Account Data](https://tribulnation.com/typed/grvt/how-to/fetch-account-data)
90
+ - [Place & Manage Orders](https://tribulnation.com/typed/grvt/how-to/place-and-manage-orders)
91
+ - [Paginate Through Results](https://tribulnation.com/typed/grvt/how-to/paginate-through-results)
92
+ - [Listen To Live Streams](https://tribulnation.com/typed/grvt/how-to/listen-to-live-streams)
93
+ - [Manage Transfers & Withdrawals](https://tribulnation.com/typed/grvt/how-to/manage-transfers-and-withdrawals)
94
+
95
+ ## Reference
96
+
97
+ - [API Keys Setup](https://tribulnation.com/typed/grvt/api-keys)
98
+ - [Async Usage](https://tribulnation.com/typed/grvt/reference/async-usage)
99
+ - [Error Handling](https://tribulnation.com/typed/grvt/reference/error-handling)
100
+ - [Environment Variables](https://tribulnation.com/typed/grvt/reference/env-vars)
101
+ - [Timestamps](https://tribulnation.com/typed/grvt/reference/timestamps)
102
+
103
+ ## Design Philosophy
104
+
105
+ Typed GRVT follows the principles outlined in [this blog post](https://tribulnation.com/blog/clients).
106
+
107
+ *Details matter. Developer experience matters.*
108
+
109
+ ## License
110
+
111
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,88 @@
1
+ # Typed GRVT
2
+
3
+ <p align="center">
4
+ <em>A fully typed, validated async client for the GRVT API.</em>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://pypi.org/project/typed-grvt/">
9
+ <img src="https://img.shields.io/pypi/v/typed-grvt.svg" alt="PyPI version">
10
+ </a>
11
+ <a href="https://pypi.org/project/typed-grvt/">
12
+ <img src="https://img.shields.io/pypi/pyversions/typed-grvt.svg" alt="Python versions">
13
+ </a>
14
+ <a href="https://tribulnation.com/typed/grvt">
15
+ <img src="https://img.shields.io/badge/docs-live-black" alt="Docs">
16
+ </a>
17
+ <a href="LICENSE">
18
+ <img src="https://img.shields.io/pypi/l/typed-grvt.svg" alt="License">
19
+ </a>
20
+ </p>
21
+
22
+ ---
23
+
24
+ - **Documentation**: [https://tribulnation.com/typed/grvt](https://tribulnation.com/typed/grvt)
25
+ - **Source Code**: [https://github.com/tribulnation/typed/tree/main/packages/grvt](https://github.com/tribulnation/typed/tree/main/packages/grvt)
26
+
27
+ ---
28
+
29
+ ```python
30
+ from typed_grvt import Grvt
31
+
32
+ async with Grvt.new(public=True) as client:
33
+ ticker = await client.market.ticker('BTC_USDT_Perp')
34
+ print(ticker.get('mark_price'))
35
+ ```
36
+
37
+ `Grvt.new()` connects to GRVT production. Pass `env='testnet'` (or `env='staging'`) to use a
38
+ test deployment instead.
39
+
40
+ ## Why Typed GRVT?
41
+
42
+ - **🎯 Precise Types**: Typed orders, fills, positions and market data across perpetuals, futures, options, spot swaps and stable perpetuals, not `dict`/`Any`.
43
+ - **✅ Runtime Validation**: Every HTTP reply and WebSocket feed message is validated by default, with a per-call `validate` override.
44
+ - **⚡ Async First**: Async HTTP and JSON-RPC over WebSocket, with a per-call `transport` choice and live streams, built for concurrent workflows.
45
+ - **📚 Full Surface**: Market data, trading, account history, transfers, withdrawals, deposits, RFQs and vault summaries, with session login and EIP-712 signing handled for you.
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install typed-grvt
51
+ ```
52
+
53
+ ## Surfaces
54
+
55
+ | Attribute | What it covers | Credentials |
56
+ | --- | --- | --- |
57
+ | `client.market` | Instruments, currencies, tickers, order books, trades, candles, funding rates | none |
58
+ | `client.market_streams` | Live tickers, order books, trades and candles | none |
59
+ | `client.trading` | Orders, fills, positions, account summaries, transfers, withdrawals, leverage, RFQs, vault summaries, redemption queues and redemption cancels | API key (+ signer key for `create_order`, `bulk_orders`, `transfer`, `withdrawal` and `set_derisk_mm_ratio`) |
60
+ | `client.trading_streams` | Your orders, fills, positions, transfers, deposits and withdrawals, live | API key |
61
+ | `client.edge` | Deposit addresses, bridge withdrawal quotes and status, referrals, strategies | API key (none for `investment_strategies` and `rhino_chain_id`) |
62
+
63
+ ## How To
64
+
65
+ - [Fetch Market Data](https://tribulnation.com/typed/grvt/how-to/fetch-market-data)
66
+ - [Fetch Account Data](https://tribulnation.com/typed/grvt/how-to/fetch-account-data)
67
+ - [Place & Manage Orders](https://tribulnation.com/typed/grvt/how-to/place-and-manage-orders)
68
+ - [Paginate Through Results](https://tribulnation.com/typed/grvt/how-to/paginate-through-results)
69
+ - [Listen To Live Streams](https://tribulnation.com/typed/grvt/how-to/listen-to-live-streams)
70
+ - [Manage Transfers & Withdrawals](https://tribulnation.com/typed/grvt/how-to/manage-transfers-and-withdrawals)
71
+
72
+ ## Reference
73
+
74
+ - [API Keys Setup](https://tribulnation.com/typed/grvt/api-keys)
75
+ - [Async Usage](https://tribulnation.com/typed/grvt/reference/async-usage)
76
+ - [Error Handling](https://tribulnation.com/typed/grvt/reference/error-handling)
77
+ - [Environment Variables](https://tribulnation.com/typed/grvt/reference/env-vars)
78
+ - [Timestamps](https://tribulnation.com/typed/grvt/reference/timestamps)
79
+
80
+ ## Design Philosophy
81
+
82
+ Typed GRVT follows the principles outlined in [this blog post](https://tribulnation.com/blog/clients).
83
+
84
+ *Details matter. Developer experience matters.*
85
+
86
+ ## License
87
+
88
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "typed-grvt"
7
+ version = "0.1.0"
8
+ authors = [
9
+ {name="Marcel Claramunt", email="marcel@tribulnation.com"}
10
+ ]
11
+ description = "A fully typed, validated async client for the GRVT API."
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = [
21
+ "typed-core>=0.13", "lazy-loader", "eth-account>=0.13", "httpx>=0.26", "pydantic>=2.7",
22
+ ]
23
+ requires-python = ">=3.10"
24
+ readme = {file="README.md", content-type="text/markdown"}
25
+
26
+ [tool.setuptools.package-data]
27
+ "typed_grvt" = ["py.typed", "*.pyi", "**/*.pyi"]
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/tribulnation/typed"
31
+ Documentation = "https://tribulnation.com/typed/grvt"
32
+ Website = "https://tribulnation.com/typed"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ """A fully typed, validated async client for the GRVT API.
2
+
3
+ Examples:
4
+ ```python
5
+ from typed_grvt import Grvt
6
+
7
+ async with Grvt.new(public=True) as client:
8
+ instrument = await client.market.instrument('BTC_USDT_Perp')
9
+ print(instrument['tick_size'])
10
+ ```
11
+ """
12
+
13
+ import lazy_loader as lazy
14
+
15
+ __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
@@ -0,0 +1,23 @@
1
+ from .core import (
2
+ ApiError,
3
+ AuthError,
4
+ BadRequest,
5
+ Error,
6
+ LogicError,
7
+ NetworkError,
8
+ RateLimited,
9
+ ValidationError,
10
+ )
11
+ from .main import Grvt
12
+
13
+ __all__ = [
14
+ 'Grvt',
15
+ 'Error',
16
+ 'NetworkError',
17
+ 'ValidationError',
18
+ 'ApiError',
19
+ 'BadRequest',
20
+ 'AuthError',
21
+ 'RateLimited',
22
+ 'LogicError',
23
+ ]
@@ -0,0 +1,5 @@
1
+ """GRVT client core: transports, session authentication, EIP-712 signing and error mapping."""
2
+
3
+ import lazy_loader as lazy
4
+
5
+ __getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
@@ -0,0 +1,73 @@
1
+ from .auth import Credentials, Session, SessionState, resolve_credentials
2
+ from .base import ClientBase
3
+ from .endpoint.edge import EdgeClient, EdgeEndpoint, EdgeMeta
4
+ from .endpoint.gateway import (
5
+ GatewayClient,
6
+ GatewayEndpoint,
7
+ Meta,
8
+ Sign,
9
+ SubAccount,
10
+ Transport,
11
+ )
12
+ from .env import Environment, Hosts
13
+ from .envelope import Unwrap
14
+ from .exc import (
15
+ ApiError,
16
+ AuthError,
17
+ BadRequest,
18
+ Error,
19
+ LogicError,
20
+ NetworkError,
21
+ RateLimited,
22
+ ValidationError,
23
+ )
24
+ from .signing import Signer
25
+ from .transport.edge import EdgeTransport
26
+ from .transport.gateway import GatewayTransport
27
+ from .transport.ws import GatewaySocket
28
+ from .types import (
29
+ TimestampIso,
30
+ TimestampMillis,
31
+ TimestampNanos,
32
+ timestamp_iso,
33
+ timestamp_millis,
34
+ timestamp_nanos,
35
+ )
36
+
37
+ __all__ = [
38
+ 'Credentials',
39
+ 'Session',
40
+ 'SessionState',
41
+ 'resolve_credentials',
42
+ 'ClientBase',
43
+ 'EdgeClient',
44
+ 'EdgeEndpoint',
45
+ 'EdgeMeta',
46
+ 'GatewayClient',
47
+ 'GatewayEndpoint',
48
+ 'Meta',
49
+ 'Sign',
50
+ 'SubAccount',
51
+ 'Transport',
52
+ 'Environment',
53
+ 'Hosts',
54
+ 'Unwrap',
55
+ 'Error',
56
+ 'NetworkError',
57
+ 'ValidationError',
58
+ 'ApiError',
59
+ 'BadRequest',
60
+ 'AuthError',
61
+ 'RateLimited',
62
+ 'LogicError',
63
+ 'Signer',
64
+ 'EdgeTransport',
65
+ 'GatewayTransport',
66
+ 'GatewaySocket',
67
+ 'TimestampNanos',
68
+ 'timestamp_nanos',
69
+ 'TimestampMillis',
70
+ 'timestamp_millis',
71
+ 'TimestampIso',
72
+ 'timestamp_iso',
73
+ ]
@@ -0,0 +1,283 @@
1
+ """Credential resolution and the GRVT session: cookie login, shared by every authenticated transport.
2
+
3
+ GRVT authenticates calls with a session, not per-request signatures:
4
+
5
+ 1. `POST {edge}/auth/api_key/login` with `{"api_key"}` (or `POST {edge}/auth/wallet/login`
6
+ with an EIP-712 `WalletLogin` signature, for a funding wallet without an API key).
7
+ 2. The reply sets a `gravity` cookie (24 h `Max-Age` on testnet) and an `X-Grvt-Account-Id`
8
+ header; the body names the funding account and, for a trading-account key, its
9
+ sub-account.
10
+ 3. Every authenticated call sends `Cookie: gravity=...` and `X-Grvt-Account-Id: ...`.
11
+
12
+ Payload signatures (orders, transfers, withdrawals) are separate: see `signing.py`.
13
+ """
14
+
15
+ from dataclasses import dataclass, field
16
+ from datetime import timedelta
17
+ from http.cookies import CookieError, SimpleCookie
18
+ from email.utils import parsedate_to_datetime
19
+ import os
20
+ import re
21
+ import time
22
+
23
+ import httpx
24
+ from typing_extensions import Any, Literal
25
+
26
+ from typed_core.exceptions import AuthError, ValidationError
27
+ from typed_core.http import HttpClient
28
+
29
+ from .env import Environment
30
+ from .envelope import check_edge
31
+ from .locks import LoopLock
32
+ from .owners import Connection, Owned
33
+ from .signing import Signer
34
+
35
+ ENV_PREFIXES: dict[Environment, str] = {
36
+ 'prod': 'GRVT_',
37
+ 'testnet': 'GRVT_TESTNET_',
38
+ 'staging': 'GRVT_STAGING_',
39
+ }
40
+ """Environment-variable prefix per environment, so a testnet credential can never be read
41
+ for production: `GRVT_TESTNET_API_KEY`, `GRVT_TESTNET_PRIVATE_KEY`,
42
+ `GRVT_TESTNET_TRADING_ACCOUNT_ID` (and `GRVT_API_KEY`, ... for `prod`, the default). The
43
+ last is the default trading sub-account: see `GatewayTransport.sub_account_id()`."""
44
+
45
+ REFRESH_BEFORE = timedelta(minutes=1)
46
+ """Log in again this long before the session cookie expires."""
47
+
48
+ SESSION_COOKIE = 'gravity'
49
+ ACCOUNT_HEADER = 'X-Grvt-Account-Id'
50
+
51
+
52
+ @dataclass(frozen=True, kw_only=True)
53
+ class Credentials:
54
+ """GRVT credentials. Either secret alone can open a session; the private key also signs."""
55
+
56
+ api_key: str | None = field(default=None, repr=False)
57
+ """API key, exchanged for a session cookie at login."""
58
+ private_key: str | None = field(default=None, repr=False)
59
+ """EIP-712 signer key: the API key's signer, or the funding wallet itself."""
60
+ trading_account_id: str | None = None
61
+ """Default trading sub-account id, filled into a trading call that leaves its
62
+ `sub_account_id` out (see `GatewayTransport.sub_account_id()`)."""
63
+ source: Literal['arguments', 'environment'] = 'arguments'
64
+ """Where the secrets came from: `new()`'s arguments, or the environment variables."""
65
+
66
+
67
+ def resolve_credentials(
68
+ env: Environment,
69
+ *,
70
+ api_key: str | None = None,
71
+ private_key: str | None = None,
72
+ trading_account_id: str | None = None,
73
+ public: bool = False,
74
+ ) -> Credentials | None:
75
+ """Resolve the one `Credentials` every authenticated transport of a client shares.
76
+
77
+ Called once, from the root's `.new()`. The secrets come from one source only, so an
78
+ explicit key is never paired with another account's key from the environment:
79
+
80
+ - If `api_key` or `private_key` is passed, the arguments are the whole credential set and
81
+ nothing is read from the environment (`trading_account_id` then defaults to the
82
+ sub-account the API key is scoped to, when not passed).
83
+ - Otherwise all three are read from the environment, under the environment-specific
84
+ prefix (`ENV_PREFIXES`), and an explicit `trading_account_id` overrides
85
+ `{prefix}TRADING_ACCOUNT_ID`.
86
+
87
+ Args:
88
+ env: Target environment, which picks the variable prefix.
89
+ api_key: API key; read from `{prefix}API_KEY` when neither secret is passed.
90
+ private_key: Signer private key; read from `{prefix}PRIVATE_KEY` when neither secret is
91
+ passed.
92
+ trading_account_id: Default trading sub-account id; read from
93
+ `{prefix}TRADING_ACCOUNT_ID` when not passed and neither secret is passed.
94
+ public: Skip resolution and return `None`, for a market-data-only client.
95
+
96
+ Raises:
97
+ AuthError: `public` is false and neither an API key nor a private key was found.
98
+ """
99
+ if public:
100
+ return None
101
+ prefix = ENV_PREFIXES[env]
102
+ source: Literal['arguments', 'environment'] = 'arguments'
103
+ if not api_key and not private_key:
104
+ source = 'environment'
105
+ api_key = os.environ.get(f'{prefix}API_KEY')
106
+ private_key = os.environ.get(f'{prefix}PRIVATE_KEY')
107
+ trading_account_id = trading_account_id or os.environ.get(
108
+ f'{prefix}TRADING_ACCOUNT_ID'
109
+ )
110
+ if not api_key and not private_key:
111
+ raise AuthError(
112
+ f'No GRVT credentials: set {prefix}API_KEY (and {prefix}PRIVATE_KEY to sign orders, '
113
+ 'transfers and withdrawals), pass api_key/private_key, or build with `public=True` '
114
+ 'for market data only.'
115
+ )
116
+ return Credentials(
117
+ api_key=api_key,
118
+ private_key=private_key,
119
+ trading_account_id=trading_account_id,
120
+ source=source,
121
+ )
122
+
123
+
124
+ @dataclass(frozen=True, kw_only=True)
125
+ class SessionState:
126
+ """One logged-in session."""
127
+
128
+ token: str = field(repr=False)
129
+ """`gravity` cookie value."""
130
+ account_id: str | None = field(repr=False)
131
+ """`X-Grvt-Account-Id` header value (an off-chain `ACC:...` id)."""
132
+ expires_at: float
133
+ """Unix seconds at which the cookie expires."""
134
+ funding_account_address: str | None
135
+ """Funding (main) account address the session belongs to."""
136
+ sub_account_id: str | None
137
+ """Trading sub-account id, for a trading-account-level API key."""
138
+
139
+ @property
140
+ def headers(self) -> dict[str, str]:
141
+ """Headers authenticating one call with this session."""
142
+ headers = {'Cookie': f'{SESSION_COOKIE}={self.token}'}
143
+ if self.account_id:
144
+ headers[ACCOUNT_HEADER] = self.account_id
145
+ return headers
146
+
147
+ def fresh(self) -> bool:
148
+ """Whether the cookie is valid for at least `REFRESH_BEFORE` more."""
149
+ return time.time() < self.expires_at - REFRESH_BEFORE.total_seconds()
150
+
151
+
152
+ MAX_AGE = re.compile(r'-?[0-9]+')
153
+ """A valid cookie `Max-Age`: an integer number of seconds (RFC 6265 §5.2.2)."""
154
+
155
+
156
+ def cookie_expiry(response: httpx.Response) -> float:
157
+ """Unix seconds at which the response's `gravity` cookie expires.
158
+
159
+ Reads `Max-Age` first, then `Expires`. An attribute that does not parse is ignored, as
160
+ RFC 6265 has a browser do: a malformed `Max-Age` falls back to `Expires`.
161
+
162
+ Raises:
163
+ ValidationError: The response set no `gravity` cookie, or one with no valid lifetime.
164
+ """
165
+ for header in response.headers.get_list('set-cookie'):
166
+ cookie = SimpleCookie()
167
+ try:
168
+ cookie.load(header)
169
+ except CookieError:
170
+ continue
171
+ if (morsel := cookie.get(SESSION_COOKIE)) is None:
172
+ continue
173
+ max_age = morsel['max-age'].strip()
174
+ if MAX_AGE.fullmatch(max_age):
175
+ return time.time() + int(max_age)
176
+ if morsel['expires']:
177
+ try:
178
+ return parsedate_to_datetime(morsel['expires']).timestamp()
179
+ except (TypeError, ValueError, OverflowError):
180
+ pass
181
+ raise ValidationError('Login response set no gravity cookie with a valid lifetime')
182
+
183
+
184
+ def login_failure(payload: Any) -> AuthError:
185
+ """The error for a login the edge host answered without success (an HTTP `200` whose body
186
+ says `"status": "failure"`, or one that is not an object).
187
+
188
+ Carries `(code, message, body)` when the body names a venue code, else one message.
189
+
190
+ Args:
191
+ payload: The decoded login reply.
192
+ """
193
+ if not isinstance(payload, dict):
194
+ return AuthError(f'Login failed: unexpected reply {payload!r:.500}')
195
+ message = str(
196
+ payload.get('message') or payload.get('error') or payload.get('status') or ''
197
+ )
198
+ if (code := payload.get('code')) is not None:
199
+ return AuthError(code, message, payload)
200
+ return AuthError(f'Login failed: {message or "no reason given"}')
201
+
202
+
203
+ @dataclass(kw_only=True)
204
+ class Session(Owned):
205
+ """Lazily logs in, caches the session and refreshes it ahead of expiry.
206
+
207
+ Shared by every authenticated transport of one client (trading HTTP, trading WebSocket,
208
+ edge), so a single login serves all of them. Concurrent callers wait on one login; the
209
+ lock is per event loop (`LoopLock`), so a client survives being reused across two
210
+ `asyncio.run` calls, provided each one enters it with `async with client`: leaving it
211
+ closes the HTTP clients and sockets, whose pooled connections otherwise stay bound to the
212
+ first loop and fail in the next one with a raw `RuntimeError`.
213
+
214
+ Each transport sharing the session takes one ownership of it while it is owned itself
215
+ (`Owned.shared`), and the login connection closes when the last owner leaves: `async with
216
+ client.trading` alone closes it on exit, while an edge block still open keeps it.
217
+ """
218
+
219
+ edge_url: str
220
+ """Edge host base URL, where the login endpoints live."""
221
+ api_key: str | None = field(default=None, repr=False)
222
+ """API key to log in with; preferred over `signer`."""
223
+ signer: Signer | None = None
224
+ """Funding-wallet signer to log in with when there is no API key."""
225
+ http: HttpClient = field(default_factory=HttpClient)
226
+ """Dedicated client, so the login's `Set-Cookie` never lands in a shared cookie jar."""
227
+ current: SessionState | None = field(default=None, init=False, repr=False)
228
+ lock: LoopLock = field(default_factory=LoopLock, init=False, repr=False)
229
+
230
+ def connections(self) -> list[Connection]:
231
+ """The login connection, closed when the last owner leaves."""
232
+ return [self.http]
233
+
234
+ async def state(self) -> SessionState:
235
+ """The live session, logging in first when there is none or it is about to expire."""
236
+ async with self.lock:
237
+ if self.current is None or not self.current.fresh():
238
+ self.current = await self.login()
239
+ return self.current
240
+
241
+ async def headers(self) -> dict[str, str]:
242
+ """Headers authenticating one call."""
243
+ return (await self.state()).headers
244
+
245
+ def invalidate(self, state: SessionState):
246
+ """Forget `state` after the venue rejected it, unless a newer session already replaced it.
247
+
248
+ Args:
249
+ state: The session the rejected call was sent with.
250
+ """
251
+ if self.current is state:
252
+ self.current = None
253
+
254
+ async def login(self) -> SessionState:
255
+ """Log in with the API key, or with a wallet signature when there is no API key.
256
+
257
+ Raises:
258
+ AuthError: The venue rejected the credentials, or there are none to log in with.
259
+ """
260
+ if self.api_key:
261
+ path, body = '/auth/api_key/login', {'api_key': self.api_key}
262
+ elif self.signer is not None:
263
+ signature = self.signer.wallet_login()
264
+ path, body = (
265
+ '/auth/wallet/login',
266
+ {'address': self.signer.address, 'signature': signature},
267
+ )
268
+ else:
269
+ raise AuthError('No API key or wallet key to log in with')
270
+ response = await self.http.request('POST', self.edge_url + path, json=body)
271
+ payload = check_edge(response)
272
+ if not isinstance(payload, dict) or payload.get('status', 'success') != 'success':
273
+ raise login_failure(payload)
274
+ token = response.cookies.get(SESSION_COOKIE)
275
+ if not token:
276
+ raise ValidationError('Login response set no gravity cookie')
277
+ return SessionState(
278
+ token=token,
279
+ account_id=response.headers.get(ACCOUNT_HEADER),
280
+ expires_at=cookie_expiry(response),
281
+ funding_account_address=payload.get('funding_account_address'),
282
+ sub_account_id=payload.get('sub_account_id'),
283
+ )