cartha-cli 1.0.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,524 @@
1
+ """Miner status command - shows miner info without password."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from datetime import UTC, datetime, timedelta
7
+ from typing import Any
8
+
9
+ import bittensor as bt
10
+ import typer
11
+ from rich.json import JSON
12
+ from rich.status import Status
13
+ from rich.table import Table
14
+
15
+ from ..config import settings
16
+ from ..display import display_clock_and_countdown
17
+ from ..pair import get_uid_from_hotkey
18
+ from ..utils import format_timestamp, format_timestamp_multiline, format_evm_address, normalize_hex
19
+ from ..verifier import VerifierError, fetch_miner_status, get_lock_status, process_lock_transaction
20
+ from ..wallet import load_wallet
21
+ from .common import (
22
+ console,
23
+ handle_unexpected_exception,
24
+ handle_wallet_exception,
25
+ )
26
+ from .shared_options import (
27
+ wallet_name_option,
28
+ wallet_hotkey_option,
29
+ slot_option,
30
+ auto_fetch_uid_option,
31
+ network_option,
32
+ netuid_option,
33
+ json_output_option,
34
+ tx_hash_option,
35
+ refresh_option,
36
+ )
37
+
38
+ # Note: CLI does NOT convert pool_id to pool_name - verifier handles that
39
+ # CLI just displays the pool_name from verifier response (capitalized)
40
+
41
+
42
+ def miner_status(
43
+ wallet_name: str = wallet_name_option(required=True),
44
+ wallet_hotkey: str = wallet_hotkey_option(required=True),
45
+ slot: int | None = slot_option(),
46
+ auto_fetch_uid: bool = auto_fetch_uid_option(),
47
+ network: str = network_option(),
48
+ netuid: int = netuid_option(),
49
+ json_output: bool = json_output_option(),
50
+ refresh: bool = refresh_option(),
51
+ tx_hash: str | None = tx_hash_option(),
52
+ ) -> None:
53
+ """Show miner status and pool information (no authentication required).
54
+
55
+ USAGE:
56
+ ------
57
+ Interactive mode: 'cartha miner status' (will prompt for wallet)
58
+ With arguments: 'cartha miner status -w cold -wh hot'
59
+
60
+ ALIASES:
61
+ --------
62
+ Wallet: --wallet-name, --coldkey, -w | --wallet-hotkey, --hotkey, -wh
63
+ Slot: --slot, --uid, -u | Network: --network, -n
64
+ TX: --tx-hash, --tx, --transaction (for --refresh)
65
+
66
+ FEATURES:
67
+ ---------
68
+ - Shows all your lock positions across pools and EVM addresses
69
+ - No password required (public endpoint)
70
+ - Auto-fetches your UID from Bittensor network
71
+ - Use --refresh to manually trigger lock processing (if verifier hasn't detected it yet)
72
+ - Displays expiration warnings for positions expiring soon
73
+
74
+ Use 'cartha miner password' to view your password (requires authentication).
75
+ """
76
+ try:
77
+ # Auto-map netuid and verifier URL based on network
78
+ if network == "test":
79
+ netuid = 78
80
+ elif network == "finney":
81
+ netuid = 35
82
+ # Warn that mainnet is not live yet
83
+ console.print()
84
+ console.print("[bold yellow]⚠️ MAINNET NOT AVAILABLE YET[/]")
85
+ console.print("[yellow]Cartha subnet is currently in testnet phase (subnet 78).[/]")
86
+ console.print("[yellow]Mainnet (subnet 35) has not been announced yet.[/]")
87
+ console.print("[dim]Use --network test to access testnet.[/]")
88
+ console.print()
89
+ # Note: netuid parameter is kept for backwards compatibility / explicit override
90
+
91
+ from ..config import get_verifier_url_for_network
92
+ expected_verifier_url = get_verifier_url_for_network(network)
93
+ if settings.verifier_url != expected_verifier_url:
94
+ settings.verifier_url = expected_verifier_url
95
+
96
+ wallet = load_wallet(wallet_name, wallet_hotkey, None)
97
+ hotkey = wallet.hotkey.ss58_address
98
+
99
+ # Fetch UID automatically by default, prompt if disabled
100
+ if slot is None:
101
+ if auto_fetch_uid:
102
+ try:
103
+ with console.status(
104
+ "[bold cyan]Checking miner registration status...[/]",
105
+ spinner="dots",
106
+ ):
107
+ slot = get_uid_from_hotkey(
108
+ network=network, netuid=netuid, hotkey=hotkey
109
+ )
110
+ except Exception as exc:
111
+ # Exit spinner context before prompting
112
+ console.print(
113
+ "[bold red]Failed to fetch UID automatically[/]: This may be due to Bittensor network issues."
114
+ )
115
+ console.print("[yellow]Falling back to manual input...[/]")
116
+ try:
117
+ slot_input = typer.prompt("Enter your slot UID", type=int)
118
+ slot = slot_input
119
+ except (ValueError, KeyboardInterrupt):
120
+ console.print("[bold red]Invalid UID or cancelled.[/]")
121
+ raise typer.Exit(code=1) from exc
122
+
123
+ # Check if slot is None after fetching (outside spinner context)
124
+ if slot is None:
125
+ console.print(
126
+ "[bold yellow]Hotkey is not registered or has been deregistered[/] "
127
+ f"on netuid {netuid} ({network} network)."
128
+ )
129
+ console.print(
130
+ "[yellow]You do not belong to any UID at the moment.[/] "
131
+ "Please register your hotkey first using 'cartha miner register'."
132
+ )
133
+ raise typer.Exit(code=0)
134
+ else:
135
+ console.print(
136
+ "[bold cyan]UID not provided.[/] "
137
+ "[yellow]Auto-fetch disabled. Enter UID manually.[/]"
138
+ )
139
+ try:
140
+ slot_input = typer.prompt(
141
+ "Enter your slot UID (from 'cartha miner register' output)",
142
+ type=int,
143
+ )
144
+ slot = slot_input
145
+ except (ValueError, KeyboardInterrupt):
146
+ console.print("[bold red]Invalid UID or cancelled.[/]")
147
+ raise typer.Exit(code=1)
148
+
149
+ slot_id = str(slot)
150
+
151
+ # Fetch status without authentication (public endpoint)
152
+ with console.status(
153
+ "[bold cyan]Fetching miner status from Cartha verifier...[/]",
154
+ spinner="dots",
155
+ ):
156
+ status = fetch_miner_status(
157
+ hotkey=hotkey,
158
+ slot=slot_id,
159
+ )
160
+ except bt.KeyFileError as exc:
161
+ handle_wallet_exception(
162
+ wallet_name=wallet_name, wallet_hotkey=wallet_hotkey, exc=exc
163
+ )
164
+ except typer.Exit:
165
+ raise
166
+ except VerifierError as exc:
167
+ error_msg = str(exc)
168
+ status_code = getattr(exc, "status_code", None)
169
+
170
+ # Handle 404 Not Found - endpoint not deployed yet
171
+ if status_code == 404 or "not found" in error_msg.lower():
172
+ console.print(
173
+ "[bold yellow]⚠ Endpoint not found[/]\n"
174
+ "[yellow]The verifier service needs to be redeployed with the new endpoint.[/]\n"
175
+ "[dim]This endpoint requires verifier version with /v1/miner/status support.[/]\n"
176
+ "[dim]Please contact the verifier administrator or wait for deployment.[/]"
177
+ )
178
+ raise typer.Exit(code=1) from exc
179
+
180
+ if "timed out" in error_msg.lower() or "timeout" in error_msg.lower():
181
+ console.print(f"[bold red]Request timed out[/]")
182
+ console.print(f"[yellow]{error_msg}[/]")
183
+ else:
184
+ console.print(f"[bold red]Verifier request failed[/]: {exc}")
185
+ raise typer.Exit(code=1) from exc
186
+ except Exception as exc:
187
+ error_msg = str(exc)
188
+ error_type = type(exc).__name__
189
+
190
+ is_timeout = (
191
+ "timed out" in error_msg.lower()
192
+ or "timeout" in error_msg.lower()
193
+ or error_type == "Timeout"
194
+ or (
195
+ hasattr(exc, "__cause__")
196
+ and exc.__cause__ is not None
197
+ and (
198
+ "timeout" in str(exc.__cause__).lower()
199
+ or "Timeout" in type(exc.__cause__).__name__
200
+ )
201
+ )
202
+ )
203
+
204
+ if is_timeout:
205
+ console.print(f"[bold red]Request timed out[/]")
206
+ console.print(
207
+ f"[yellow]CLI failed to reach Cartha verifier\n"
208
+ f"Possible causes: Network latency or the verifier is receiving too many requests\n"
209
+ f"Tip: Try again in a moment\n"
210
+ f"Error details: {error_msg}[/]"
211
+ )
212
+ raise typer.Exit(code=1) from exc
213
+
214
+ handle_unexpected_exception("Unable to fetch miner status", exc)
215
+
216
+ # Handle --refresh flag: manually trigger verifier if position not found
217
+ state = status.get("state", "").lower()
218
+ if refresh and state not in ("verified", "active"):
219
+ console.print()
220
+ console.print("[yellow]Position not found or not verified yet.[/]")
221
+ console.print("[dim]Using --refresh to manually trigger verifier processing...[/]\n")
222
+
223
+ # Prompt for tx_hash if not provided
224
+ tx_hash_normalized = None
225
+ if tx_hash:
226
+ tx_hash_normalized = normalize_hex(tx_hash)
227
+ if len(tx_hash_normalized) != 66:
228
+ console.print(
229
+ "[bold red]Error:[/] Transaction hash must be 66 characters (0x + 64 hex chars)"
230
+ )
231
+ raise typer.Exit(code=1)
232
+ else:
233
+ while True:
234
+ tx_input = typer.prompt(
235
+ "Transaction hash of your lock transaction (0x...)",
236
+ show_default=False,
237
+ )
238
+ tx_hash_normalized = normalize_hex(tx_input)
239
+ if len(tx_hash_normalized) == 66:
240
+ break
241
+ console.print(
242
+ "[bold red]Error:[/] Transaction hash must be 66 characters (0x + 64 hex chars)"
243
+ )
244
+
245
+ try:
246
+ # Step 1: Check if already verified (to avoid unnecessary on-chain polling)
247
+ console.print(f"[dim]Checking transaction status: {tx_hash_normalized}[/]\n")
248
+ with Status(
249
+ "[bold cyan]Checking lock status...[/]",
250
+ console=console,
251
+ spinner="dots",
252
+ ) as status_spinner:
253
+ lock_status_result = get_lock_status(tx_hash=tx_hash_normalized)
254
+
255
+ is_verified = lock_status_result.get("verified", False)
256
+
257
+ if is_verified:
258
+ console.print("[bold green]✓ Transaction is already verified![/]")
259
+ console.print("[dim]No need to trigger manual processing.[/]\n")
260
+ else:
261
+ # Step 2: Only trigger manual processing if not verified
262
+ message = lock_status_result.get("message", "")
263
+ console.print(f"[yellow]Status:[/] {message}\n")
264
+ console.print("[bold cyan]Triggering manual processing...[/]")
265
+
266
+ with Status(
267
+ "[bold cyan]Processing transaction...[/]",
268
+ console=console,
269
+ spinner="dots",
270
+ ) as process_spinner:
271
+ process_result = process_lock_transaction(tx_hash=tx_hash_normalized)
272
+
273
+ if process_result.get("success"):
274
+ console.print("[bold green]✓ Processing triggered successfully![/]\n")
275
+ else:
276
+ console.print("[yellow]Processing triggered but result unclear.[/]\n")
277
+
278
+ # Step 3: Wait a moment for database to update
279
+ console.print("[dim]Waiting for verifier to update...[/]")
280
+ time.sleep(2)
281
+
282
+ # Step 4: Re-fetch miner status
283
+ console.print("[dim]Re-fetching miner status...[/]\n")
284
+ with Status(
285
+ "[bold cyan]Fetching updated status...[/]",
286
+ console=console,
287
+ spinner="dots",
288
+ ):
289
+ status = fetch_miner_status(
290
+ hotkey=hotkey,
291
+ slot=slot_id,
292
+ )
293
+
294
+ # Check if status improved
295
+ new_state = status.get("state", "").lower()
296
+ if new_state in ("verified", "active"):
297
+ console.print("[bold green]✓ Position verified successfully![/]\n")
298
+ else:
299
+ console.print(
300
+ "[yellow]Position not yet verified.[/] "
301
+ "[dim]The verifier will continue processing automatically.[/]\n"
302
+ )
303
+
304
+ except VerifierError as refresh_exc:
305
+ console.print(f"[bold red]Refresh failed:[/] {refresh_exc}")
306
+ console.print(
307
+ "[dim]Continuing to display current status...[/]\n"
308
+ )
309
+ except Exception as refresh_exc:
310
+ console.print(f"[bold red]Error during refresh:[/] {refresh_exc}")
311
+ console.print(
312
+ "[dim]Continuing to display current status...[/]\n"
313
+ )
314
+
315
+ sanitized = dict(status)
316
+ sanitized.setdefault("state", "unknown")
317
+ sanitized["hotkey"] = hotkey
318
+ sanitized["slot"] = slot_id
319
+ # Explicitly remove password from display
320
+ sanitized.pop("pwd", None)
321
+
322
+ if json_output:
323
+ console.print(JSON.from_data(sanitized))
324
+ return
325
+
326
+ # Display clock and countdown
327
+ display_clock_and_countdown()
328
+
329
+ # Display info about Cartha being the Liquidity Provider for 0xMarkets DEX
330
+ console.print("[dim]Cartha is the Liquidity Provider for 0xMarkets DEX[/]")
331
+ console.print()
332
+
333
+ table = Table(title="Miner Status", show_header=False)
334
+ table.add_row("Hotkey", hotkey)
335
+ table.add_row("Slot UID", slot_id)
336
+ table.add_row("State", sanitized["state"])
337
+
338
+ # Show lock amounts for verified/active states
339
+ state = sanitized.get("state", "").lower()
340
+ if state in ("verified", "active"):
341
+ # Show EVM addresses used - display all addresses
342
+ evm_addresses = sanitized.get("miner_evm_addresses")
343
+ if evm_addresses:
344
+ if len(evm_addresses) == 1:
345
+ table.add_row("EVM Address", evm_addresses[0])
346
+ elif len(evm_addresses) <= 3:
347
+ # Show up to 3 addresses with line breaks
348
+ evm_display = "\n".join(evm_addresses)
349
+ table.add_row("EVM Addresses", evm_display)
350
+ else:
351
+ # Show count for many addresses
352
+ table.add_row("EVM Addresses", f"{len(evm_addresses)} addresses (see pool details below)")
353
+
354
+
355
+ console.print(table)
356
+
357
+ # Show detailed status information for verified/active pairs
358
+ if state in ("verified", "active"):
359
+ pools = sanitized.get("pools", [])
360
+
361
+ # Display per-pool table (primary display for lock information)
362
+ if pools:
363
+ console.print()
364
+ console.print("[bold cyan]━━━ Active Pools ━━━[/]")
365
+
366
+ pool_table = Table(show_header=True, header_style="bold cyan", padding=(0, 1), row_styles=["", "dim"])
367
+ pool_table.add_column("Pool Name", style="cyan", no_wrap=True)
368
+ pool_table.add_column("Amount Locked", style="green", justify="right")
369
+ pool_table.add_column("Pending Amount", style="yellow", justify="right")
370
+ pool_table.add_column("Lock Days", justify="center")
371
+ pool_table.add_column("Expires At", style="yellow")
372
+ pool_table.add_column("Status", justify="center")
373
+ pool_table.add_column("EVM Address", style="dim")
374
+
375
+ for idx, pool in enumerate(pools):
376
+ # Get pool name from verifier response (already converted by verifier)
377
+ # Display capitalized version
378
+ pool_name = pool.get("pool_name")
379
+ if pool_name:
380
+ # Capitalize pool name for display
381
+ pool_display = pool_name.upper()
382
+ else:
383
+ # Fallback: if verifier didn't provide name, show last 8 chars of pool_id
384
+ pool_id = pool.get("pool_id", "")
385
+ if pool_id:
386
+ pool_id_normalized = str(pool_id).lower().strip()
387
+ pool_display = f"Pool ({pool_id_normalized[-8:]})"
388
+ else:
389
+ pool_display = "Unknown"
390
+
391
+ # Format amount locked
392
+ amount_usdc = pool.get("amount_usdc", 0)
393
+ amount_str = f"{amount_usdc:.2f}"
394
+
395
+ # Format pending amount (top-up that will be active in next epoch)
396
+ pending_amount_usdc = pool.get("pending_lock_amount_usdc")
397
+ if pending_amount_usdc is not None and pending_amount_usdc > 0:
398
+ pending_str = f"{pending_amount_usdc:.2f} USDC"
399
+ else:
400
+ pending_str = "[dim]-[/]"
401
+
402
+ # Format lock days
403
+ lock_days = pool.get("lock_days", 0)
404
+ lock_days_str = str(lock_days)
405
+
406
+ # Format expiration with days countdown
407
+ pool_expires_at = pool.get("expires_at")
408
+ expires_str = "N/A"
409
+ days_left_str = ""
410
+ if pool_expires_at:
411
+ try:
412
+ if isinstance(pool_expires_at, str):
413
+ exp_dt = datetime.fromisoformat(
414
+ pool_expires_at.replace("Z", "+00:00")
415
+ )
416
+ elif isinstance(pool_expires_at, datetime):
417
+ exp_dt = pool_expires_at
418
+ else:
419
+ exp_dt = None
420
+ if exp_dt:
421
+ # Ensure timezone-aware
422
+ if exp_dt.tzinfo is None:
423
+ exp_dt = exp_dt.replace(tzinfo=UTC)
424
+
425
+ # Format timestamp on multiple lines
426
+ expires_str = format_timestamp_multiline(exp_dt.timestamp())
427
+
428
+ # Calculate days left
429
+ now = datetime.now(UTC)
430
+ time_until_expiry = (exp_dt - now).total_seconds()
431
+ days_until_expiry = time_until_expiry / 86400
432
+
433
+ # Format days left with color coding (on separate line)
434
+ if days_until_expiry < 0:
435
+ days_left_str = "\n[bold red]⚠ EXPIRED[/]"
436
+ elif days_until_expiry <= 7:
437
+ days_left_str = (
438
+ f"\n[bold red]⚠ {int(days_until_expiry)}d left[/]"
439
+ )
440
+ elif days_until_expiry <= 15:
441
+ days_left_str = (
442
+ f"\n[bold yellow]⚠ {int(days_until_expiry)}d left[/]"
443
+ )
444
+ else:
445
+ days_left_str = f"\n({int(days_until_expiry)}d left)"
446
+ except Exception:
447
+ expires_str = str(pool_expires_at)
448
+
449
+ # Format status - only Active and In Next Epoch (remove Verified)
450
+ is_active = pool.get("is_active", False)
451
+ pool_in_upcoming = pool.get("in_upcoming_epoch", False)
452
+
453
+ status_parts = []
454
+ if is_active:
455
+ status_parts.append("[green]Active[/]")
456
+ if pool_in_upcoming:
457
+ status_parts.append("[bold green]In Next Epoch[/]")
458
+ if not status_parts:
459
+ status_parts.append("[dim]None[/]")
460
+
461
+ status_str = " / ".join(status_parts)
462
+
463
+ # EVM address - format in standard crypto wallet display
464
+ evm_addr = pool.get("evm_address", "")
465
+ evm_display = format_evm_address(evm_addr)
466
+
467
+ pool_table.add_row(
468
+ pool_display,
469
+ f"{amount_str} USDC",
470
+ pending_str,
471
+ lock_days_str,
472
+ expires_str + days_left_str,
473
+ status_str,
474
+ evm_display,
475
+ )
476
+
477
+ # Add spacing row between pools (except after the last one)
478
+ if idx < len(pools) - 1:
479
+ pool_table.add_row(
480
+ "", # Empty row for visual spacing
481
+ "",
482
+ "",
483
+ "",
484
+ "",
485
+ "",
486
+ "",
487
+ )
488
+
489
+ console.print(pool_table)
490
+
491
+ # Concise reminder
492
+ console.print()
493
+ console.print("[bold cyan]━━━ Reminders ━━━[/]")
494
+ console.print(
495
+ "• Lock expiration: USDC returned automatically, emissions stop for that pool."
496
+ )
497
+ console.print(
498
+ "• Top-ups/extensions: Happen automatically on-chain. No CLI action needed."
499
+ )
500
+ if pools and len(pools) > 1:
501
+ console.print(
502
+ "• Multiple pools: Each pool is tracked separately. Expired pools stop earning, others continue."
503
+ )
504
+
505
+ # Link to web interface
506
+ console.print()
507
+ console.print("[bold cyan]━━━ Web Interface ━━━[/]")
508
+ console.print(
509
+ "[cyan]🌐 View and manage your positions:[/] [bold]https://cartha.finance[/]"
510
+ )
511
+ console.print(
512
+ "[dim] • View all your lock positions[/]"
513
+ )
514
+ console.print(
515
+ "[dim] • Extend lock days[/]"
516
+ )
517
+ console.print(
518
+ "[dim] • Top up existing positions[/]"
519
+ )
520
+ console.print(
521
+ "[dim] • Claim testnet USDC from faucet[/]"
522
+ )
523
+
524
+ return