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,484 @@
|
|
|
1
|
+
"""Pair status command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import bittensor as bt
|
|
9
|
+
import typer
|
|
10
|
+
from rich.json import JSON
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from ..config import settings
|
|
14
|
+
from ..display import display_clock_and_countdown
|
|
15
|
+
from ..pair import (
|
|
16
|
+
build_pair_auth_payload,
|
|
17
|
+
get_uid_from_hotkey,
|
|
18
|
+
)
|
|
19
|
+
from ..utils import format_timestamp, format_timestamp_multiline, format_evm_address
|
|
20
|
+
from ..verifier import VerifierError, fetch_pair_status
|
|
21
|
+
from ..wallet import load_wallet
|
|
22
|
+
from .common import (
|
|
23
|
+
console,
|
|
24
|
+
handle_unexpected_exception,
|
|
25
|
+
handle_wallet_exception,
|
|
26
|
+
)
|
|
27
|
+
from .shared_options import (
|
|
28
|
+
wallet_name_option,
|
|
29
|
+
wallet_hotkey_option,
|
|
30
|
+
slot_option,
|
|
31
|
+
auto_fetch_uid_option,
|
|
32
|
+
network_option,
|
|
33
|
+
netuid_option,
|
|
34
|
+
json_output_option,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Import pool name helper
|
|
38
|
+
try:
|
|
39
|
+
from ...testnet.pool_ids import pool_id_to_name
|
|
40
|
+
except ImportError:
|
|
41
|
+
# Fallback if running from different context
|
|
42
|
+
def pool_id_to_name(pool_id: str) -> str | None:
|
|
43
|
+
"""Simple fallback to decode pool ID."""
|
|
44
|
+
try:
|
|
45
|
+
hex_str = pool_id.lower().removeprefix("0x")
|
|
46
|
+
pool_bytes = bytes.fromhex(hex_str)
|
|
47
|
+
name = pool_bytes.rstrip(b"\x00").decode("utf-8", errors="ignore")
|
|
48
|
+
if name and name.isprintable():
|
|
49
|
+
return name
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def pair_status(
|
|
56
|
+
wallet_name: str = wallet_name_option(required=True),
|
|
57
|
+
wallet_hotkey: str = wallet_hotkey_option(required=True),
|
|
58
|
+
slot: int | None = slot_option(),
|
|
59
|
+
auto_fetch_uid: bool = auto_fetch_uid_option(),
|
|
60
|
+
network: str = network_option(),
|
|
61
|
+
netuid: int = netuid_option(),
|
|
62
|
+
json_output: bool = json_output_option(),
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Show the verifier state for a miner pair (legacy - use 'cartha miner status' instead).
|
|
65
|
+
|
|
66
|
+
USAGE:
|
|
67
|
+
------
|
|
68
|
+
Interactive mode: 'cartha pair status' (will prompt for wallet)
|
|
69
|
+
With arguments: 'cartha pair status -w cold -wh hot'
|
|
70
|
+
|
|
71
|
+
ALIASES:
|
|
72
|
+
--------
|
|
73
|
+
Wallet: --wallet-name, --coldkey, -w | --wallet-hotkey, --hotkey, -wh
|
|
74
|
+
Slot: --slot, --uid, -u | Network: --network, -n
|
|
75
|
+
|
|
76
|
+
DEPRECATED: Use 'cartha miner status' instead for faster status checks without authentication.
|
|
77
|
+
|
|
78
|
+
State Legend:
|
|
79
|
+
════════════════════════════════════════════════════════════════
|
|
80
|
+
|
|
81
|
+
1. active - In current frozen epoch, earning rewards
|
|
82
|
+
|
|
83
|
+
2. verified - Has lock proof, not in current active epoch
|
|
84
|
+
|
|
85
|
+
3. pending - Registered, no lock proof submitted yet
|
|
86
|
+
|
|
87
|
+
4. revoked - Revoked (deregistered or evicted)
|
|
88
|
+
|
|
89
|
+
5. unknown - No pair record found
|
|
90
|
+
|
|
91
|
+
════════════════════════════════════════════════════════════════
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
# Auto-map netuid and verifier URL based on network
|
|
95
|
+
if network == "test":
|
|
96
|
+
netuid = 78
|
|
97
|
+
elif network == "finney":
|
|
98
|
+
netuid = 35
|
|
99
|
+
# Warn that mainnet is not live yet
|
|
100
|
+
console.print()
|
|
101
|
+
console.print("[bold yellow]⚠️ MAINNET NOT AVAILABLE YET[/]")
|
|
102
|
+
console.print("[yellow]Cartha subnet is currently in testnet phase (subnet 78).[/]")
|
|
103
|
+
console.print("[yellow]Mainnet (subnet 35) has not been announced yet.[/]")
|
|
104
|
+
console.print("[dim]Use --network test to access testnet.[/]")
|
|
105
|
+
console.print()
|
|
106
|
+
# Note: netuid parameter is kept for backwards compatibility / explicit override
|
|
107
|
+
|
|
108
|
+
from ..config import get_verifier_url_for_network
|
|
109
|
+
expected_verifier_url = get_verifier_url_for_network(network)
|
|
110
|
+
if settings.verifier_url != expected_verifier_url:
|
|
111
|
+
settings.verifier_url = expected_verifier_url
|
|
112
|
+
|
|
113
|
+
console.print("[bold cyan]Loading wallet...[/]")
|
|
114
|
+
wallet = load_wallet(wallet_name, wallet_hotkey, None)
|
|
115
|
+
hotkey = wallet.hotkey.ss58_address
|
|
116
|
+
|
|
117
|
+
# Fetch UID automatically by default, prompt if disabled
|
|
118
|
+
if slot is None:
|
|
119
|
+
if auto_fetch_uid:
|
|
120
|
+
# Auto-fetch enabled (default) - try to fetch from network
|
|
121
|
+
console.print("[bold cyan]Fetching UID from subnet...[/]")
|
|
122
|
+
try:
|
|
123
|
+
slot = get_uid_from_hotkey(
|
|
124
|
+
network=network, netuid=netuid, hotkey=hotkey
|
|
125
|
+
)
|
|
126
|
+
if slot is None:
|
|
127
|
+
console.print(
|
|
128
|
+
"[bold yellow]Hotkey is not registered or has been deregistered[/] "
|
|
129
|
+
f"on netuid {netuid} ({network} network)."
|
|
130
|
+
)
|
|
131
|
+
console.print(
|
|
132
|
+
"[yellow]You do not belong to any UID at the moment.[/] "
|
|
133
|
+
"Please register your hotkey first using 'cartha miner register'."
|
|
134
|
+
)
|
|
135
|
+
raise typer.Exit(code=0)
|
|
136
|
+
console.print(f"[bold green]Found UID: {slot}[/]")
|
|
137
|
+
except typer.Exit:
|
|
138
|
+
raise
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
console.print(
|
|
141
|
+
"[bold red]Failed to fetch UID automatically[/]: This may be due to Bittensor network issues."
|
|
142
|
+
)
|
|
143
|
+
console.print("[yellow]Falling back to manual input...[/]")
|
|
144
|
+
try:
|
|
145
|
+
slot_input = typer.prompt("Enter your slot UID", type=int)
|
|
146
|
+
slot = slot_input
|
|
147
|
+
console.print(f"[bold green]Using UID: {slot}[/]")
|
|
148
|
+
except (ValueError, KeyboardInterrupt):
|
|
149
|
+
console.print("[bold red]Invalid UID or cancelled.[/]")
|
|
150
|
+
raise typer.Exit(code=1) from exc
|
|
151
|
+
else:
|
|
152
|
+
# Auto-fetch disabled (--no-auto-fetch-uid) - prompt for UID
|
|
153
|
+
console.print(
|
|
154
|
+
"[bold cyan]UID not provided.[/] "
|
|
155
|
+
"[yellow]Auto-fetch disabled. Enter UID manually.[/]"
|
|
156
|
+
)
|
|
157
|
+
try:
|
|
158
|
+
slot_input = typer.prompt(
|
|
159
|
+
"Enter your slot UID (from 'cartha miner register' output)",
|
|
160
|
+
type=int,
|
|
161
|
+
)
|
|
162
|
+
slot = slot_input
|
|
163
|
+
console.print(f"[bold green]Using UID: {slot}[/]")
|
|
164
|
+
except (ValueError, KeyboardInterrupt):
|
|
165
|
+
console.print("[bold red]Invalid UID or cancelled.[/]")
|
|
166
|
+
raise typer.Exit(code=1)
|
|
167
|
+
|
|
168
|
+
slot_id = str(slot)
|
|
169
|
+
# Skip metagraph check - verifier will validate the pair anyway
|
|
170
|
+
# This avoids slow metagraph() calls that cause timeouts
|
|
171
|
+
|
|
172
|
+
console.print("[bold cyan]Signing hotkey ownership challenge...[/]")
|
|
173
|
+
auth_payload = build_pair_auth_payload(
|
|
174
|
+
network=network,
|
|
175
|
+
netuid=netuid,
|
|
176
|
+
slot=slot_id,
|
|
177
|
+
hotkey=hotkey,
|
|
178
|
+
wallet_name=wallet_name,
|
|
179
|
+
wallet_hotkey=wallet_hotkey,
|
|
180
|
+
)
|
|
181
|
+
with console.status(
|
|
182
|
+
"[bold cyan]Verifying ownership with Cartha verifier...[/]",
|
|
183
|
+
spinner="dots",
|
|
184
|
+
):
|
|
185
|
+
status = fetch_pair_status(
|
|
186
|
+
hotkey=hotkey,
|
|
187
|
+
slot=slot_id,
|
|
188
|
+
network=network,
|
|
189
|
+
netuid=netuid,
|
|
190
|
+
message=auth_payload["message"],
|
|
191
|
+
signature=auth_payload["signature"],
|
|
192
|
+
)
|
|
193
|
+
except bt.KeyFileError as exc:
|
|
194
|
+
handle_wallet_exception(
|
|
195
|
+
wallet_name=wallet_name, wallet_hotkey=wallet_hotkey, exc=exc
|
|
196
|
+
)
|
|
197
|
+
except typer.Exit:
|
|
198
|
+
raise
|
|
199
|
+
except VerifierError as exc:
|
|
200
|
+
# VerifierError handling
|
|
201
|
+
error_msg = str(exc)
|
|
202
|
+
if "timed out" in error_msg.lower() or "timeout" in error_msg.lower():
|
|
203
|
+
console.print(f"[bold red]Request timed out[/]")
|
|
204
|
+
# Print the full error message (may be multi-line)
|
|
205
|
+
console.print(f"[yellow]{error_msg}[/]")
|
|
206
|
+
else:
|
|
207
|
+
console.print(f"[bold red]Verifier request failed[/]: {exc}")
|
|
208
|
+
raise typer.Exit(code=1) from exc
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
# Check if it's a timeout-related error (even if wrapped)
|
|
211
|
+
error_msg = str(exc)
|
|
212
|
+
error_type = type(exc).__name__
|
|
213
|
+
|
|
214
|
+
# Check for timeout indicators in the exception
|
|
215
|
+
is_timeout = (
|
|
216
|
+
"timed out" in error_msg.lower()
|
|
217
|
+
or "timeout" in error_msg.lower()
|
|
218
|
+
or error_type == "Timeout"
|
|
219
|
+
or (
|
|
220
|
+
hasattr(exc, "__cause__")
|
|
221
|
+
and exc.__cause__ is not None
|
|
222
|
+
and (
|
|
223
|
+
"timeout" in str(exc.__cause__).lower()
|
|
224
|
+
or "Timeout" in type(exc.__cause__).__name__
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if is_timeout:
|
|
230
|
+
console.print(f"[bold red]Request timed out[/]")
|
|
231
|
+
console.print(
|
|
232
|
+
f"[yellow]CLI failed to reach Cartha verifier\n"
|
|
233
|
+
f"Possible causes: Network latency or the verifier is receiving too many requests\n"
|
|
234
|
+
f"Tip: Try again in a moment\n"
|
|
235
|
+
f"Error details: {error_msg}[/]"
|
|
236
|
+
)
|
|
237
|
+
raise typer.Exit(code=1) from exc
|
|
238
|
+
|
|
239
|
+
handle_unexpected_exception("Unable to fetch pair status", exc)
|
|
240
|
+
|
|
241
|
+
initial_status = dict(status)
|
|
242
|
+
password_payload: dict[str, Any] | None = None
|
|
243
|
+
|
|
244
|
+
existing_pwd = initial_status.get("pwd")
|
|
245
|
+
state = initial_status.get("state") or "unknown"
|
|
246
|
+
has_pwd_flag = initial_status.get("has_pwd") or bool(existing_pwd)
|
|
247
|
+
|
|
248
|
+
# Note: Password registration removed - new lock flow uses session tokens instead
|
|
249
|
+
# The has_pwd flag is kept for backward compatibility but passwords are no longer used
|
|
250
|
+
|
|
251
|
+
sanitized = dict(status)
|
|
252
|
+
sanitized.setdefault("state", "unknown")
|
|
253
|
+
sanitized["hotkey"] = hotkey
|
|
254
|
+
sanitized["slot"] = slot_id
|
|
255
|
+
password = sanitized.get("pwd")
|
|
256
|
+
|
|
257
|
+
if json_output:
|
|
258
|
+
console.print(JSON.from_data(sanitized))
|
|
259
|
+
if password:
|
|
260
|
+
console.print(
|
|
261
|
+
"[bold yellow]Keep it safe[/] — for your eyes only. Exposure might allow others to steal your locked USDC rewards."
|
|
262
|
+
)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# Display clock and countdown
|
|
266
|
+
display_clock_and_countdown()
|
|
267
|
+
|
|
268
|
+
table = Table(title="Pair Status", show_header=False)
|
|
269
|
+
table.add_row("Hotkey", hotkey)
|
|
270
|
+
table.add_row("Slot UID", slot_id)
|
|
271
|
+
table.add_row("State", sanitized["state"])
|
|
272
|
+
|
|
273
|
+
# Show lock amounts for verified/active states
|
|
274
|
+
state = sanitized.get("state", "").lower()
|
|
275
|
+
if state in ("verified", "active"):
|
|
276
|
+
# Show EVM addresses used - display all addresses
|
|
277
|
+
evm_addresses = sanitized.get("miner_evm_addresses")
|
|
278
|
+
if evm_addresses:
|
|
279
|
+
if len(evm_addresses) == 1:
|
|
280
|
+
table.add_row("EVM Address", evm_addresses[0])
|
|
281
|
+
elif len(evm_addresses) <= 3:
|
|
282
|
+
# Show up to 3 addresses with line breaks
|
|
283
|
+
evm_display = "\n".join(evm_addresses)
|
|
284
|
+
table.add_row("EVM Addresses", evm_display)
|
|
285
|
+
else:
|
|
286
|
+
# Show count for many addresses
|
|
287
|
+
table.add_row("EVM Addresses", f"{len(evm_addresses)} addresses (see pool details below)")
|
|
288
|
+
|
|
289
|
+
table.add_row("Password issued", "yes" if sanitized.get("has_pwd") else "no")
|
|
290
|
+
issued_at = sanitized.get("issued_at")
|
|
291
|
+
if issued_at:
|
|
292
|
+
# Try to parse and format the timestamp
|
|
293
|
+
try:
|
|
294
|
+
if isinstance(issued_at, (int, float)) or (
|
|
295
|
+
isinstance(issued_at, str) and issued_at.isdigit()
|
|
296
|
+
):
|
|
297
|
+
# Numeric timestamp
|
|
298
|
+
formatted_time = format_timestamp(issued_at)
|
|
299
|
+
elif isinstance(issued_at, str):
|
|
300
|
+
# Try parsing as ISO format datetime string
|
|
301
|
+
try:
|
|
302
|
+
dt = datetime.fromisoformat(issued_at.replace("Z", "+00:00"))
|
|
303
|
+
timestamp = dt.timestamp()
|
|
304
|
+
formatted_time = format_timestamp(timestamp)
|
|
305
|
+
except (ValueError, AttributeError):
|
|
306
|
+
# If parsing fails, display as-is
|
|
307
|
+
formatted_time = issued_at
|
|
308
|
+
else:
|
|
309
|
+
formatted_time = str(issued_at)
|
|
310
|
+
table.add_row("Password issued at", formatted_time)
|
|
311
|
+
except Exception:
|
|
312
|
+
table.add_row("Password issued at", str(issued_at))
|
|
313
|
+
if password:
|
|
314
|
+
table.add_row("Pair password", password)
|
|
315
|
+
console.print(table)
|
|
316
|
+
|
|
317
|
+
# Show warnings and reminders
|
|
318
|
+
if password:
|
|
319
|
+
console.print()
|
|
320
|
+
console.print(
|
|
321
|
+
"[bold yellow]🔐 Keep your password safe[/] — Exposure might allow others to steal your locked USDC rewards."
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Show detailed status information for verified/active pairs
|
|
325
|
+
if state in ("verified", "active"):
|
|
326
|
+
pools = sanitized.get("pools", [])
|
|
327
|
+
in_upcoming_epoch = sanitized.get("in_upcoming_epoch")
|
|
328
|
+
expires_at = sanitized.get("expires_at")
|
|
329
|
+
|
|
330
|
+
# Display per-pool table (primary display for lock information)
|
|
331
|
+
if pools:
|
|
332
|
+
console.print()
|
|
333
|
+
console.print("[bold cyan]━━━ Active Pools ━━━[/]")
|
|
334
|
+
|
|
335
|
+
pool_table = Table(show_header=True, header_style="bold cyan")
|
|
336
|
+
pool_table.add_column("Pool Name", style="cyan", no_wrap=True)
|
|
337
|
+
pool_table.add_column("Amount Locked", style="green", justify="right")
|
|
338
|
+
pool_table.add_column("Lock Days", justify="center")
|
|
339
|
+
pool_table.add_column("Expires At", style="yellow")
|
|
340
|
+
pool_table.add_column("Status", justify="center")
|
|
341
|
+
pool_table.add_column("EVM Address", style="dim")
|
|
342
|
+
|
|
343
|
+
for pool in pools:
|
|
344
|
+
# Format pool name - use human-readable name if available
|
|
345
|
+
pool_name = pool.get("pool_name")
|
|
346
|
+
if not pool_name:
|
|
347
|
+
# Fallback to pool_id if name not available
|
|
348
|
+
pool_id = pool.get("pool_id", "")
|
|
349
|
+
if pool_id:
|
|
350
|
+
# Try to convert pool_id to name
|
|
351
|
+
pool_name = pool_id_to_name(pool_id) or pool_id[:10] + "..."
|
|
352
|
+
else:
|
|
353
|
+
pool_name = "Unknown"
|
|
354
|
+
pool_display = pool_name
|
|
355
|
+
|
|
356
|
+
# Format amount locked
|
|
357
|
+
amount_usdc = pool.get("amount_usdc", 0)
|
|
358
|
+
amount_str = f"{amount_usdc:.2f}"
|
|
359
|
+
|
|
360
|
+
# Format lock days
|
|
361
|
+
lock_days = pool.get("lock_days", 0)
|
|
362
|
+
lock_days_str = str(lock_days)
|
|
363
|
+
|
|
364
|
+
# Format expiration
|
|
365
|
+
pool_expires_at = pool.get("expires_at")
|
|
366
|
+
expires_str = "N/A"
|
|
367
|
+
if pool_expires_at:
|
|
368
|
+
try:
|
|
369
|
+
if isinstance(pool_expires_at, str):
|
|
370
|
+
exp_dt = datetime.fromisoformat(
|
|
371
|
+
pool_expires_at.replace("Z", "+00:00")
|
|
372
|
+
)
|
|
373
|
+
elif isinstance(pool_expires_at, datetime):
|
|
374
|
+
exp_dt = pool_expires_at
|
|
375
|
+
else:
|
|
376
|
+
exp_dt = None
|
|
377
|
+
if exp_dt:
|
|
378
|
+
expires_str = format_timestamp(exp_dt.timestamp())
|
|
379
|
+
except Exception:
|
|
380
|
+
expires_str = str(pool_expires_at)
|
|
381
|
+
|
|
382
|
+
# Format status - show which pools are active, verified, and included in next epoch
|
|
383
|
+
is_active = pool.get("is_active", False)
|
|
384
|
+
is_verified = pool.get("is_verified", False)
|
|
385
|
+
pool_in_upcoming = pool.get("in_upcoming_epoch", False)
|
|
386
|
+
|
|
387
|
+
status_parts = []
|
|
388
|
+
if is_active:
|
|
389
|
+
status_parts.append("[green]Active[/]")
|
|
390
|
+
if is_verified:
|
|
391
|
+
status_parts.append("[cyan]Verified[/]")
|
|
392
|
+
if pool_in_upcoming:
|
|
393
|
+
status_parts.append("[bold green]In Next Epoch[/]")
|
|
394
|
+
if not status_parts:
|
|
395
|
+
status_parts.append("[dim]None[/]")
|
|
396
|
+
|
|
397
|
+
status_str = " / ".join(status_parts)
|
|
398
|
+
|
|
399
|
+
# EVM address (full address for clarity)
|
|
400
|
+
evm_addr = pool.get("evm_address", "")
|
|
401
|
+
evm_display = (
|
|
402
|
+
evm_addr
|
|
403
|
+
if len(evm_addr) <= 42
|
|
404
|
+
else (evm_addr[:20] + "..." + evm_addr[-6:])
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
pool_table.add_row(
|
|
408
|
+
pool_display,
|
|
409
|
+
f"{amount_str} USDC",
|
|
410
|
+
lock_days_str,
|
|
411
|
+
expires_str,
|
|
412
|
+
status_str,
|
|
413
|
+
evm_display,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
console.print(pool_table)
|
|
417
|
+
|
|
418
|
+
console.print()
|
|
419
|
+
console.print("[bold cyan]━━━ Epoch Status ━━━[/]")
|
|
420
|
+
|
|
421
|
+
# Upcoming epoch inclusion status
|
|
422
|
+
if in_upcoming_epoch:
|
|
423
|
+
console.print(
|
|
424
|
+
"[bold green]✓ Included in upcoming epoch[/] — You will receive rewards for the next epoch."
|
|
425
|
+
)
|
|
426
|
+
elif in_upcoming_epoch is False:
|
|
427
|
+
console.print(
|
|
428
|
+
"[bold yellow]⚠ Not included in upcoming epoch[/] — Use [bold]cartha vault lock[/] to be included."
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Expiration date information and warnings (aggregated)
|
|
432
|
+
if expires_at:
|
|
433
|
+
try:
|
|
434
|
+
# Parse expiration datetime
|
|
435
|
+
if isinstance(expires_at, str):
|
|
436
|
+
exp_dt = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
|
|
437
|
+
elif isinstance(expires_at, datetime):
|
|
438
|
+
exp_dt = expires_at
|
|
439
|
+
else:
|
|
440
|
+
exp_dt = None
|
|
441
|
+
|
|
442
|
+
if exp_dt:
|
|
443
|
+
now = datetime.now(UTC)
|
|
444
|
+
time_until_expiry = (exp_dt - now).total_seconds()
|
|
445
|
+
days_until_expiry = time_until_expiry / 86400
|
|
446
|
+
|
|
447
|
+
console.print()
|
|
448
|
+
console.print("[bold cyan]━━━ Lock Expiration ━━━[/]")
|
|
449
|
+
|
|
450
|
+
if days_until_expiry < 0:
|
|
451
|
+
console.print(
|
|
452
|
+
"[bold red]⚠ EXPIRED[/] — Some locks expired. USDC will be returned. No more emissions for expired pools."
|
|
453
|
+
)
|
|
454
|
+
elif days_until_expiry <= 7:
|
|
455
|
+
console.print(
|
|
456
|
+
f"[bold red]⚠ Expiring in {days_until_expiry:.1f} days[/] — Make a new lock transaction on-chain to continue receiving emissions."
|
|
457
|
+
)
|
|
458
|
+
elif days_until_expiry <= 30:
|
|
459
|
+
console.print(
|
|
460
|
+
f"[bold yellow]⚠ Expiring in {days_until_expiry:.0f} days[/] — Consider making a new lock transaction on-chain soon."
|
|
461
|
+
)
|
|
462
|
+
else:
|
|
463
|
+
console.print(
|
|
464
|
+
f"[bold green]✓ Valid for {days_until_expiry:.0f} days[/]"
|
|
465
|
+
)
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
# Concise reminder
|
|
470
|
+
console.print()
|
|
471
|
+
console.print("[bold cyan]━━━ Reminders ━━━[/]")
|
|
472
|
+
console.print(
|
|
473
|
+
"• Lock expiration: USDC returned automatically, emissions stop for that pool."
|
|
474
|
+
)
|
|
475
|
+
console.print(
|
|
476
|
+
"• Top-ups/extensions: Happen automatically on-chain. No CLI action needed."
|
|
477
|
+
)
|
|
478
|
+
if pools and len(pools) > 1:
|
|
479
|
+
console.print(
|
|
480
|
+
"• Multiple pools: Each pool is tracked separately. Expired pools stop earning, others continue."
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Explicitly return to ensure clean exit
|
|
484
|
+
return
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Pools command - show current available pools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .common import console
|
|
8
|
+
|
|
9
|
+
# Import pool helpers for pool_id conversion
|
|
10
|
+
try:
|
|
11
|
+
from ...testnet.pool_ids import (
|
|
12
|
+
list_pools,
|
|
13
|
+
pool_id_to_chain_id,
|
|
14
|
+
pool_id_to_vault_address,
|
|
15
|
+
)
|
|
16
|
+
except ImportError:
|
|
17
|
+
# Fallback if running from different context
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
# Try adding parent directory to path
|
|
22
|
+
testnet_dir = Path(__file__).parent.parent.parent / "testnet"
|
|
23
|
+
if testnet_dir.exists():
|
|
24
|
+
sys.path.insert(0, str(testnet_dir.parent))
|
|
25
|
+
try:
|
|
26
|
+
from testnet.pool_ids import (
|
|
27
|
+
list_pools,
|
|
28
|
+
pool_id_to_chain_id,
|
|
29
|
+
pool_id_to_vault_address,
|
|
30
|
+
)
|
|
31
|
+
except ImportError:
|
|
32
|
+
# Final fallback
|
|
33
|
+
def list_pools() -> dict[str, str]:
|
|
34
|
+
"""Fallback: return empty dict."""
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
def pool_id_to_vault_address(pool_id: str) -> str | None:
|
|
38
|
+
"""Fallback: return None."""
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def pool_id_to_chain_id(pool_id: str) -> int | None:
|
|
42
|
+
"""Fallback: return None."""
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def pools(
|
|
47
|
+
json_output: bool = typer.Option(
|
|
48
|
+
False, "--json", help="Emit responses as JSON."
|
|
49
|
+
),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Show all available pools with their names, IDs, vault addresses, and chain IDs.
|
|
52
|
+
|
|
53
|
+
USAGE:
|
|
54
|
+
------
|
|
55
|
+
cartha vault pools (or: cartha v pools)
|
|
56
|
+
cartha vault pools --json (for JSON output)
|
|
57
|
+
|
|
58
|
+
OUTPUT:
|
|
59
|
+
-------
|
|
60
|
+
- Pool names: BTCUSD, ETHUSD, EURUSD, etc.
|
|
61
|
+
- Pool IDs: Full hex identifiers (0x...)
|
|
62
|
+
- Vault addresses: Contract addresses for each pool
|
|
63
|
+
- Chain IDs: Which blockchain network (e.g., 84532 for Base Sepolia)
|
|
64
|
+
|
|
65
|
+
Use these pool names directly in 'cartha vault lock -p BTCUSD ...'
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
available_pools = list_pools()
|
|
69
|
+
|
|
70
|
+
if json_output:
|
|
71
|
+
# JSON output format
|
|
72
|
+
import json
|
|
73
|
+
|
|
74
|
+
pools_data = []
|
|
75
|
+
for pool_name, pool_id_hex in sorted(available_pools.items()):
|
|
76
|
+
vault_addr = pool_id_to_vault_address(pool_id_hex)
|
|
77
|
+
chain_id = pool_id_to_chain_id(pool_id_hex)
|
|
78
|
+
pool_data = {
|
|
79
|
+
"name": pool_name,
|
|
80
|
+
"pool_id": pool_id_hex,
|
|
81
|
+
}
|
|
82
|
+
if vault_addr:
|
|
83
|
+
pool_data["vault_address"] = vault_addr
|
|
84
|
+
if chain_id:
|
|
85
|
+
pool_data["chain_id"] = chain_id
|
|
86
|
+
pools_data.append(pool_data)
|
|
87
|
+
|
|
88
|
+
console.print(json.dumps(pools_data, indent=2))
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Multi-line text output
|
|
92
|
+
if not available_pools:
|
|
93
|
+
console.print("[yellow]No pools available.[/]")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
console.print("\n[bold cyan]Available Pools[/]\n")
|
|
97
|
+
|
|
98
|
+
for idx, (pool_name, pool_id_hex) in enumerate(sorted(available_pools.items()), 1):
|
|
99
|
+
vault_addr = pool_id_to_vault_address(pool_id_hex)
|
|
100
|
+
chain_id = pool_id_to_chain_id(pool_id_hex)
|
|
101
|
+
|
|
102
|
+
# Ensure full pool ID is displayed (normalize to ensure 0x prefix)
|
|
103
|
+
pool_id_display = pool_id_hex if pool_id_hex.startswith("0x") else f"0x{pool_id_hex}"
|
|
104
|
+
|
|
105
|
+
# Ensure full vault address is displayed
|
|
106
|
+
vault_display = vault_addr if vault_addr else "[dim]N/A[/]"
|
|
107
|
+
|
|
108
|
+
chain_display = str(chain_id) if chain_id else "[dim]N/A[/]"
|
|
109
|
+
|
|
110
|
+
console.print(f"[bold cyan]Pool {idx}:[/] {pool_name}")
|
|
111
|
+
console.print(f" [yellow]Pool ID:[/] {pool_id_display}")
|
|
112
|
+
console.print(f" [green]Vault Address:[/] {vault_display}")
|
|
113
|
+
console.print(f" [dim]Chain ID:[/] {chain_display}")
|
|
114
|
+
|
|
115
|
+
# Add spacing between pools except for the last one
|
|
116
|
+
if idx < len(available_pools):
|
|
117
|
+
console.print()
|
|
118
|
+
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
console.print(f"[bold red]Error:[/] Failed to list pools: {exc}")
|
|
121
|
+
raise typer.Exit(code=1)
|