allium-cli 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,306 @@
1
+ Metadata-Version: 2.4
2
+ Name: allium-cli
3
+ Version: 0.2.0
4
+ Summary: CLI for Allium blockchain data APIs
5
+ Project-URL: Documentation, https://docs.allium.so
6
+ Project-URL: Homepage, https://allium.so
7
+ Project-URL: Repository, https://github.com/Allium-Science/allium-cli
8
+ Author-email: Allium <support@allium.so>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Environment :: Console
12
+ Classifier: Framework :: Pydantic :: 2
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Database :: Front-Ends
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: click>=8.1
19
+ Requires-Dist: eth-account~=0.13
20
+ Requires-Dist: httpx~=0.27
21
+ Requires-Dist: privy-client~=0.5
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: pympp[tempo]==0.3
24
+ Requires-Dist: questionary>=2.0
25
+ Requires-Dist: rich-click>=1.9.7
26
+ Requires-Dist: rich>=13.0
27
+ Requires-Dist: tomli-w>=1.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Allium CLI
31
+
32
+ Command-line interface for querying blockchain data across 80+ chains via the [Allium](https://allium.so) platform. Supports realtime token prices, wallet balances, transaction history, and SQL queries against Allium's data warehouse.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ curl -sSL https://raw.githubusercontent.com/Allium-Science/allium-cli/main/install.sh | sh
38
+ ```
39
+
40
+ Or install directly with your preferred package manager:
41
+
42
+ ```bash
43
+ uv tool install allium-cli # recommended
44
+ pipx install allium-cli
45
+ pip install allium-cli
46
+ ```
47
+
48
+ This installs the `allium` command. Run `allium auth setup` to configure authentication.
49
+
50
+ ## Authentication
51
+
52
+ The CLI supports four authentication methods. Run the interactive wizard, or pass arguments directly for scripted/CI setups:
53
+
54
+ ```bash
55
+ # Interactive wizard (arrow-key selection)
56
+ allium auth setup
57
+
58
+ # Non-interactive one-liners
59
+ allium auth setup --method api_key --api-key sk-...
60
+ allium auth setup --method x402_key --private-key 0x... --network eip155:8453
61
+ allium auth setup --method x402_privy \
62
+ --privy-app-id ... --privy-app-secret ... \
63
+ --privy-wallet-id ... --network eip155:8453
64
+ allium auth setup --method tempo --private-key 0x... --chain-id 42431
65
+ ```
66
+
67
+ | Method | Description |
68
+ |---|---|
69
+ | **API Key** | Standard key from [app.allium.so/settings/api-keys](https://app.allium.so/settings/api-keys) |
70
+ | **x402 Private Key** | Pay-per-call with USDC on Base -- no API key needed |
71
+ | **x402 Privy** | x402 via Privy server wallets -- no private key handling |
72
+ | **Tempo MPP** | Tempo micropayment protocol |
73
+
74
+ Optional flags: `--name <profile-name>` (defaults to the method name), `--no-active` (skip setting as active profile).
75
+
76
+ Credentials are stored in `~/.config/allium/credentials.toml` (file permissions restricted to owner).
77
+
78
+ ### Profile management
79
+
80
+ ```bash
81
+ allium auth list # Show all profiles
82
+ allium auth use <name> # Switch active profile
83
+ allium auth remove <name> # Delete a profile
84
+ ```
85
+
86
+ ## Global Options
87
+
88
+ ```
89
+ --profile TEXT Override the active auth profile for this command
90
+ --format [json|table|csv] Output format (default: json)
91
+ -v, --verbose Show progress details (run IDs, spinners, status)
92
+ --help Show help and exit
93
+ ```
94
+
95
+ ## Commands
96
+
97
+ ### `allium realtime` -- Realtime Blockchain Data
98
+
99
+ Query realtime blockchain data with 3-5s freshness across 20+ chains.
100
+
101
+ #### Prices
102
+
103
+ Token prices derived from on-chain DEX trades with VWAP calculation and outlier detection.
104
+
105
+ ```bash
106
+ # Latest minute-level price and OHLC values
107
+ allium realtime prices latest \
108
+ --chain solana --token-address So11111111111111111111111111111111111111112
109
+
110
+ # Price at a specific timestamp
111
+ allium realtime prices at-timestamp \
112
+ --chain ethereum --token-address 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
113
+ --timestamp 2026-01-15T12:00:00Z --time-granularity 1h
114
+
115
+ # Historical price series
116
+ allium realtime prices history \
117
+ --chain ethereum --token-address 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
118
+ --start-timestamp 2026-01-01T00:00:00Z --end-timestamp 2026-01-07T00:00:00Z \
119
+ --time-granularity 1d
120
+
121
+ # 24h/1h price stats (high, low, volume, trade count, percent change)
122
+ allium realtime prices stats \
123
+ --chain solana --token-address So11111111111111111111111111111111111111112
124
+ ```
125
+
126
+ **Options:** `--chain`, `--token-address` (repeatable, paired in order), `--body` (JSON override), `--timestamp`, `--start-timestamp`, `--end-timestamp`, `--time-granularity [15s|1m|5m|1h|1d]`
127
+
128
+ #### Tokens
129
+
130
+ ```bash
131
+ # List top tokens by volume
132
+ allium realtime tokens list --chain ethereum --sort volume --limit 10
133
+
134
+ # Fuzzy search by name or symbol
135
+ allium realtime tokens search -q "USDC"
136
+
137
+ # Exact lookup by chain + contract address
138
+ allium realtime tokens chain-address \
139
+ --chain ethereum --token-address 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
140
+ ```
141
+
142
+ **Options:** `--chain`, `--token-address` (repeatable), `--sort [volume|trade_count|fully_diluted_valuation|address|name]`, `--order [asc|desc]`, `--limit`, `-q/--query`
143
+
144
+ #### Balances
145
+
146
+ ```bash
147
+ # Current token balances for a wallet
148
+ allium realtime balances latest \
149
+ --chain ethereum --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
150
+
151
+ # Historical balance snapshots
152
+ allium realtime balances history \
153
+ --chain ethereum --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \
154
+ --start-timestamp 2026-01-01T00:00:00Z --limit 100
155
+ ```
156
+
157
+ **Options:** `--chain`, `--address` (repeatable, paired), `--start-timestamp`, `--end-timestamp`, `--limit`, `--body`
158
+
159
+ #### Transactions
160
+
161
+ ```bash
162
+ # Wallet transaction activity with decoded activities and labels
163
+ allium realtime transactions \
164
+ --chain ethereum --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \
165
+ --activity-type dex_trade --lookback-days 7 --limit 50
166
+ ```
167
+
168
+ **Options:** `--chain`, `--address` (repeatable), `--activity-type`, `--lookback-days`, `--limit`, `--body`
169
+
170
+ #### PnL
171
+
172
+ ```bash
173
+ # Wallet profit and loss
174
+ allium realtime pnl \
175
+ --chain ethereum --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
176
+
177
+ # With historical breakdown
178
+ allium realtime pnl \
179
+ --chain ethereum --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 \
180
+ --with-historical-breakdown
181
+ ```
182
+
183
+ **Options:** `--chain`, `--address` (repeatable), `--with-historical-breakdown`, `--body`
184
+
185
+ ---
186
+
187
+ ### `allium explorer` -- SQL Query Execution
188
+
189
+ Run SQL queries on Allium's data warehouse. By default, the CLI polls silently and prints results. Use `-v` for progress details.
190
+
191
+ ```bash
192
+ # Execute ad-hoc SQL (requires x402 or Tempo auth)
193
+ allium explorer run-sql "SELECT chain, COUNT(*) FROM crosschain.dex.trades GROUP BY chain LIMIT 10"
194
+
195
+ # Execute SQL from a file
196
+ allium explorer run-sql query.sql --limit 1000
197
+
198
+ # Run a saved query by ID with parameters
199
+ allium explorer run abc123 --param start_date=2026-01-01 --param chain=ethereum
200
+
201
+ # Just get the run ID without waiting
202
+ allium explorer run-sql "SELECT 1" --no-wait
203
+
204
+ # Check status of a query run
205
+ allium explorer status <run_id>
206
+
207
+ # Fetch results of a completed run
208
+ allium explorer results <run_id>
209
+
210
+ # Pipe CSV output directly
211
+ allium --format csv explorer run-sql "SELECT 1" > output.csv
212
+ ```
213
+
214
+ | Command | Description |
215
+ |---|---|
216
+ | `run-sql <SQL_OR_FILE>` | Execute ad-hoc SQL (x402/Tempo auth required) |
217
+ | `run <QUERY_ID>` | Execute a saved Explorer query by ID |
218
+ | `status <RUN_ID>` | Check query run status (created, running, success, failed, canceled) |
219
+ | `results <RUN_ID>` | Download results of a completed run |
220
+
221
+ **Options:** `--limit`, `--no-wait`, `--param key=value` (repeatable), `--compute-profile`
222
+
223
+ ---
224
+
225
+ ### `allium mp` -- Machine Payment Tracking
226
+
227
+ Track costs for x402 and Tempo micropayment API calls. Payments are logged automatically to `~/.config/allium/cost_log.csv`.
228
+
229
+ ```bash
230
+ # Total spend summary grouped by method and network
231
+ allium mp cost
232
+
233
+ # Full itemized payment history
234
+ allium mp cost list
235
+
236
+ # Export as CSV
237
+ allium --format csv mp cost list
238
+
239
+ # Clear the cost log
240
+ allium mp cost clear
241
+ ```
242
+
243
+ | Command | Description |
244
+ |---|---|
245
+ | `mp cost` | Total spend summary (grouped by method/network with call counts) |
246
+ | `mp cost list` | Full itemized history with per-row details |
247
+ | `mp cost clear` | Delete the cost log (with confirmation prompt) |
248
+
249
+ ---
250
+
251
+ ### `allium auth` -- Authentication Management
252
+
253
+ ```bash
254
+ # Interactive setup wizard (arrow-key selection)
255
+ allium auth setup
256
+
257
+ # Non-interactive setup (for scripts/CI)
258
+ allium auth setup --method api_key --api-key sk-...
259
+ allium auth setup --method tempo --private-key 0x... --chain-id 42431
260
+
261
+ # List all configured profiles
262
+ allium auth list
263
+
264
+ # Switch active profile
265
+ allium auth use <name>
266
+
267
+ # Delete a profile
268
+ allium auth remove <name>
269
+ ```
270
+
271
+ ## JSON Body Override
272
+
273
+ All realtime commands support a `--body` flag that accepts either inline JSON or a path to a `.json` file. When provided, it overrides all other options:
274
+
275
+ ```bash
276
+ # Inline JSON
277
+ allium realtime prices latest --body '[{"chain":"solana","token_address":"So111..."}]'
278
+
279
+ # From file
280
+ allium realtime prices latest --body tokens.json
281
+ ```
282
+
283
+ ## Shell Completions
284
+
285
+ Tab-completion is available for all commands, subcommands, and options. Add one of the following to your shell config:
286
+
287
+ ```bash
288
+ # Bash — add to ~/.bashrc
289
+ eval "$(_ALLIUM_COMPLETE=bash_source allium)"
290
+
291
+ # Zsh — add to ~/.zshrc
292
+ eval "$(_ALLIUM_COMPLETE=zsh_source allium)"
293
+
294
+ # Fish — add to ~/.config/fish/config.fish
295
+ _ALLIUM_COMPLETE=fish_source allium | source
296
+ ```
297
+
298
+ Reload your shell to activate completions.
299
+
300
+ ## Documentation
301
+
302
+ Full API documentation: [docs.allium.so](https://docs.allium.so)
303
+
304
+ ## Contributing
305
+
306
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and release instructions.
@@ -0,0 +1,44 @@
1
+ cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cli/main.py,sha256=9eV1TaJhIbVyp6B6i4mfc82dpfgNavwWleGnJYk1DnM,3042
3
+ cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ cli/auth/api_key.py,sha256=Md52mwytMhBfJieNnsqtw8DfDt4N7ayXM4SLFXeu1Rs,184
5
+ cli/auth/tempo.py,sha256=bY-Do6zWTZqZE177ynfS7xz-E3VpjyZymw1dxIOT91U,3309
6
+ cli/auth/x402_key.py,sha256=61BGrcwX-GNSvZgr_C--gWn0mhvUgCNNysfgCHGqetI,1047
7
+ cli/auth/x402_privy.py,sha256=GsBImZk_qGliILA6ITZ8kp6OeJ-YaZs_iZ6BXb1kBHk,832
8
+ cli/clients/__init__.py,sha256=Y0lLc0ZL4-Y24sbul6_G_Tc0N0ygnHDaDRKRs-el7Ps,135
9
+ cli/clients/factory.py,sha256=dvq5r2KuDN9sECVq2AyowgmxUTfVPiJQ7NgJ9jCKOr4,2630
10
+ cli/clients/http.py,sha256=d33N3AnVToHNOgJM3BRat732T-CqpoN1kl5A2vv_GR8,2877
11
+ cli/clients/protocol.py,sha256=Z-SrpRuMulcfPxgb3EDADYwHoxfchch9cKbTl1qsd2I,518
12
+ cli/clients/tempo.py,sha256=mtkjjZFoG8SLxNUp0bwvq28oXwJHHAIff4JGlw12NOM,1606
13
+ cli/clients/x402.py,sha256=_RyvjAvKLcM2esP12pobGeasMxjfx4NDbow0i40B5lY,5929
14
+ cli/commands/__init__.py,sha256=QcGvn8b-5lURzbZh-6RJndY1cbaNqQlBDVNW3XJ9Ywc,448
15
+ cli/commands/auth.py,sha256=39gD2z7EdhyuWgiCC7FCBvK3OLTjUFLnFUib-wQGoFg,12256
16
+ cli/commands/explorer.py,sha256=JX0dlzUYWcH4BszdxTHm5yxpg6mQFth9YDdnsy_BbHQ,8930
17
+ cli/commands/mp.py,sha256=se26EafW2o4gTGURIWfEiF_3pjnE-qgFFWFwGscD4-M,4954
18
+ cli/commands/realtime.py,sha256=VQQaq5g_Tkfwvt35xOwbC_3J28tcST8gDoe5w_Bujus,12312
19
+ cli/constants/__init__.py,sha256=FEM6EUJ2FPh_7s0sdO8ejyz4tpQjzBflAibDXgwrHKU,589
20
+ cli/constants/api.py,sha256=ge9bm1FDueq6IEsugZyQwtsmJiCmlWpc7TWqacwiCR4,126
21
+ cli/constants/config.py,sha256=dUTqHRRZ2ZHfIVYxkd4VuQpnp82AbEKJAykEP4mB5-Q,421
22
+ cli/constants/ui.py,sha256=FDJ-zlo4m_OpYImsC_DcRUkjeE7UD2VtSc_0ww287sU,1371
23
+ cli/types/__init__.py,sha256=WHsiIfqi42_J3bXvQwTrqE7zWI_aToYc0Qwc9Fn0YIw,769
24
+ cli/types/config.py,sha256=ZATqu0LdSlc3myBFljQ7Aduqu7dMbG5noWD0rnZkW2w,885
25
+ cli/types/context.py,sha256=YIhRgSajB4Th1L_qme1Wzn3uj79zQPwu3xS4Lem5o84,347
26
+ cli/types/enums.py,sha256=Czw5kId_mu198T_S0AX3-CyaOfM-zAZ-ePH21mSA4Tw,1196
27
+ cli/types/labels.py,sha256=Ye2FdzvoFT8FWiHHKhqiYJcCzn6LNX5u3Y5tm_G0ssk,693
28
+ cli/types/profiles.py,sha256=abgoV-XFauYn1BdtzXBPelra5YVXz5MlYcrTs77zp2I,1142
29
+ cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ cli/utils/async_cmd.py,sha256=kq-A4pccLChOTYBJ-er9PnQgtGBfUoX5R4YLK6mI39g,339
31
+ cli/utils/body.py,sha256=c9kXdbovnD7efivYxMDnPVtgjE5bqvRAB1mVPOYPTF8,1287
32
+ cli/utils/config.py,sha256=Uaaa7eHvxYQatrUMjeF0x04XzD0fp9al82XSd7L_KSQ,2753
33
+ cli/utils/console.py,sha256=vTMOYCq6FEhOeTxrePCHIzg_goyTI25JWFYUIcRYR08,129
34
+ cli/utils/cost_log.py,sha256=tej5nFR1MyKHGBziQEUNR71z001ioW8gx1FM4XFtJSk,3186
35
+ cli/utils/errors.py,sha256=KZ1sjKRmyixIxASG_ZdilD7toFiknwXZmxYutEiRhJI,2062
36
+ cli/utils/options.py,sha256=tvzSxSObkD-7ycg0HlZKsCGzDZ3tpqcy6oTA6HoHaaU,1220
37
+ cli/utils/output.py,sha256=YvqlPu6dwT3VXQtQqYzgFg8xy1dqTHqdZH23ygGy_Xg,3141
38
+ cli/utils/payment.py,sha256=P5SoOuqvwi5U4eEt4QfvKvY1oErvjLxUqNf01jiSVxU,644
39
+ cli/utils/version.py,sha256=pVT9UaRZfREUOA1FwgJGcjYxnjvhNJaKyrNB6B-7uXc,1580
40
+ allium_cli-0.2.0.dist-info/METADATA,sha256=yexBYxaBDAePDv8xDq5_DubQz924-JnAshbvMZCikD8,9562
41
+ allium_cli-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
42
+ allium_cli-0.2.0.dist-info/entry_points.txt,sha256=L7gMV98VY361HCDk8oWm0JfSMNmW9Gvoi6R0WNISIZs,41
43
+ allium_cli-0.2.0.dist-info/licenses/LICENSE,sha256=A0_y9c4gKSgfkof9sHKFRbnaT7euA2hl0J1Ucqde5DA,1063
44
+ allium_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ allium = cli.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Allium
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.
cli/__init__.py ADDED
File without changes
cli/auth/__init__.py ADDED
File without changes
cli/auth/api_key.py ADDED
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from cli.types.profiles import ApiKeyProfile
4
+
5
+
6
+ def get_headers(profile: ApiKeyProfile) -> dict[str, str]:
7
+ return {"X-API-Key": profile.api_key}
cli/auth/tempo.py ADDED
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ import httpx
8
+ from mpp import Challenge
9
+ from mpp.client.transport import PaymentTransport
10
+ from mpp.methods.tempo import ChargeIntent, TempoAccount, tempo
11
+ from mpp.methods.tempo._defaults import CHAIN_RPC_URLS
12
+
13
+ from cli.types.profiles import TempoProfile
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class TempoPaymentInfo:
20
+ """cost details from the most recent 402 challenge."""
21
+
22
+ amount: str = "0"
23
+ currency: str = ""
24
+ recipient: str = ""
25
+
26
+
27
+ class _CostCapturingTransport(httpx.AsyncBaseTransport):
28
+ """captures payment amounts from 402 challenges during transport."""
29
+
30
+ def __init__(self, inner: httpx.AsyncBaseTransport | None = None) -> None:
31
+ self._inner = inner or httpx.AsyncHTTPTransport()
32
+ self.last_payment = TempoPaymentInfo()
33
+
34
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
35
+ response = await self._inner.handle_async_request(request)
36
+ if response.status_code == 402:
37
+ self._capture_challenge(response)
38
+ return response
39
+
40
+ def _capture_challenge(self, response: httpx.Response) -> None:
41
+ for header in response.headers.get_list("www-authenticate"):
42
+ if not header.lower().startswith("payment "):
43
+ continue
44
+ try:
45
+ challenge = Challenge.from_www_authenticate(header)
46
+ req = challenge.request
47
+ self.last_payment = TempoPaymentInfo(
48
+ amount=str(req.get("amount", "0")),
49
+ currency=str(req.get("currency", "")),
50
+ recipient=str(req.get("recipient", "")),
51
+ )
52
+ return
53
+ except Exception:
54
+ logger.debug("Failed to parse challenge header", exc_info=True)
55
+
56
+ async def aclose(self) -> None:
57
+ await self._inner.aclose()
58
+
59
+
60
+ @dataclass
61
+ class TempoResult:
62
+ """bundles HTTP response with payment metadata."""
63
+
64
+ response: httpx.Response
65
+ payment: TempoPaymentInfo
66
+
67
+
68
+ def _get_tempo_config(profile: TempoProfile) -> tuple[TempoAccount, str, int]:
69
+ chain_id = int(profile.chain_id)
70
+ rpc_url = CHAIN_RPC_URLS.get(chain_id)
71
+ if rpc_url is None:
72
+ supported = ", ".join(str(cid) for cid in sorted(CHAIN_RPC_URLS))
73
+ raise ValueError(
74
+ f"Unsupported Tempo chain ID: {chain_id}. Supported chain IDs: {supported}"
75
+ )
76
+ account = TempoAccount.from_key(profile.private_key)
77
+ return account, rpc_url, chain_id
78
+
79
+
80
+ async def tempo_request(
81
+ profile: TempoProfile,
82
+ method: str,
83
+ url: str,
84
+ **kwargs: Any,
85
+ ) -> TempoResult:
86
+ account, rpc_url, _ = _get_tempo_config(profile)
87
+ kwargs.setdefault("timeout", 30.0)
88
+
89
+ cost_transport = _CostCapturingTransport()
90
+ payment_transport = PaymentTransport(
91
+ methods=[
92
+ tempo(
93
+ account=account,
94
+ rpc_url=rpc_url,
95
+ intents={"charge": ChargeIntent()},
96
+ )
97
+ ],
98
+ inner=cost_transport,
99
+ )
100
+
101
+ async with httpx.AsyncClient(transport=payment_transport) as client:
102
+ response = await client.request(method, url, **kwargs)
103
+ return TempoResult(response=response, payment=cost_transport.last_payment)
cli/auth/x402_key.py ADDED
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from eth_account import Account
7
+ from eth_account.messages import encode_typed_data
8
+
9
+ from cli.types.profiles import X402KeyProfile
10
+
11
+
12
+ def make_signer(profile: X402KeyProfile) -> tuple[str, Callable[[dict[str, Any]], str]]:
13
+ """return (wallet_address, sign_fn) for x402 using a raw private key."""
14
+ account = Account.from_key(profile.private_key)
15
+ address: str = account.address
16
+
17
+ def sign(typed_data: dict[str, Any]) -> str:
18
+ domain = typed_data["domain"]
19
+ msg = typed_data["message"]
20
+ primary = typed_data.get("primary_type", typed_data.get("primaryType"))
21
+ types = {k: v for k, v in typed_data["types"].items() if k != "EIP712Domain"}
22
+ signable = encode_typed_data(
23
+ primaryType=primary,
24
+ domain_data=domain,
25
+ types=types,
26
+ message=msg,
27
+ )
28
+ signed = account.sign_message(signable)
29
+ return signed.signature.hex()
30
+
31
+ return address, sign
cli/auth/x402_privy.py ADDED
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from privy import PrivyAPI
7
+
8
+ from cli.types.profiles import X402PrivyProfile
9
+
10
+
11
+ def make_signer(
12
+ profile: X402PrivyProfile,
13
+ ) -> tuple[str, Callable[[dict[str, Any]], str]]:
14
+ """return (wallet_address, sign_fn) for x402 using privy wallet RPC."""
15
+ privy = PrivyAPI(app_id=profile.privy_app_id, app_secret=profile.privy_app_secret)
16
+ wallet = privy.wallets.get(wallet_id=profile.privy_wallet_id)
17
+ address: str = wallet.address
18
+
19
+ def sign(typed_data: dict[str, Any]) -> str:
20
+ result = privy.wallets.rpc(
21
+ wallet_id=profile.privy_wallet_id,
22
+ method="eth_signTypedData_v4",
23
+ params={"typed_data": typed_data},
24
+ )
25
+ return result.data.signature
26
+
27
+ return address, sign
@@ -0,0 +1,4 @@
1
+ from cli.clients.factory import get_client
2
+ from cli.clients.protocol import ClientProtocol
3
+
4
+ __all__ = ["ClientProtocol", "get_client"]
cli/clients/factory.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ from cli.clients.http import AlliumHTTPClient
8
+ from cli.clients.protocol import ClientProtocol
9
+ from cli.clients.x402 import X402Client
10
+ from cli.constants.config import EXIT_AUTH
11
+ from cli.types.profiles import (
12
+ ApiKeyProfile,
13
+ ProfileUnion,
14
+ TempoProfile,
15
+ X402KeyProfile,
16
+ X402PrivyProfile,
17
+ )
18
+ from cli.utils.console import err_console
19
+
20
+
21
+ class _X402SignerAdapter:
22
+ """adapts auth provider to X402Signer protocol."""
23
+
24
+ def __init__(
25
+ self,
26
+ wallet_address: str,
27
+ sign_fn: Callable[[dict[str, Any]], str],
28
+ target_network: str,
29
+ ) -> None:
30
+ self._address = wallet_address
31
+ self._sign_fn = sign_fn
32
+ self._target_network = target_network
33
+
34
+ @property
35
+ def address(self) -> str:
36
+ return self._address
37
+
38
+ @property
39
+ def target_network(self) -> str:
40
+ return self._target_network
41
+
42
+ def sign(self, typed_data: dict[str, Any]) -> str:
43
+ return self._sign_fn(typed_data)
44
+
45
+
46
+ def _make_x402_client(
47
+ base_url: str,
48
+ make_signer: Callable[..., tuple[str, Callable[[dict[str, Any]], str]]],
49
+ profile: X402KeyProfile | X402PrivyProfile,
50
+ ) -> X402Client:
51
+ try:
52
+ address, sign_fn = make_signer(profile)
53
+ except Exception as exc:
54
+ err_console.print(f"[red]Failed to initialize wallet signer:[/red] {exc}")
55
+ sys.exit(EXIT_AUTH)
56
+ signer = _X402SignerAdapter(address, sign_fn, str(profile.target_network))
57
+ http = AlliumHTTPClient(base_url=base_url)
58
+ return X402Client(http, signer)
59
+
60
+
61
+ def get_client(profile: ProfileUnion) -> ClientProtocol:
62
+ """create an authenticated client from a profile."""
63
+ base_url = profile.base_url
64
+
65
+ if isinstance(profile, ApiKeyProfile):
66
+ from cli.auth.api_key import get_headers
67
+
68
+ return AlliumHTTPClient(base_url=base_url, headers=get_headers(profile))
69
+
70
+ if isinstance(profile, X402KeyProfile):
71
+ from cli.auth.x402_key import make_signer
72
+
73
+ return _make_x402_client(base_url, make_signer, profile)
74
+
75
+ if isinstance(profile, X402PrivyProfile):
76
+ from cli.auth.x402_privy import make_signer
77
+
78
+ return _make_x402_client(base_url, make_signer, profile)
79
+
80
+ if isinstance(profile, TempoProfile):
81
+ from cli.clients.tempo import TempoClient
82
+
83
+ try:
84
+ return TempoClient(profile)
85
+ except Exception as exc:
86
+ err_console.print(f"[red]Failed to initialize Tempo client:[/red] {exc}")
87
+ sys.exit(EXIT_AUTH)
88
+
89
+ raise ValueError(f"Unknown profile type: {type(profile)}")