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.
- cartha_cli/__init__.py +34 -0
- cartha_cli/bt.py +206 -0
- cartha_cli/commands/__init__.py +25 -0
- cartha_cli/commands/common.py +76 -0
- cartha_cli/commands/config.py +294 -0
- cartha_cli/commands/health.py +463 -0
- cartha_cli/commands/help.py +49 -0
- cartha_cli/commands/miner_password.py +283 -0
- cartha_cli/commands/miner_status.py +524 -0
- cartha_cli/commands/pair_status.py +484 -0
- cartha_cli/commands/pools.py +121 -0
- cartha_cli/commands/prove_lock.py +1260 -0
- cartha_cli/commands/register.py +274 -0
- cartha_cli/commands/shared_options.py +235 -0
- cartha_cli/commands/version.py +15 -0
- cartha_cli/config.py +75 -0
- cartha_cli/display.py +62 -0
- cartha_cli/eth712.py +7 -0
- cartha_cli/main.py +237 -0
- cartha_cli/pair.py +201 -0
- cartha_cli/utils.py +274 -0
- cartha_cli/verifier.py +342 -0
- cartha_cli/wallet.py +59 -0
- cartha_cli-1.0.0.dist-info/METADATA +180 -0
- cartha_cli-1.0.0.dist-info/RECORD +28 -0
- cartha_cli-1.0.0.dist-info/WHEEL +4 -0
- cartha_cli-1.0.0.dist-info/entry_points.txt +2 -0
- cartha_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|