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,1260 @@
|
|
|
1
|
+
"""Lock command - create new lock positions with verifier-signed EIP-712 LockRequest."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import webbrowser
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import urlencode
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich import box
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.prompt import Confirm
|
|
16
|
+
from rich.status import Status
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from web3 import Web3
|
|
19
|
+
|
|
20
|
+
from ..config import settings
|
|
21
|
+
from ..pair import build_pair_auth_payload, get_uid_from_hotkey
|
|
22
|
+
from ..utils import normalize_hex, usdc_to_base_units
|
|
23
|
+
from ..verifier import (
|
|
24
|
+
VerifierError,
|
|
25
|
+
get_lock_status,
|
|
26
|
+
process_lock_transaction,
|
|
27
|
+
request_lock_signature,
|
|
28
|
+
verify_hotkey,
|
|
29
|
+
)
|
|
30
|
+
from ..wallet import load_wallet
|
|
31
|
+
from .common import console, exit_with_error, handle_unexpected_exception
|
|
32
|
+
from .shared_options import (
|
|
33
|
+
wallet_name_option,
|
|
34
|
+
wallet_hotkey_option,
|
|
35
|
+
network_option,
|
|
36
|
+
pool_id_option,
|
|
37
|
+
chain_id_option,
|
|
38
|
+
vault_address_option,
|
|
39
|
+
owner_evm_option,
|
|
40
|
+
amount_option,
|
|
41
|
+
lock_days_option,
|
|
42
|
+
json_output_option,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Import pool helpers for pool_id conversion
|
|
46
|
+
try:
|
|
47
|
+
from ...testnet.pool_ids import (
|
|
48
|
+
format_pool_id,
|
|
49
|
+
list_pools,
|
|
50
|
+
pool_id_to_chain_id,
|
|
51
|
+
pool_id_to_name,
|
|
52
|
+
pool_id_to_vault_address,
|
|
53
|
+
pool_name_to_id,
|
|
54
|
+
vault_address_to_chain_id,
|
|
55
|
+
vault_address_to_pool_id,
|
|
56
|
+
)
|
|
57
|
+
except ImportError:
|
|
58
|
+
# Fallback if running from different context
|
|
59
|
+
import sys
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
|
|
62
|
+
# Try adding parent directory to path
|
|
63
|
+
testnet_dir = Path(__file__).parent.parent.parent / "testnet"
|
|
64
|
+
if testnet_dir.exists():
|
|
65
|
+
sys.path.insert(0, str(testnet_dir.parent))
|
|
66
|
+
try:
|
|
67
|
+
from testnet.pool_ids import (
|
|
68
|
+
format_pool_id,
|
|
69
|
+
list_pools,
|
|
70
|
+
pool_id_to_chain_id,
|
|
71
|
+
pool_id_to_name,
|
|
72
|
+
pool_id_to_vault_address,
|
|
73
|
+
pool_name_to_id,
|
|
74
|
+
vault_address_to_chain_id,
|
|
75
|
+
vault_address_to_pool_id,
|
|
76
|
+
)
|
|
77
|
+
except ImportError:
|
|
78
|
+
# Final fallback
|
|
79
|
+
def pool_name_to_id(pool_name: str) -> str:
|
|
80
|
+
"""Fallback: encode pool name as hex."""
|
|
81
|
+
name_bytes = pool_name.encode("utf-8")
|
|
82
|
+
padded = name_bytes.ljust(32, b"\x00")
|
|
83
|
+
return "0x" + padded.hex()
|
|
84
|
+
|
|
85
|
+
def pool_id_to_name(pool_id: str) -> str | None:
|
|
86
|
+
"""Fallback: try to decode."""
|
|
87
|
+
try:
|
|
88
|
+
hex_str = pool_id.lower().removeprefix("0x")
|
|
89
|
+
pool_bytes = bytes.fromhex(hex_str)
|
|
90
|
+
name = pool_bytes.rstrip(b"\x00").decode("utf-8", errors="ignore")
|
|
91
|
+
return name if name and name.isprintable() else None
|
|
92
|
+
except Exception:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def format_pool_id(pool_id: str) -> str:
|
|
96
|
+
"""Fallback: return pool_id as-is."""
|
|
97
|
+
return pool_id
|
|
98
|
+
|
|
99
|
+
def list_pools() -> dict[str, str]:
|
|
100
|
+
"""Fallback: return empty dict."""
|
|
101
|
+
return {}
|
|
102
|
+
|
|
103
|
+
def pool_id_to_vault_address(pool_id: str) -> str | None:
|
|
104
|
+
"""Fallback: return None."""
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def vault_address_to_pool_id(vault_address: str) -> str | None:
|
|
108
|
+
"""Fallback: return None."""
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def pool_id_to_chain_id(pool_id: str) -> int | None:
|
|
112
|
+
"""Fallback: return None."""
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def vault_address_to_chain_id(vault_address: str) -> int | None:
|
|
116
|
+
"""Fallback: return None."""
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def prove_lock(
|
|
121
|
+
coldkey: str | None = wallet_name_option(required=False),
|
|
122
|
+
hotkey: str | None = wallet_hotkey_option(required=False),
|
|
123
|
+
network: str = network_option(),
|
|
124
|
+
chain: int | None = chain_id_option(),
|
|
125
|
+
vault: str | None = vault_address_option(),
|
|
126
|
+
pool_id: str | None = pool_id_option(),
|
|
127
|
+
amount: str | None = amount_option(),
|
|
128
|
+
lock_days: int | None = lock_days_option(),
|
|
129
|
+
owner: str | None = owner_evm_option(),
|
|
130
|
+
json_output: bool = json_output_option(),
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Create a new lock position with verifier-signed EIP-712 LockRequest.
|
|
133
|
+
|
|
134
|
+
USAGE:
|
|
135
|
+
------
|
|
136
|
+
Interactive mode (recommended): Just run 'cartha vault lock' and follow prompts
|
|
137
|
+
With arguments: Provide flags to skip prompts (e.g., -w cold -wh hot -p BTCUSD -a 100 -d 30 -e 0xEVM...)
|
|
138
|
+
|
|
139
|
+
ALIASES:
|
|
140
|
+
--------
|
|
141
|
+
Wallet: --wallet-name, --coldkey, -w | --wallet-hotkey, --hotkey, -wh
|
|
142
|
+
Network: --network, -n (auto-maps: test=netuid 78, finney=netuid 35)
|
|
143
|
+
Pool: --pool-id, --pool, --poolid, -p (accepts names like BTCUSD or hex IDs)
|
|
144
|
+
Amount: --amount, -a | Lock days: --lock-days, --days, -d
|
|
145
|
+
Owner: --owner-evm, --owner, --evm, -e
|
|
146
|
+
Chain/Vault: auto-detected from pool (can override with --chain-id, --vault-address)
|
|
147
|
+
|
|
148
|
+
FLOW:
|
|
149
|
+
-----
|
|
150
|
+
1. Check registration on subnet (netuid auto-mapped from network)
|
|
151
|
+
2. Authenticate with Bittensor hotkey signature
|
|
152
|
+
3. Check for duplicate positions (rejects early if exists)
|
|
153
|
+
4. Request EIP-712 LockRequest signature from verifier
|
|
154
|
+
5. Open Cartha Lock UI for approval and lock transactions
|
|
155
|
+
6. Auto-detect transaction completion
|
|
156
|
+
7. Verifier automatically processes and adds to upcoming epoch
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
# Step 1: Determine netuid and verifier URL based on network
|
|
160
|
+
if network == "test":
|
|
161
|
+
netuid = 78
|
|
162
|
+
elif network == "finney":
|
|
163
|
+
netuid = 35
|
|
164
|
+
# Warn that mainnet is not live yet
|
|
165
|
+
console.print()
|
|
166
|
+
console.print("[bold yellow]⚠️ MAINNET NOT AVAILABLE YET[/]")
|
|
167
|
+
console.print()
|
|
168
|
+
console.print("[yellow]Cartha subnet is currently in testnet phase (subnet 78 on test network).[/]")
|
|
169
|
+
console.print("[yellow]Mainnet (subnet 35 on finney network) has not been announced yet.[/]")
|
|
170
|
+
console.print()
|
|
171
|
+
console.print("[bold cyan]To use testnet:[/]")
|
|
172
|
+
console.print(" cartha vault lock --network test ...")
|
|
173
|
+
console.print()
|
|
174
|
+
console.print("[dim]If you continue with finney network, the CLI will attempt to connect[/]")
|
|
175
|
+
console.print("[dim]but the subnet may not be operational yet.[/]")
|
|
176
|
+
console.print()
|
|
177
|
+
if not Confirm.ask("[yellow]Continue with finney network anyway?[/]", default=False):
|
|
178
|
+
console.print("[yellow]Cancelled. Use --network test for testnet.[/]")
|
|
179
|
+
raise typer.Exit(code=0)
|
|
180
|
+
else:
|
|
181
|
+
# Default to finney settings if unknown network
|
|
182
|
+
netuid = 35
|
|
183
|
+
|
|
184
|
+
# Auto-map verifier URL based on network (if not explicitly set via env var)
|
|
185
|
+
from ..config import get_verifier_url_for_network
|
|
186
|
+
expected_verifier_url = get_verifier_url_for_network(network)
|
|
187
|
+
if settings.verifier_url != expected_verifier_url:
|
|
188
|
+
console.print(
|
|
189
|
+
f"[dim]Using verifier for {network} network: {expected_verifier_url}[/]"
|
|
190
|
+
)
|
|
191
|
+
# Update settings for this session
|
|
192
|
+
settings.verifier_url = expected_verifier_url
|
|
193
|
+
|
|
194
|
+
# Step 2: Collect coldkey and hotkey
|
|
195
|
+
if coldkey is None:
|
|
196
|
+
coldkey = typer.prompt("Coldkey wallet name", default="default")
|
|
197
|
+
if hotkey is None:
|
|
198
|
+
hotkey = typer.prompt("Hotkey name", default="default")
|
|
199
|
+
|
|
200
|
+
# Load wallet to get hotkey SS58 address
|
|
201
|
+
wallet = load_wallet(coldkey, hotkey)
|
|
202
|
+
hotkey_ss58 = wallet.hotkey.ss58_address
|
|
203
|
+
|
|
204
|
+
console.print(f"\n[bold cyan]Checking registration...[/]")
|
|
205
|
+
console.print(f"[dim]Hotkey:[/] {hotkey_ss58}")
|
|
206
|
+
console.print(f"[dim]Network:[/] {network} (netuid: {netuid})")
|
|
207
|
+
|
|
208
|
+
# Step 3: Check registration via Bittensor network (same as other commands)
|
|
209
|
+
try:
|
|
210
|
+
with console.status(
|
|
211
|
+
"[bold cyan]Checking miner registration status...[/]",
|
|
212
|
+
spinner="dots",
|
|
213
|
+
):
|
|
214
|
+
uid = get_uid_from_hotkey(
|
|
215
|
+
network=network,
|
|
216
|
+
netuid=netuid,
|
|
217
|
+
hotkey=hotkey_ss58,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if uid is None:
|
|
221
|
+
console.print(
|
|
222
|
+
"[bold red]Error:[/] Hotkey is not registered or has been deregistered "
|
|
223
|
+
f"on netuid {netuid} ({network} network)."
|
|
224
|
+
)
|
|
225
|
+
console.print(
|
|
226
|
+
"[yellow]Please register your hotkey first using 'cartha miner register'.[/]"
|
|
227
|
+
)
|
|
228
|
+
raise typer.Exit(code=1)
|
|
229
|
+
|
|
230
|
+
console.print(f"[bold green]✓ Registered[/] - UID: {uid}")
|
|
231
|
+
except typer.Exit:
|
|
232
|
+
raise
|
|
233
|
+
except Exception as exc:
|
|
234
|
+
handle_unexpected_exception("Registration check failed", exc)
|
|
235
|
+
|
|
236
|
+
# Step 4: Generate Bittensor signature for authentication
|
|
237
|
+
console.print(f"\n[bold cyan]Authenticating with Bittensor hotkey...[/]")
|
|
238
|
+
try:
|
|
239
|
+
auth_payload = build_pair_auth_payload(
|
|
240
|
+
network=network,
|
|
241
|
+
netuid=netuid,
|
|
242
|
+
slot=str(uid),
|
|
243
|
+
hotkey=hotkey_ss58,
|
|
244
|
+
wallet_name=coldkey,
|
|
245
|
+
wallet_hotkey=hotkey,
|
|
246
|
+
skip_metagraph_check=True, # Already checked via verifier
|
|
247
|
+
challenge_prefix="cartha-lock",
|
|
248
|
+
)
|
|
249
|
+
except Exception as exc:
|
|
250
|
+
handle_unexpected_exception("Failed to generate Bittensor signature", exc)
|
|
251
|
+
|
|
252
|
+
# Step 5: Verify hotkey and get session token
|
|
253
|
+
try:
|
|
254
|
+
auth_result = verify_hotkey(
|
|
255
|
+
hotkey=hotkey_ss58,
|
|
256
|
+
signature=auth_payload["signature"],
|
|
257
|
+
message=auth_payload["message"],
|
|
258
|
+
)
|
|
259
|
+
session_token = auth_result["session_token"]
|
|
260
|
+
expires_at = auth_result["expires_at"]
|
|
261
|
+
console.print(
|
|
262
|
+
f"[bold green]✓ Authenticated[/] - Session expires at {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime(expires_at))}"
|
|
263
|
+
)
|
|
264
|
+
except VerifierError as exc:
|
|
265
|
+
exit_with_error(f"Authentication failed: {exc}")
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
handle_unexpected_exception("Authentication failed", exc)
|
|
268
|
+
|
|
269
|
+
# Step 6: Collect lock parameters
|
|
270
|
+
console.print(f"\n[bold cyan]Collecting lock parameters...[/]")
|
|
271
|
+
|
|
272
|
+
# Chain ID - will be auto-detected after pool_id/vault is selected
|
|
273
|
+
# We'll set it after vault matching
|
|
274
|
+
|
|
275
|
+
# Pool ID (collect first, then auto-match vault)
|
|
276
|
+
available_pools = list_pools()
|
|
277
|
+
if pool_id is None:
|
|
278
|
+
# Show available pools if we have them
|
|
279
|
+
if available_pools:
|
|
280
|
+
console.print("\n[bold cyan]Available pools:[/]")
|
|
281
|
+
for pool_name, pool_id_hex in sorted(available_pools.items()):
|
|
282
|
+
vault_addr = pool_id_to_vault_address(pool_id_hex)
|
|
283
|
+
if vault_addr:
|
|
284
|
+
console.print(
|
|
285
|
+
f" - {pool_name}: {format_pool_id(pool_id_hex)} "
|
|
286
|
+
f"[dim](Vault: {vault_addr})[/]"
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
console.print(f" - {pool_name}: {format_pool_id(pool_id_hex)}")
|
|
290
|
+
console.print()
|
|
291
|
+
|
|
292
|
+
while True:
|
|
293
|
+
pool_input = typer.prompt(
|
|
294
|
+
"Pool ID (name or hex string)", show_default=False
|
|
295
|
+
)
|
|
296
|
+
pool_id_clean = pool_input.strip()
|
|
297
|
+
|
|
298
|
+
# Check if it's a readable pool name
|
|
299
|
+
pool_id_upper = pool_id_clean.upper()
|
|
300
|
+
if available_pools and pool_id_upper in available_pools:
|
|
301
|
+
pool_id = pool_name_to_id(pool_id_upper).lower()
|
|
302
|
+
console.print(
|
|
303
|
+
f"[dim]Converted pool name to ID:[/] {pool_id_upper} → {format_pool_id(pool_id)}"
|
|
304
|
+
)
|
|
305
|
+
break
|
|
306
|
+
# Check if it's a hex string
|
|
307
|
+
elif pool_id_clean.startswith("0x") and len(pool_id_clean) == 66:
|
|
308
|
+
pool_id = pool_id_clean.lower()
|
|
309
|
+
break
|
|
310
|
+
else:
|
|
311
|
+
# Try to normalize
|
|
312
|
+
pool_id_normalized = normalize_hex(pool_id_clean).lower()
|
|
313
|
+
if len(pool_id_normalized) == 66:
|
|
314
|
+
pool_id = pool_id_normalized
|
|
315
|
+
break
|
|
316
|
+
console.print(
|
|
317
|
+
"[bold red]Error:[/] Pool ID must be a recognized pool name or a 66-character hex string (0x...)"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Normalize pool_id - handle both pool names and hex strings
|
|
321
|
+
else:
|
|
322
|
+
# Pool ID was provided via command line, check if it's a pool name
|
|
323
|
+
pool_id_clean = pool_id.strip()
|
|
324
|
+
pool_id_upper = pool_id_clean.upper()
|
|
325
|
+
|
|
326
|
+
# Check if it's a readable pool name
|
|
327
|
+
if available_pools and pool_id_upper in available_pools:
|
|
328
|
+
pool_id = pool_name_to_id(pool_id_upper).lower()
|
|
329
|
+
console.print(
|
|
330
|
+
f"[dim]Converted pool name to ID:[/] {pool_id_upper} → {format_pool_id(pool_id)}"
|
|
331
|
+
)
|
|
332
|
+
# Check if it's already a hex string
|
|
333
|
+
elif pool_id_clean.startswith("0x") and len(pool_id_clean) == 66:
|
|
334
|
+
pool_id = pool_id_clean.lower()
|
|
335
|
+
else:
|
|
336
|
+
# Try to normalize as hex
|
|
337
|
+
pool_id_normalized = normalize_hex(pool_id_clean).lower()
|
|
338
|
+
if len(pool_id_normalized) == 66:
|
|
339
|
+
pool_id = pool_id_normalized
|
|
340
|
+
else:
|
|
341
|
+
# If not a valid hex and not a known pool name, just normalize it
|
|
342
|
+
if not pool_id.startswith("0x"):
|
|
343
|
+
pool_id = "0x" + pool_id
|
|
344
|
+
pool_id = pool_id.lower()
|
|
345
|
+
|
|
346
|
+
# Auto-match vault address from pool ID
|
|
347
|
+
if vault is None:
|
|
348
|
+
auto_vault = pool_id_to_vault_address(pool_id)
|
|
349
|
+
if auto_vault:
|
|
350
|
+
vault = Web3.to_checksum_address(auto_vault)
|
|
351
|
+
pool_name = pool_id_to_name(pool_id)
|
|
352
|
+
console.print(
|
|
353
|
+
f"[bold green]✓ Auto-matched vault[/] - {pool_name or 'Pool'} → {vault}"
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
# Fallback: prompt for vault if no mapping found
|
|
357
|
+
console.print(
|
|
358
|
+
"[yellow]⚠ No vault mapping found for this pool ID. Please provide vault address.[/]"
|
|
359
|
+
)
|
|
360
|
+
while True:
|
|
361
|
+
vault = typer.prompt("Vault contract address", show_default=False)
|
|
362
|
+
if Web3.is_address(vault):
|
|
363
|
+
vault = Web3.to_checksum_address(vault)
|
|
364
|
+
break
|
|
365
|
+
console.print(
|
|
366
|
+
"[bold red]Error:[/] Vault address must be a valid EVM address (0x...)"
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
# Vault was provided, verify it matches pool ID if possible
|
|
370
|
+
if Web3.is_address(vault):
|
|
371
|
+
vault = Web3.to_checksum_address(vault)
|
|
372
|
+
expected_pool_id = vault_address_to_pool_id(vault)
|
|
373
|
+
if expected_pool_id and expected_pool_id.lower() != pool_id.lower():
|
|
374
|
+
pool_name = pool_id_to_name(pool_id)
|
|
375
|
+
expected_pool_name = pool_id_to_name(expected_pool_id)
|
|
376
|
+
console.print(
|
|
377
|
+
f"[bold yellow]⚠ Warning:[/] Vault {vault} is mapped to pool "
|
|
378
|
+
f"{expected_pool_name or expected_pool_id}, but you selected "
|
|
379
|
+
f"{pool_name or pool_id}"
|
|
380
|
+
)
|
|
381
|
+
if not Confirm.ask(
|
|
382
|
+
"[yellow]Continue anyway?[/]", default=False
|
|
383
|
+
):
|
|
384
|
+
raise typer.Exit(code=1)
|
|
385
|
+
else:
|
|
386
|
+
exit_with_error("Invalid vault address format")
|
|
387
|
+
|
|
388
|
+
# Auto-match chain ID from pool ID or vault address
|
|
389
|
+
if chain is None:
|
|
390
|
+
# Try to get chain ID from pool ID first
|
|
391
|
+
auto_chain_id = None
|
|
392
|
+
try:
|
|
393
|
+
auto_chain_id = pool_id_to_chain_id(pool_id)
|
|
394
|
+
except NameError:
|
|
395
|
+
# Function not available - this shouldn't happen if imports worked
|
|
396
|
+
# But handle gracefully by trying to import it
|
|
397
|
+
try:
|
|
398
|
+
from testnet.pool_ids import pool_id_to_chain_id
|
|
399
|
+
auto_chain_id = pool_id_to_chain_id(pool_id)
|
|
400
|
+
except ImportError:
|
|
401
|
+
pass
|
|
402
|
+
|
|
403
|
+
if not auto_chain_id:
|
|
404
|
+
# Fallback: try to get from vault address
|
|
405
|
+
try:
|
|
406
|
+
auto_chain_id = vault_address_to_chain_id(vault)
|
|
407
|
+
except NameError:
|
|
408
|
+
try:
|
|
409
|
+
from testnet.pool_ids import vault_address_to_chain_id
|
|
410
|
+
auto_chain_id = vault_address_to_chain_id(vault)
|
|
411
|
+
except ImportError:
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
if auto_chain_id:
|
|
415
|
+
chain = auto_chain_id
|
|
416
|
+
chain_name = "Base Sepolia" if chain == 84532 else f"Chain {chain}"
|
|
417
|
+
console.print(
|
|
418
|
+
f"[bold green]✓ Auto-matched chain ID[/] - {chain_name} (chain ID: {chain})"
|
|
419
|
+
)
|
|
420
|
+
else:
|
|
421
|
+
# Fallback: prompt for chain ID if no mapping found
|
|
422
|
+
console.print(
|
|
423
|
+
"[yellow]⚠ No chain ID mapping found. Please provide chain ID.[/]"
|
|
424
|
+
)
|
|
425
|
+
while True:
|
|
426
|
+
try:
|
|
427
|
+
chain_input = typer.prompt("Chain ID", show_default=False)
|
|
428
|
+
chain = int(chain_input)
|
|
429
|
+
if chain <= 0:
|
|
430
|
+
console.print(
|
|
431
|
+
"[bold red]Error:[/] Chain ID must be a positive integer"
|
|
432
|
+
)
|
|
433
|
+
continue
|
|
434
|
+
break
|
|
435
|
+
except ValueError:
|
|
436
|
+
console.print("[bold red]Error:[/] Chain ID must be a valid integer")
|
|
437
|
+
else:
|
|
438
|
+
# Chain ID was provided, verify it matches vault if possible
|
|
439
|
+
expected_chain_id = None
|
|
440
|
+
try:
|
|
441
|
+
expected_chain_id = vault_address_to_chain_id(vault)
|
|
442
|
+
except NameError:
|
|
443
|
+
try:
|
|
444
|
+
from testnet.pool_ids import vault_address_to_chain_id
|
|
445
|
+
expected_chain_id = vault_address_to_chain_id(vault)
|
|
446
|
+
except ImportError:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
if expected_chain_id and expected_chain_id != chain:
|
|
450
|
+
chain_name = "Base Sepolia" if expected_chain_id == 84532 else f"Chain {expected_chain_id}"
|
|
451
|
+
console.print(
|
|
452
|
+
f"[bold yellow]⚠ Warning:[/] Vault {vault} is on {chain_name} (chain ID: {expected_chain_id}), "
|
|
453
|
+
f"but you specified chain ID {chain}"
|
|
454
|
+
)
|
|
455
|
+
if not Confirm.ask(
|
|
456
|
+
"[yellow]Continue anyway?[/]", default=False
|
|
457
|
+
):
|
|
458
|
+
raise typer.Exit(code=1)
|
|
459
|
+
|
|
460
|
+
# Amount
|
|
461
|
+
amount_base_units: int | None = None
|
|
462
|
+
if amount is None:
|
|
463
|
+
while True:
|
|
464
|
+
try:
|
|
465
|
+
amount_input = typer.prompt(
|
|
466
|
+
"Lock amount in USDC (e.g. 250.5)", show_default=False
|
|
467
|
+
)
|
|
468
|
+
amount_base_units = usdc_to_base_units(amount_input)
|
|
469
|
+
if amount_base_units <= 0:
|
|
470
|
+
console.print("[bold red]Error:[/] Amount must be positive")
|
|
471
|
+
continue
|
|
472
|
+
break
|
|
473
|
+
except Exception as exc:
|
|
474
|
+
console.print(f"[bold red]Error:[/] Invalid amount: {exc}")
|
|
475
|
+
else:
|
|
476
|
+
try:
|
|
477
|
+
amount_as_int = int(float(amount))
|
|
478
|
+
if amount_as_int >= 1_000_000_000:
|
|
479
|
+
amount_base_units = amount_as_int
|
|
480
|
+
else:
|
|
481
|
+
amount_base_units = usdc_to_base_units(amount)
|
|
482
|
+
except (ValueError, Exception):
|
|
483
|
+
amount_base_units = usdc_to_base_units(amount)
|
|
484
|
+
|
|
485
|
+
# Lock days
|
|
486
|
+
if lock_days is None:
|
|
487
|
+
while True:
|
|
488
|
+
try:
|
|
489
|
+
lock_days_input = typer.prompt(
|
|
490
|
+
"Lock duration in days (e.g., 365)", show_default=False
|
|
491
|
+
)
|
|
492
|
+
lock_days = int(lock_days_input)
|
|
493
|
+
if lock_days <= 0:
|
|
494
|
+
console.print(
|
|
495
|
+
"[bold red]Error:[/] Lock days must be positive"
|
|
496
|
+
)
|
|
497
|
+
continue
|
|
498
|
+
if lock_days > 1825: # 5 years max
|
|
499
|
+
console.print(
|
|
500
|
+
"[bold red]Error:[/] Lock days cannot exceed 1825 (5 years)"
|
|
501
|
+
)
|
|
502
|
+
continue
|
|
503
|
+
break
|
|
504
|
+
except ValueError:
|
|
505
|
+
console.print("[bold red]Error:[/] Lock days must be a valid integer")
|
|
506
|
+
|
|
507
|
+
# Owner (EVM address)
|
|
508
|
+
if owner is None:
|
|
509
|
+
while True:
|
|
510
|
+
owner = typer.prompt("EVM address (owner)", show_default=False)
|
|
511
|
+
if Web3.is_address(owner):
|
|
512
|
+
owner = Web3.to_checksum_address(owner)
|
|
513
|
+
break
|
|
514
|
+
console.print(
|
|
515
|
+
"[bold red]Error:[/] EVM address must be a valid address (0x...)"
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
if not Web3.is_address(owner):
|
|
519
|
+
exit_with_error("Invalid EVM address format")
|
|
520
|
+
owner = Web3.to_checksum_address(owner)
|
|
521
|
+
|
|
522
|
+
# Step 7: Check for existing position to avoid wasting user's time
|
|
523
|
+
console.print(f"\n[bold cyan]Checking for existing positions...[/]")
|
|
524
|
+
try:
|
|
525
|
+
from ..verifier import fetch_miner_status
|
|
526
|
+
|
|
527
|
+
existing_status = fetch_miner_status(hotkey=hotkey_ss58, slot=str(uid))
|
|
528
|
+
|
|
529
|
+
# Check if position already exists with same pool + EVM
|
|
530
|
+
if existing_status.get("pools"):
|
|
531
|
+
for pool in existing_status["pools"]:
|
|
532
|
+
pool_id_existing = pool.get("pool_id", "").lower()
|
|
533
|
+
evm_existing = pool.get("evm_address", "").lower()
|
|
534
|
+
|
|
535
|
+
if pool_id_existing == pool_id.lower() and evm_existing == owner.lower():
|
|
536
|
+
# Found duplicate!
|
|
537
|
+
pool_name_display = pool.get("pool_name", "").upper() or "Pool"
|
|
538
|
+
console.print(
|
|
539
|
+
f"\n[bold red]Error: Position already exists![/]\n"
|
|
540
|
+
)
|
|
541
|
+
console.print(
|
|
542
|
+
f"[yellow]You already have a lock position on {pool_name_display} with this EVM address:[/]"
|
|
543
|
+
)
|
|
544
|
+
console.print(f" • Amount: [bold]{pool.get('amount_usdc', 0):.2f} USDC[/]")
|
|
545
|
+
console.print(f" • Lock days: [bold]{pool.get('lock_days', 0)}[/]")
|
|
546
|
+
console.print(f" • EVM: [dim]{pool.get('evm_address')}[/]")
|
|
547
|
+
|
|
548
|
+
expires_at = pool.get('expires_at')
|
|
549
|
+
if expires_at:
|
|
550
|
+
console.print(f" • Expires: [bold]{expires_at}[/]")
|
|
551
|
+
|
|
552
|
+
console.print()
|
|
553
|
+
console.print(
|
|
554
|
+
f"[bold cyan]To add more USDC or extend your lock period:[/]\n"
|
|
555
|
+
f" Visit: [bold]https://cartha.finance/manage[/]"
|
|
556
|
+
)
|
|
557
|
+
console.print()
|
|
558
|
+
console.print(
|
|
559
|
+
f"[dim]Note: You can create a new position on the same pool using a different EVM address.[/]"
|
|
560
|
+
)
|
|
561
|
+
raise typer.Exit(code=1)
|
|
562
|
+
|
|
563
|
+
console.print("[dim]✓ No existing position found - proceeding...[/]")
|
|
564
|
+
except typer.Exit:
|
|
565
|
+
raise
|
|
566
|
+
except Exception as check_exc:
|
|
567
|
+
# If status check fails, log warning but continue (don't block users if verifier is down)
|
|
568
|
+
console.print(
|
|
569
|
+
f"[yellow]Warning: Could not check existing positions ({check_exc})[/]"
|
|
570
|
+
)
|
|
571
|
+
console.print("[dim]Continuing anyway...[/]")
|
|
572
|
+
|
|
573
|
+
# Step 8: Request EIP-712 LockRequest signature from verifier
|
|
574
|
+
console.print(f"\n[bold cyan]Requesting signature from verifier...[/]")
|
|
575
|
+
try:
|
|
576
|
+
sig_result = request_lock_signature(
|
|
577
|
+
session_token=session_token,
|
|
578
|
+
pool_id=pool_id,
|
|
579
|
+
amount=amount_base_units,
|
|
580
|
+
lock_days=lock_days,
|
|
581
|
+
hotkey=hotkey_ss58,
|
|
582
|
+
miner_slot=str(uid),
|
|
583
|
+
uid=str(uid),
|
|
584
|
+
owner=owner,
|
|
585
|
+
chain_id=chain,
|
|
586
|
+
vault_address=vault,
|
|
587
|
+
)
|
|
588
|
+
signature = sig_result["signature"]
|
|
589
|
+
timestamp = sig_result["timestamp"]
|
|
590
|
+
nonce = sig_result["nonce"]
|
|
591
|
+
expires_at_sig = sig_result["expiresAt"]
|
|
592
|
+
approve_tx = sig_result["approveTx"]
|
|
593
|
+
lock_tx = sig_result["lockTx"]
|
|
594
|
+
|
|
595
|
+
console.print(f"[bold green]✓ Signature received[/]")
|
|
596
|
+
console.print(f"[dim]Expires at:[/] {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime(expires_at_sig))}")
|
|
597
|
+
except VerifierError as exc:
|
|
598
|
+
exit_with_error(f"Failed to request signature: {exc}")
|
|
599
|
+
except Exception as exc:
|
|
600
|
+
handle_unexpected_exception("Signature request failed", exc)
|
|
601
|
+
|
|
602
|
+
# Step 9: Display lock details and get confirmation
|
|
603
|
+
console.print(f"\n[bold cyan]Lock Details:[/]")
|
|
604
|
+
summary_table = Table(show_header=False, box=box.SIMPLE)
|
|
605
|
+
summary_table.add_column(style="cyan")
|
|
606
|
+
summary_table.add_column(style="yellow")
|
|
607
|
+
|
|
608
|
+
# Show pool name if available
|
|
609
|
+
pool_name = pool_id_to_name(pool_id)
|
|
610
|
+
pool_display = (
|
|
611
|
+
pool_name.upper() if pool_name else format_pool_id(pool_id)
|
|
612
|
+
)
|
|
613
|
+
summary_table.add_row("Pool", pool_display)
|
|
614
|
+
|
|
615
|
+
human_amount = Decimal(amount_base_units) / Decimal(10**6)
|
|
616
|
+
amount_str = f"{human_amount:.6f}".rstrip("0").rstrip(".")
|
|
617
|
+
summary_table.add_row("Amount", f"{amount_str} USDC ({amount_base_units} base units)")
|
|
618
|
+
|
|
619
|
+
summary_table.add_row("Lock Days", str(lock_days))
|
|
620
|
+
summary_table.add_row("Owner (EVM)", owner)
|
|
621
|
+
summary_table.add_row("Hotkey", hotkey_ss58)
|
|
622
|
+
summary_table.add_row("UID", str(uid))
|
|
623
|
+
summary_table.add_row("Chain ID", str(chain))
|
|
624
|
+
summary_table.add_row("Vault", vault)
|
|
625
|
+
|
|
626
|
+
# Calculate unlock date
|
|
627
|
+
unlock_timestamp = int(time.time()) + (lock_days * 24 * 60 * 60)
|
|
628
|
+
unlock_date = time.strftime("%Y-%m-%d", time.gmtime(unlock_timestamp))
|
|
629
|
+
summary_table.add_row("Unlock Date", unlock_date)
|
|
630
|
+
|
|
631
|
+
console.print(summary_table)
|
|
632
|
+
console.print()
|
|
633
|
+
|
|
634
|
+
# LP Risk Disclosure
|
|
635
|
+
console.print(Panel(
|
|
636
|
+
"[bold yellow]⚠️ LIQUIDITY PROVIDER RISK DISCLOSURE[/]\n\n"
|
|
637
|
+
"By locking USDC, you agree that:\n\n"
|
|
638
|
+
"• Your funds will be used as DEX liquidity for leveraged trading\n"
|
|
639
|
+
"• Liquidation events may result in partial loss of capital\n"
|
|
640
|
+
"• Lost funds are NOT reimbursed - this is the LP risk model\n"
|
|
641
|
+
"• You earn subnet rewards + liquidation fees in return\n"
|
|
642
|
+
"• Minimum collateral: 100k USDC to maintain full emission scoring\n"
|
|
643
|
+
"• If your withdrawable balance falls below 100k USDC, your emission scoring will be reduced\n\n"
|
|
644
|
+
"[bold red]Only commit funds you can afford to lose.[/]\n\n"
|
|
645
|
+
"[dim]This disclosure is required for all liquidity providers.[/]\n"
|
|
646
|
+
"[dim]more information: https://docs.0xmarkets.io/legal-and-risk[/]",
|
|
647
|
+
title="[bold red]Important - Read Carefully[/]",
|
|
648
|
+
border_style="red",
|
|
649
|
+
padding=(1, 2),
|
|
650
|
+
))
|
|
651
|
+
console.print()
|
|
652
|
+
|
|
653
|
+
if not Confirm.ask(
|
|
654
|
+
"[bold yellow]I understand the risks and wish to proceed[/]",
|
|
655
|
+
default=False,
|
|
656
|
+
):
|
|
657
|
+
console.print("[bold red]Lock cancelled. Your funds remain safe.[/]")
|
|
658
|
+
raise typer.Exit(code=0)
|
|
659
|
+
|
|
660
|
+
console.print()
|
|
661
|
+
if not Confirm.ask(
|
|
662
|
+
"[bold yellow]Proceed with lock creation?[/]", default=True
|
|
663
|
+
):
|
|
664
|
+
console.print("[bold yellow]Cancelled.[/]")
|
|
665
|
+
raise typer.Exit(code=0)
|
|
666
|
+
|
|
667
|
+
# Step 10: Display transaction data - Phase 1: Approve
|
|
668
|
+
console.print(f"\n[bold cyan]Transaction Data[/]")
|
|
669
|
+
console.print(
|
|
670
|
+
"\n[bold yellow]⚠️ Execute these transactions to complete your lock:[/]\n"
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
# USDC contract address for Base Sepolia
|
|
674
|
+
usdc_contract_address = "0x2340D09c348930A76c8c2783EDa8610F699A51A8"
|
|
675
|
+
|
|
676
|
+
console.print("[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/]")
|
|
677
|
+
console.print("[bold]Phase 1: Approve USDC[/]")
|
|
678
|
+
console.print("[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/]")
|
|
679
|
+
console.print()
|
|
680
|
+
console.print("[bold]Fields for Frontend Approve Page:[/]")
|
|
681
|
+
console.print()
|
|
682
|
+
console.print(f" [yellow]Contract Address (USDC)[/]: {usdc_contract_address}")
|
|
683
|
+
console.print(f" [yellow]spender[/] (address): {vault}")
|
|
684
|
+
console.print(f" [yellow]amount[/] (uint256): {amount_base_units}")
|
|
685
|
+
console.print(f" [yellow]owner[/] (address): {owner}")
|
|
686
|
+
console.print()
|
|
687
|
+
|
|
688
|
+
# Open browser with frontend URL for Phase 1
|
|
689
|
+
frontend_url = settings.lock_ui_url
|
|
690
|
+
phase1_params = {
|
|
691
|
+
"phase": "1",
|
|
692
|
+
"chainId": str(chain),
|
|
693
|
+
"usdcAddress": "0x2340D09c348930A76c8c2783EDa8610F699A51A8",
|
|
694
|
+
"vaultAddress": vault,
|
|
695
|
+
"spender": vault,
|
|
696
|
+
"amount": str(amount_base_units),
|
|
697
|
+
"owner": owner,
|
|
698
|
+
}
|
|
699
|
+
phase1_url = f"{frontend_url}?{urlencode(phase1_params)}"
|
|
700
|
+
console.print(f"\n[bold cyan]Opening lock interface...[/]")
|
|
701
|
+
console.print(f"URL: [cyan]{phase1_url}[/]")
|
|
702
|
+
try:
|
|
703
|
+
webbrowser.open(phase1_url)
|
|
704
|
+
except Exception as e:
|
|
705
|
+
console.print(f"[bold yellow]Warning:[/] Could not open browser automatically: {e}")
|
|
706
|
+
console.print(f"Please manually open: [cyan]{phase1_url}[/]")
|
|
707
|
+
|
|
708
|
+
# Auto-detect approval by polling USDC allowance
|
|
709
|
+
console.print(f"\n[bold cyan]Waiting for approval transaction...[/]")
|
|
710
|
+
console.print("[dim]The CLI will automatically detect when the approval is complete.[/]")
|
|
711
|
+
console.print("[dim]You can also press Ctrl+C to skip and continue manually.[/]")
|
|
712
|
+
|
|
713
|
+
# Base Sepolia RPC endpoint
|
|
714
|
+
base_sepolia_rpc = "https://sepolia.base.org"
|
|
715
|
+
|
|
716
|
+
# ERC20 ABI for allowance function and Approval event
|
|
717
|
+
erc20_abi = [
|
|
718
|
+
{
|
|
719
|
+
"constant": True,
|
|
720
|
+
"inputs": [
|
|
721
|
+
{"name": "_owner", "type": "address"},
|
|
722
|
+
{"name": "_spender", "type": "address"}
|
|
723
|
+
],
|
|
724
|
+
"name": "allowance",
|
|
725
|
+
"outputs": [{"name": "", "type": "uint256"}],
|
|
726
|
+
"type": "function"
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
"anonymous": False,
|
|
730
|
+
"inputs": [
|
|
731
|
+
{"indexed": True, "name": "owner", "type": "address"},
|
|
732
|
+
{"indexed": True, "name": "spender", "type": "address"},
|
|
733
|
+
{"indexed": False, "name": "value", "type": "uint256"}
|
|
734
|
+
],
|
|
735
|
+
"name": "Approval",
|
|
736
|
+
"type": "event"
|
|
737
|
+
}
|
|
738
|
+
]
|
|
739
|
+
|
|
740
|
+
max_polls = 60 # Poll for up to 5 minutes (60 * 5 seconds)
|
|
741
|
+
poll_interval = 5 # Check every 5 seconds
|
|
742
|
+
|
|
743
|
+
approval_detected = False
|
|
744
|
+
try:
|
|
745
|
+
w3 = Web3(Web3.HTTPProvider(base_sepolia_rpc))
|
|
746
|
+
usdc_contract = w3.eth.contract(
|
|
747
|
+
address=Web3.to_checksum_address(usdc_contract_address),
|
|
748
|
+
abi=erc20_abi
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# Also check for Approval events to detect which address actually approved
|
|
752
|
+
# This helps if user approves with different wallet than specified
|
|
753
|
+
approval_event_signature = Web3.keccak(text="Approval(address,address,uint256)").hex()
|
|
754
|
+
|
|
755
|
+
with Status(
|
|
756
|
+
"[bold cyan]Waiting for approval transaction...[/]",
|
|
757
|
+
console=console,
|
|
758
|
+
spinner="dots",
|
|
759
|
+
) as status:
|
|
760
|
+
for poll_num in range(max_polls):
|
|
761
|
+
try:
|
|
762
|
+
# Check current allowance for the specified owner
|
|
763
|
+
current_allowance = usdc_contract.functions.allowance(
|
|
764
|
+
Web3.to_checksum_address(owner),
|
|
765
|
+
Web3.to_checksum_address(vault)
|
|
766
|
+
).call()
|
|
767
|
+
|
|
768
|
+
if current_allowance >= amount_base_units:
|
|
769
|
+
approval_detected = True
|
|
770
|
+
status.stop()
|
|
771
|
+
console.print("\n[bold green]✓ Approval detected![/]")
|
|
772
|
+
console.print(f"[dim]Current allowance: {current_allowance} (required: {amount_base_units})[/]")
|
|
773
|
+
break
|
|
774
|
+
|
|
775
|
+
# Also check recent Approval events to see if approval happened with different address
|
|
776
|
+
# Get latest block number
|
|
777
|
+
latest_block = w3.eth.block_number
|
|
778
|
+
# Check last 10 blocks for Approval events
|
|
779
|
+
from_block = max(0, latest_block - 10)
|
|
780
|
+
|
|
781
|
+
try:
|
|
782
|
+
# Filter for Approval events where spender is the vault
|
|
783
|
+
events = usdc_contract.events.Approval.get_logs(
|
|
784
|
+
fromBlock=from_block,
|
|
785
|
+
toBlock=latest_block,
|
|
786
|
+
argument_filters={'spender': Web3.to_checksum_address(vault)}
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
# Check if any recent approval has sufficient amount
|
|
790
|
+
for event in events:
|
|
791
|
+
if event.args.value >= amount_base_units:
|
|
792
|
+
# Found approval with sufficient amount, but from different owner
|
|
793
|
+
actual_owner = event.args.owner
|
|
794
|
+
if actual_owner.lower() != owner.lower():
|
|
795
|
+
status.stop()
|
|
796
|
+
console.print(f"\n[yellow]⚠ Approval detected, but from different address![/]")
|
|
797
|
+
console.print(f"[dim]Expected owner: {owner}[/]")
|
|
798
|
+
console.print(f"[dim]Actual approver: {actual_owner}[/]")
|
|
799
|
+
console.print(f"[dim]Approval amount: {event.args.value} (required: {amount_base_units})[/]")
|
|
800
|
+
console.print(f"\n[yellow]Please approve with the correct wallet address: {owner}[/]")
|
|
801
|
+
console.print("[dim]Or update the owner address in the CLI to match the wallet you used.[/]")
|
|
802
|
+
approval_detected = False
|
|
803
|
+
break
|
|
804
|
+
except Exception:
|
|
805
|
+
# If event filtering fails, continue with normal polling
|
|
806
|
+
pass
|
|
807
|
+
|
|
808
|
+
# Update status message
|
|
809
|
+
status.update(
|
|
810
|
+
f"[bold cyan]Waiting for approval... (checking {poll_num + 1}/{max_polls})[/] "
|
|
811
|
+
f"[dim]Current allowance: {current_allowance} / {amount_base_units}[/]"
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
time.sleep(poll_interval)
|
|
815
|
+
except KeyboardInterrupt:
|
|
816
|
+
status.stop()
|
|
817
|
+
console.print("\n[yellow]Polling interrupted by user.[/]")
|
|
818
|
+
break
|
|
819
|
+
except Exception as e:
|
|
820
|
+
# Network errors - continue polling
|
|
821
|
+
if poll_num < max_polls - 1:
|
|
822
|
+
status.update(
|
|
823
|
+
f"[yellow]Network error, retrying... (check {poll_num + 1}/{max_polls})[/]"
|
|
824
|
+
)
|
|
825
|
+
time.sleep(poll_interval)
|
|
826
|
+
else:
|
|
827
|
+
status.stop()
|
|
828
|
+
console.print(f"\n[yellow]Could not verify approval automatically: {e}[/]")
|
|
829
|
+
break
|
|
830
|
+
except Exception as e:
|
|
831
|
+
console.print(f"[yellow]Warning:[/] Could not set up approval detection: {e}")
|
|
832
|
+
console.print("[dim]Falling back to manual confirmation...[/]")
|
|
833
|
+
|
|
834
|
+
# If approval not detected, ask user
|
|
835
|
+
if not approval_detected:
|
|
836
|
+
if not Confirm.ask(
|
|
837
|
+
"\n[bold yellow]Have you completed the approve transaction?[/] (Type 'yes' to continue to Phase 2)",
|
|
838
|
+
default=False
|
|
839
|
+
):
|
|
840
|
+
console.print("[bold yellow]You can continue with Phase 2 later. The signature will expire in 5 minutes.[/]")
|
|
841
|
+
raise typer.Exit(code=0)
|
|
842
|
+
|
|
843
|
+
# Calculate hotkey bytes32 (keccak256 of SS58 string) - moved before Phase 1 display
|
|
844
|
+
hotkey_bytes = hotkey_ss58.encode("utf-8")
|
|
845
|
+
hotkey_bytes32 = Web3.keccak(hotkey_bytes)
|
|
846
|
+
# Ensure single 0x prefix (hex() doesn't include 0x)
|
|
847
|
+
hotkey_hex = hotkey_bytes32.hex()
|
|
848
|
+
if not hotkey_hex.startswith("0x"):
|
|
849
|
+
hotkey_hex = "0x" + hotkey_hex
|
|
850
|
+
|
|
851
|
+
# Convert pool_id to bytes32 hex if needed
|
|
852
|
+
pool_id_normalized = pool_id.lower().strip()
|
|
853
|
+
if not pool_id_normalized.startswith("0x"):
|
|
854
|
+
pool_id_normalized = "0x" + pool_id_normalized
|
|
855
|
+
if len(pool_id_normalized) == 42:
|
|
856
|
+
# Legacy address format: pad to bytes32
|
|
857
|
+
hex_part = pool_id_normalized[2:]
|
|
858
|
+
padded_hex = "0" * 24 + hex_part
|
|
859
|
+
pool_id_normalized = "0x" + padded_hex
|
|
860
|
+
|
|
861
|
+
# Ensure signature has single 0x prefix
|
|
862
|
+
signature_normalized = signature.strip()
|
|
863
|
+
if signature_normalized.startswith("0x0x"):
|
|
864
|
+
# Remove double 0x prefix
|
|
865
|
+
signature_normalized = signature_normalized[2:]
|
|
866
|
+
elif not signature_normalized.startswith("0x"):
|
|
867
|
+
signature_normalized = "0x" + signature_normalized
|
|
868
|
+
|
|
869
|
+
# Step 11: Display Phase 2: Lock Position
|
|
870
|
+
console.print()
|
|
871
|
+
console.print("[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/]")
|
|
872
|
+
console.print("[bold]Phase 2: Lock Position[/]")
|
|
873
|
+
console.print("[bold cyan]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/]")
|
|
874
|
+
console.print()
|
|
875
|
+
# Verify vault address matches what we expect
|
|
876
|
+
if lock_tx['to'].lower() != vault.lower():
|
|
877
|
+
console.print(
|
|
878
|
+
f"[bold yellow]⚠ Warning:[/] Transaction vault address ({lock_tx['to']}) "
|
|
879
|
+
f"does not match selected vault ({vault})"
|
|
880
|
+
)
|
|
881
|
+
console.print("[bold]Fields for Frontend Lock Page:[/]")
|
|
882
|
+
console.print()
|
|
883
|
+
console.print(f" [yellow]Contract Address (Vault)[/]: {lock_tx['to']}")
|
|
884
|
+
console.print(f" [yellow]poolId_[/] (bytes32): {pool_id_normalized}")
|
|
885
|
+
console.print(f" [yellow]amount[/] (uint256): {amount_base_units}")
|
|
886
|
+
console.print(f" [yellow]lockDays[/] (uint64): {lock_days}")
|
|
887
|
+
console.print(f" [yellow]hotkey[/] (bytes32): {hotkey_hex}")
|
|
888
|
+
console.print(f" [yellow]timestamp[/] (uint256): {timestamp}")
|
|
889
|
+
console.print(f" [yellow]signature[/] (bytes): {signature_normalized}")
|
|
890
|
+
console.print(f" [yellow]owner[/] (address): {owner}")
|
|
891
|
+
console.print()
|
|
892
|
+
|
|
893
|
+
# Open browser with frontend URL for Phase 2
|
|
894
|
+
frontend_url = settings.lock_ui_url
|
|
895
|
+
phase2_params = {
|
|
896
|
+
"phase": "2",
|
|
897
|
+
"chainId": str(chain),
|
|
898
|
+
"vaultAddress": lock_tx['to'],
|
|
899
|
+
"poolId": pool_id_normalized,
|
|
900
|
+
"amount": str(amount_base_units),
|
|
901
|
+
"lockDays": str(lock_days),
|
|
902
|
+
"hotkey": hotkey_hex,
|
|
903
|
+
"timestamp": str(timestamp),
|
|
904
|
+
"signature": signature_normalized,
|
|
905
|
+
"owner": owner,
|
|
906
|
+
}
|
|
907
|
+
phase2_url = f"{frontend_url}?{urlencode(phase2_params)}"
|
|
908
|
+
console.print(f"\n[bold cyan]Opening lock interface for Phase 2...[/]")
|
|
909
|
+
console.print(f"URL: [cyan]{phase2_url}[/]")
|
|
910
|
+
try:
|
|
911
|
+
webbrowser.open(phase2_url)
|
|
912
|
+
except Exception as e:
|
|
913
|
+
console.print(f"[bold yellow]Warning:[/] Could not open browser automatically: {e}")
|
|
914
|
+
console.print(f"Please manually open: [cyan]{phase2_url}[/]")
|
|
915
|
+
console.print()
|
|
916
|
+
console.print(
|
|
917
|
+
"[dim]The CLI will automatically detect when the lock transaction is complete.[/]"
|
|
918
|
+
)
|
|
919
|
+
console.print("[dim]You can also press Ctrl+C to skip and continue manually.[/]")
|
|
920
|
+
|
|
921
|
+
# Step 9: Auto-detect lock transaction and check status
|
|
922
|
+
# Vault ABI for LockCreated event
|
|
923
|
+
vault_abi = [
|
|
924
|
+
{
|
|
925
|
+
"anonymous": False,
|
|
926
|
+
"inputs": [
|
|
927
|
+
{"indexed": True, "internalType": "bytes32", "name": "lockId", "type": "bytes32"},
|
|
928
|
+
{"indexed": True, "internalType": "address", "name": "owner", "type": "address"},
|
|
929
|
+
{"indexed": True, "internalType": "bytes32", "name": "poolId", "type": "bytes32"},
|
|
930
|
+
{"indexed": False, "internalType": "address", "name": "vault", "type": "address"},
|
|
931
|
+
{"indexed": False, "internalType": "uint256", "name": "amount", "type": "uint256"},
|
|
932
|
+
{"indexed": False, "internalType": "uint64", "name": "start", "type": "uint64"},
|
|
933
|
+
{"indexed": False, "internalType": "uint64", "name": "lockDays", "type": "uint64"}
|
|
934
|
+
],
|
|
935
|
+
"name": "LockCreated",
|
|
936
|
+
"type": "event"
|
|
937
|
+
}
|
|
938
|
+
]
|
|
939
|
+
|
|
940
|
+
# Get RPC endpoint for Base Sepolia
|
|
941
|
+
rpc_url = None
|
|
942
|
+
if chain == 84532: # Base Sepolia
|
|
943
|
+
rpc_url = "https://sepolia.base.org"
|
|
944
|
+
else:
|
|
945
|
+
# Only Base Sepolia is supported for auto-detection
|
|
946
|
+
console.print(f"[yellow]Warning:[/] Auto-detection only supports Base Sepolia (chain ID 84532), but chain ID {chain} was specified.")
|
|
947
|
+
console.print("[dim]You'll need to enter the transaction hash manually.[/]")
|
|
948
|
+
rpc_url = None
|
|
949
|
+
|
|
950
|
+
lock_detected = False
|
|
951
|
+
tx_hash_normalized = None
|
|
952
|
+
|
|
953
|
+
if rpc_url:
|
|
954
|
+
try:
|
|
955
|
+
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
956
|
+
vault_contract = w3.eth.contract(
|
|
957
|
+
address=Web3.to_checksum_address(vault),
|
|
958
|
+
abi=vault_abi
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
# Calculate expected pool_id bytes32
|
|
962
|
+
pool_id_bytes = Web3.to_bytes(hexstr=pool_id_normalized)
|
|
963
|
+
|
|
964
|
+
max_polls = 60 # Poll for up to 5 minutes (60 * 5 seconds)
|
|
965
|
+
poll_interval = 5 # Check every 5 seconds
|
|
966
|
+
|
|
967
|
+
with Status(
|
|
968
|
+
"[bold cyan]Waiting for lock transaction...[/]",
|
|
969
|
+
console=console,
|
|
970
|
+
spinner="dots",
|
|
971
|
+
) as status:
|
|
972
|
+
start_block = w3.eth.block_number
|
|
973
|
+
for poll_num in range(max_polls):
|
|
974
|
+
try:
|
|
975
|
+
latest_block = w3.eth.block_number
|
|
976
|
+
# Check last 20 blocks for LockCreated events
|
|
977
|
+
from_block = max(start_block - 1, latest_block - 20)
|
|
978
|
+
|
|
979
|
+
# Filter for LockCreated events matching owner and poolId
|
|
980
|
+
events = vault_contract.events.LockCreated().get_logs(
|
|
981
|
+
fromBlock=from_block,
|
|
982
|
+
toBlock=latest_block,
|
|
983
|
+
argument_filters={
|
|
984
|
+
'owner': Web3.to_checksum_address(owner),
|
|
985
|
+
'poolId': pool_id_bytes
|
|
986
|
+
}
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
if events:
|
|
990
|
+
# Found matching lock event
|
|
991
|
+
event = events[-1] # Get the most recent one
|
|
992
|
+
tx_hash_normalized = event['transactionHash'].hex()
|
|
993
|
+
lock_detected = True
|
|
994
|
+
status.stop()
|
|
995
|
+
console.print("\n[bold green]✓ Lock transaction detected![/]")
|
|
996
|
+
console.print(f"[dim]Transaction hash: {tx_hash_normalized}[/]")
|
|
997
|
+
break
|
|
998
|
+
|
|
999
|
+
# Update status message
|
|
1000
|
+
status.update(
|
|
1001
|
+
f"[bold cyan]Waiting for lock transaction... (checking {poll_num + 1}/{max_polls})[/]"
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
time.sleep(poll_interval)
|
|
1005
|
+
except KeyboardInterrupt:
|
|
1006
|
+
status.stop()
|
|
1007
|
+
console.print("\n[yellow]Polling interrupted by user.[/]")
|
|
1008
|
+
break
|
|
1009
|
+
except Exception as e:
|
|
1010
|
+
# Network errors - continue polling
|
|
1011
|
+
if poll_num < max_polls - 1:
|
|
1012
|
+
status.update(
|
|
1013
|
+
f"[yellow]Network error, retrying... (check {poll_num + 1}/{max_polls})[/]"
|
|
1014
|
+
)
|
|
1015
|
+
time.sleep(poll_interval)
|
|
1016
|
+
else:
|
|
1017
|
+
status.stop()
|
|
1018
|
+
console.print(f"\n[yellow]Could not detect lock transaction automatically: {e}[/]")
|
|
1019
|
+
break
|
|
1020
|
+
except Exception as e:
|
|
1021
|
+
console.print(f"[yellow]Warning:[/] Could not set up lock detection: {e}")
|
|
1022
|
+
console.print("[dim]Falling back to manual transaction hash entry...[/]")
|
|
1023
|
+
|
|
1024
|
+
# If lock not detected automatically, prompt for transaction hash
|
|
1025
|
+
if not lock_detected:
|
|
1026
|
+
console.print()
|
|
1027
|
+
while True:
|
|
1028
|
+
tx_hash = typer.prompt(
|
|
1029
|
+
"[bold cyan]Transaction hash (0x...)[/] (Press Enter to skip)",
|
|
1030
|
+
show_default=False,
|
|
1031
|
+
default=""
|
|
1032
|
+
)
|
|
1033
|
+
if not tx_hash.strip():
|
|
1034
|
+
# User skipped - show instructions
|
|
1035
|
+
console.print()
|
|
1036
|
+
console.print("[bold green]✓ Lock flow complete![/]")
|
|
1037
|
+
console.print()
|
|
1038
|
+
console.print(
|
|
1039
|
+
"[dim]The verifier will automatically detect the lock after you execute the transaction.[/]"
|
|
1040
|
+
)
|
|
1041
|
+
console.print(
|
|
1042
|
+
f"[dim]Check your lock status with: [bold]cartha miner status --wallet-name {coldkey} --wallet-hotkey {hotkey}[/][/]"
|
|
1043
|
+
)
|
|
1044
|
+
console.print(
|
|
1045
|
+
f"[dim]Or visit the frontend: [bold]{frontend_url}/manage[/][/]"
|
|
1046
|
+
)
|
|
1047
|
+
console.print()
|
|
1048
|
+
tx_hash_normalized = None
|
|
1049
|
+
break
|
|
1050
|
+
|
|
1051
|
+
tx_hash_normalized = normalize_hex(tx_hash)
|
|
1052
|
+
if len(tx_hash_normalized) == 66:
|
|
1053
|
+
break
|
|
1054
|
+
console.print(
|
|
1055
|
+
"[bold red]Error:[/] Transaction hash must be 32 bytes (0x + 64 hex characters)"
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# If we have a transaction hash (either auto-detected or manually entered), check status
|
|
1059
|
+
if tx_hash_normalized:
|
|
1060
|
+
console.print()
|
|
1061
|
+
console.print("[dim]Verification can take up to 1 minute. Please wait...[/]")
|
|
1062
|
+
console.print()
|
|
1063
|
+
|
|
1064
|
+
max_status_polls = 8 # Poll for up to 40 seconds (8 * 5 seconds)
|
|
1065
|
+
status_poll_interval = 5 # Check every 5 seconds
|
|
1066
|
+
verified = False
|
|
1067
|
+
status_result = None
|
|
1068
|
+
|
|
1069
|
+
with Status(
|
|
1070
|
+
"[bold cyan]Waiting for verifier to process lock...[/]",
|
|
1071
|
+
console=console,
|
|
1072
|
+
spinner="dots",
|
|
1073
|
+
) as status:
|
|
1074
|
+
for poll_num in range(max_status_polls):
|
|
1075
|
+
try:
|
|
1076
|
+
status.update(
|
|
1077
|
+
f"[bold cyan]Checking lock status... ({poll_num + 1}/{max_status_polls})[/] "
|
|
1078
|
+
f"[dim](This can take up to 1 minute)[/]"
|
|
1079
|
+
)
|
|
1080
|
+
status_result = get_lock_status(tx_hash=tx_hash_normalized)
|
|
1081
|
+
|
|
1082
|
+
if status_result.get("verified"):
|
|
1083
|
+
verified = True
|
|
1084
|
+
status.stop()
|
|
1085
|
+
break
|
|
1086
|
+
|
|
1087
|
+
# If not verified yet, continue polling
|
|
1088
|
+
if poll_num < max_status_polls - 1:
|
|
1089
|
+
time.sleep(status_poll_interval)
|
|
1090
|
+
except KeyboardInterrupt:
|
|
1091
|
+
status.stop()
|
|
1092
|
+
console.print("\n[yellow]Status check interrupted.[/]")
|
|
1093
|
+
break
|
|
1094
|
+
except Exception as exc:
|
|
1095
|
+
# Network errors - continue polling
|
|
1096
|
+
if poll_num < max_status_polls - 1:
|
|
1097
|
+
status.update(
|
|
1098
|
+
f"[yellow]Network error, retrying... ({poll_num + 1}/{max_status_polls})[/]"
|
|
1099
|
+
)
|
|
1100
|
+
time.sleep(status_poll_interval)
|
|
1101
|
+
else:
|
|
1102
|
+
status.stop()
|
|
1103
|
+
console.print(f"\n[yellow]Could not check lock status: {exc}[/]")
|
|
1104
|
+
break
|
|
1105
|
+
|
|
1106
|
+
if verified and status_result:
|
|
1107
|
+
# Position is processed - show success message
|
|
1108
|
+
console.print("\n[bold green]✓ Lock successful![/]")
|
|
1109
|
+
console.print()
|
|
1110
|
+
console.print(
|
|
1111
|
+
f"[bold cyan]Lock ID:[/] {status_result.get('lockId', 'N/A')}"
|
|
1112
|
+
)
|
|
1113
|
+
console.print(
|
|
1114
|
+
f"[bold cyan]Added to epoch:[/] {status_result.get('addedToEpoch', 'N/A')}"
|
|
1115
|
+
)
|
|
1116
|
+
console.print()
|
|
1117
|
+
console.print("[bold]Check your lock status:[/]")
|
|
1118
|
+
console.print(
|
|
1119
|
+
f" • CLI: [bold]cartha miner status --wallet-name {coldkey} --wallet-hotkey {hotkey}[/]"
|
|
1120
|
+
)
|
|
1121
|
+
console.print(
|
|
1122
|
+
f" • Frontend: [bold]{frontend_url}/manage[/]"
|
|
1123
|
+
)
|
|
1124
|
+
console.print()
|
|
1125
|
+
else:
|
|
1126
|
+
# Position not yet processed after polling - ask if user wants to manually trigger
|
|
1127
|
+
message = status_result.get("message", "") if status_result else ""
|
|
1128
|
+
console.print("\n[yellow]Position not yet processed by verifier after waiting.[/]")
|
|
1129
|
+
if message:
|
|
1130
|
+
console.print(f"[dim]{message}[/]")
|
|
1131
|
+
console.print()
|
|
1132
|
+
|
|
1133
|
+
if Confirm.ask(
|
|
1134
|
+
"[bold cyan]Would you like to manually trigger processing?[/] (This will nudge the verifier to check)",
|
|
1135
|
+
default=False,
|
|
1136
|
+
):
|
|
1137
|
+
try:
|
|
1138
|
+
with Status(
|
|
1139
|
+
"[bold cyan]Triggering verifier processing...[/]",
|
|
1140
|
+
console=console,
|
|
1141
|
+
spinner="dots",
|
|
1142
|
+
) as process_status:
|
|
1143
|
+
process_result = process_lock_transaction(tx_hash=tx_hash_normalized)
|
|
1144
|
+
process_status.stop()
|
|
1145
|
+
|
|
1146
|
+
if process_result.get("success"):
|
|
1147
|
+
# Give database a moment to commit
|
|
1148
|
+
time.sleep(1.5)
|
|
1149
|
+
|
|
1150
|
+
# Check status again
|
|
1151
|
+
verified = False
|
|
1152
|
+
status_result = None
|
|
1153
|
+
for retry in range(3):
|
|
1154
|
+
try:
|
|
1155
|
+
status_result = get_lock_status(tx_hash=tx_hash_normalized)
|
|
1156
|
+
if status_result.get("verified"):
|
|
1157
|
+
verified = True
|
|
1158
|
+
break
|
|
1159
|
+
elif retry < 2:
|
|
1160
|
+
time.sleep(0.5)
|
|
1161
|
+
except Exception:
|
|
1162
|
+
if retry < 2:
|
|
1163
|
+
time.sleep(0.5)
|
|
1164
|
+
continue
|
|
1165
|
+
|
|
1166
|
+
if verified and status_result:
|
|
1167
|
+
console.print("\n[bold green]✓ Lock successful![/]")
|
|
1168
|
+
console.print()
|
|
1169
|
+
console.print(
|
|
1170
|
+
f"[bold cyan]Lock ID:[/] {status_result.get('lockId', 'N/A')}"
|
|
1171
|
+
)
|
|
1172
|
+
console.print(
|
|
1173
|
+
f"[bold cyan]Added to epoch:[/] {status_result.get('addedToEpoch', 'N/A')}"
|
|
1174
|
+
)
|
|
1175
|
+
console.print()
|
|
1176
|
+
console.print("[bold]Check your lock status:[/]")
|
|
1177
|
+
console.print(
|
|
1178
|
+
f" • CLI: [bold]cartha miner status --wallet-name {coldkey} --wallet-hotkey {hotkey}[/]"
|
|
1179
|
+
)
|
|
1180
|
+
console.print(
|
|
1181
|
+
f" • Frontend: [bold]{frontend_url}/manage[/]"
|
|
1182
|
+
)
|
|
1183
|
+
console.print()
|
|
1184
|
+
else:
|
|
1185
|
+
console.print("\n[yellow]Processing triggered but not yet verified.[/]")
|
|
1186
|
+
console.print(
|
|
1187
|
+
"[dim]The verifier will process it automatically. Check status later.[/]"
|
|
1188
|
+
)
|
|
1189
|
+
console.print()
|
|
1190
|
+
console.print("[bold]Check your lock status:[/]")
|
|
1191
|
+
console.print(
|
|
1192
|
+
f" • CLI: [bold]cartha miner status --wallet-name {coldkey} --wallet-hotkey {hotkey}[/]"
|
|
1193
|
+
)
|
|
1194
|
+
console.print(
|
|
1195
|
+
f" • Frontend: [bold]{frontend_url}/manage[/]"
|
|
1196
|
+
)
|
|
1197
|
+
console.print()
|
|
1198
|
+
else:
|
|
1199
|
+
console.print("\n[yellow]Could not trigger processing.[/]")
|
|
1200
|
+
console.print(
|
|
1201
|
+
"[dim]The verifier will process it automatically. Check status later.[/]"
|
|
1202
|
+
)
|
|
1203
|
+
console.print()
|
|
1204
|
+
console.print("[bold]Check your lock status:[/]")
|
|
1205
|
+
console.print(
|
|
1206
|
+
f" • CLI: [bold]cartha miner status --wallet-name {coldkey} --wallet-hotkey {hotkey}[/]"
|
|
1207
|
+
)
|
|
1208
|
+
console.print(
|
|
1209
|
+
f" • Frontend: [bold]{frontend_url}/manage[/]"
|
|
1210
|
+
)
|
|
1211
|
+
console.print()
|
|
1212
|
+
except VerifierError as process_exc:
|
|
1213
|
+
console.print(f"\n[yellow]Error triggering processing: {process_exc}[/]")
|
|
1214
|
+
console.print(
|
|
1215
|
+
"[dim]The verifier will process it automatically. Check status later.[/]"
|
|
1216
|
+
)
|
|
1217
|
+
console.print()
|
|
1218
|
+
console.print("[bold]Check your lock status:[/]")
|
|
1219
|
+
console.print(
|
|
1220
|
+
f" • CLI: [bold]cartha miner status --wallet-name {coldkey} --wallet-hotkey {hotkey}[/]"
|
|
1221
|
+
)
|
|
1222
|
+
console.print(
|
|
1223
|
+
f" • Frontend: [bold]{frontend_url}/manage[/]"
|
|
1224
|
+
)
|
|
1225
|
+
console.print()
|
|
1226
|
+
except Exception as process_exc:
|
|
1227
|
+
console.print(f"\n[yellow]Error: {process_exc}[/]")
|
|
1228
|
+
console.print(
|
|
1229
|
+
"[dim]The verifier will process it automatically. Check status later.[/]"
|
|
1230
|
+
)
|
|
1231
|
+
console.print()
|
|
1232
|
+
console.print("[bold]Check your lock status:[/]")
|
|
1233
|
+
console.print(
|
|
1234
|
+
f" • CLI: [bold]cartha miner status --wallet-name {coldkey} --wallet-hotkey {hotkey}[/]"
|
|
1235
|
+
)
|
|
1236
|
+
console.print(
|
|
1237
|
+
f" • Frontend: [bold]{frontend_url}/manage[/]"
|
|
1238
|
+
)
|
|
1239
|
+
console.print()
|
|
1240
|
+
else:
|
|
1241
|
+
# User declined manual processing
|
|
1242
|
+
console.print()
|
|
1243
|
+
console.print("[bold]Check your lock status:[/]")
|
|
1244
|
+
console.print(
|
|
1245
|
+
f" • CLI: [bold]cartha miner status --wallet-name {coldkey} --wallet-hotkey {hotkey}[/]"
|
|
1246
|
+
)
|
|
1247
|
+
console.print(
|
|
1248
|
+
f" • Frontend: [bold]{frontend_url}/manage[/]"
|
|
1249
|
+
)
|
|
1250
|
+
console.print()
|
|
1251
|
+
console.print("[dim]If the verifier doesn't detect your position automatically, you can manually trigger processing later:[/]")
|
|
1252
|
+
console.print(
|
|
1253
|
+
f" [bold]cartha miner status --wallet-name {coldkey} --wallet-hotkey {hotkey} --refresh --tx-hash {tx_hash_normalized}[/]"
|
|
1254
|
+
)
|
|
1255
|
+
console.print()
|
|
1256
|
+
|
|
1257
|
+
except typer.Exit:
|
|
1258
|
+
raise
|
|
1259
|
+
except Exception as exc:
|
|
1260
|
+
handle_unexpected_exception("Lock creation failed", exc)
|