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
cartha_cli/verifier.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""HTTP helpers for interacting with the Cartha verifier service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import requests # type: ignore[import-untyped]
|
|
9
|
+
|
|
10
|
+
from .config import settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VerifierError(RuntimeError):
|
|
14
|
+
"""Raised when the verifier cannot be reached or returns an error."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _build_url(path: str) -> str:
|
|
22
|
+
base = settings.verifier_url.rstrip("/")
|
|
23
|
+
return f"{base}{path}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _request(
|
|
27
|
+
method: str,
|
|
28
|
+
path: str,
|
|
29
|
+
*,
|
|
30
|
+
params: dict[str, Any] | None = None,
|
|
31
|
+
json_data: dict[str, Any] | None = None,
|
|
32
|
+
headers: dict[str, str] | None = None,
|
|
33
|
+
retry: bool = True,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Make HTTP request with automatic retry logic.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
method: HTTP method (GET, POST, etc.)
|
|
39
|
+
path: API path
|
|
40
|
+
params: Query parameters
|
|
41
|
+
json_data: JSON body for POST requests
|
|
42
|
+
headers: Additional headers
|
|
43
|
+
retry: Whether to retry on transient failures (default: True)
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Response JSON data as dict
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
VerifierError: If request fails after retries
|
|
50
|
+
"""
|
|
51
|
+
request_headers: dict[str, str] = {"Accept": "application/json"}
|
|
52
|
+
if headers:
|
|
53
|
+
request_headers.update(headers)
|
|
54
|
+
url = _build_url(path)
|
|
55
|
+
|
|
56
|
+
max_attempts = settings.retry_max_attempts if retry else 1
|
|
57
|
+
backoff_factor = settings.retry_backoff_factor
|
|
58
|
+
retry_statuses = settings.retry_on_status
|
|
59
|
+
|
|
60
|
+
last_exception: Exception | None = None
|
|
61
|
+
response: requests.Response | None = None
|
|
62
|
+
|
|
63
|
+
for attempt in range(max_attempts):
|
|
64
|
+
try:
|
|
65
|
+
# Use separate connection and read timeouts
|
|
66
|
+
# Connection timeout: 5s (fast fail if can't connect)
|
|
67
|
+
# Read timeout: 60s (allow time for response to arrive after connection established)
|
|
68
|
+
# This helps when verifier processes quickly but response transmission is slow
|
|
69
|
+
response = requests.request(
|
|
70
|
+
method,
|
|
71
|
+
url,
|
|
72
|
+
params=params,
|
|
73
|
+
json=json_data,
|
|
74
|
+
headers=request_headers,
|
|
75
|
+
timeout=(5, 60), # (connect_timeout, read_timeout) in seconds
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Check if we should retry based on status code
|
|
79
|
+
if retry and attempt < max_attempts - 1 and response.status_code in retry_statuses:
|
|
80
|
+
wait_time = backoff_factor ** attempt
|
|
81
|
+
time.sleep(wait_time)
|
|
82
|
+
continue # Retry the request
|
|
83
|
+
|
|
84
|
+
# If we get here, either success or non-retryable error - break and process
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
except requests.Timeout as exc:
|
|
88
|
+
last_exception = exc
|
|
89
|
+
# Retry timeouts if we have attempts left
|
|
90
|
+
if retry and attempt < max_attempts - 1:
|
|
91
|
+
wait_time = backoff_factor ** attempt
|
|
92
|
+
time.sleep(wait_time)
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Explicitly handle timeout - request took longer than allowed
|
|
96
|
+
# This could be connection timeout (5s) or read timeout (60s)
|
|
97
|
+
error_msg = (
|
|
98
|
+
f"Request to verifier timed out after {max_attempts} attempt(s): {url}\n"
|
|
99
|
+
"This is a CLI-side timeout.\n"
|
|
100
|
+
"If verifier logs show request completed, this is likely slow network response transmission.\n"
|
|
101
|
+
"Possible causes: network latency, large response size, or slow connection.\n"
|
|
102
|
+
"Tip: Try again in a moment or check verifier logs to confirm request was processed."
|
|
103
|
+
)
|
|
104
|
+
raise VerifierError(error_msg) from exc
|
|
105
|
+
except requests.RequestException as exc: # pragma: no cover - network failure
|
|
106
|
+
last_exception = exc
|
|
107
|
+
# Retry network errors if we have attempts left
|
|
108
|
+
if retry and attempt < max_attempts - 1:
|
|
109
|
+
wait_time = backoff_factor ** attempt
|
|
110
|
+
time.sleep(wait_time)
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
# Provide more context about the failed URL
|
|
114
|
+
error_msg = f"Failed to reach verifier at {url} after {max_attempts} attempt(s): {exc}"
|
|
115
|
+
raise VerifierError(error_msg) from exc
|
|
116
|
+
|
|
117
|
+
# If we exhausted retries without getting a response, raise the last exception
|
|
118
|
+
if response is None and last_exception:
|
|
119
|
+
raise VerifierError(f"Request failed after {max_attempts} attempts: {last_exception}") from last_exception
|
|
120
|
+
|
|
121
|
+
# Process the response (response should be set at this point)
|
|
122
|
+
assert response is not None # nosec - response should be set if we got here
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
data = response.json()
|
|
126
|
+
except ValueError:
|
|
127
|
+
data = None
|
|
128
|
+
|
|
129
|
+
if response.status_code >= 400:
|
|
130
|
+
if isinstance(data, dict):
|
|
131
|
+
detail = data.get("detail") or data.get("error") or response.text
|
|
132
|
+
else:
|
|
133
|
+
detail = response.text or "Unknown verifier error"
|
|
134
|
+
|
|
135
|
+
# Handle FastAPI validation errors which return detail as a list
|
|
136
|
+
if isinstance(detail, list):
|
|
137
|
+
# Format list of validation errors into a readable string
|
|
138
|
+
formatted_errors = []
|
|
139
|
+
for item in detail:
|
|
140
|
+
if isinstance(item, dict):
|
|
141
|
+
# Extract field location and message
|
|
142
|
+
loc = item.get("loc", [])
|
|
143
|
+
msg = item.get("msg", "Validation error")
|
|
144
|
+
field = " -> ".join(str(x) for x in loc) if loc else "unknown"
|
|
145
|
+
formatted_errors.append(f"{field}: {msg}")
|
|
146
|
+
else:
|
|
147
|
+
formatted_errors.append(str(item))
|
|
148
|
+
detail = "; ".join(formatted_errors)
|
|
149
|
+
elif isinstance(detail, str):
|
|
150
|
+
detail = detail.strip()
|
|
151
|
+
else:
|
|
152
|
+
detail = str(detail)
|
|
153
|
+
|
|
154
|
+
# Log error details for debugging (only in debug mode or for 500 errors)
|
|
155
|
+
if response.status_code >= 500:
|
|
156
|
+
import logging
|
|
157
|
+
|
|
158
|
+
logger = logging.getLogger(__name__)
|
|
159
|
+
logger.debug(f"Verifier error - URL: {url}")
|
|
160
|
+
logger.debug(f"Verifier error - Status: {response.status_code}")
|
|
161
|
+
logger.debug(f"Verifier error - Response: {response.text[:500]}")
|
|
162
|
+
|
|
163
|
+
raise VerifierError(detail, status_code=response.status_code)
|
|
164
|
+
|
|
165
|
+
if not isinstance(data, dict):
|
|
166
|
+
raise VerifierError("Unexpected verifier response payload.")
|
|
167
|
+
return data
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def fetch_miner_status(
|
|
171
|
+
*,
|
|
172
|
+
hotkey: str,
|
|
173
|
+
slot: str,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""Return miner status without authentication (public endpoint)."""
|
|
176
|
+
return _request(
|
|
177
|
+
"GET",
|
|
178
|
+
"/v1/miner/status",
|
|
179
|
+
params={"hotkey": hotkey, "slot": slot},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def fetch_pair_status(
|
|
184
|
+
*,
|
|
185
|
+
hotkey: str,
|
|
186
|
+
slot: str,
|
|
187
|
+
network: str,
|
|
188
|
+
netuid: int,
|
|
189
|
+
message: str,
|
|
190
|
+
signature: str,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
"""Return the status for a (hotkey, slotUID) pair after verifying ownership."""
|
|
193
|
+
payload = {
|
|
194
|
+
"hotkey": hotkey,
|
|
195
|
+
"slot": slot,
|
|
196
|
+
"network": network,
|
|
197
|
+
"netuid": netuid,
|
|
198
|
+
"message": message,
|
|
199
|
+
"signature": signature,
|
|
200
|
+
}
|
|
201
|
+
return _request(
|
|
202
|
+
"POST",
|
|
203
|
+
"/v1/pair/status",
|
|
204
|
+
json_data=payload,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# REMOVED: fetch_pair_password and register_pair_password
|
|
209
|
+
# These functions are no longer needed - the new lock flow uses session tokens instead of passwords.
|
|
210
|
+
# The verifier endpoints /v1/pair/password/* have been removed.
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def check_registration(
|
|
214
|
+
*,
|
|
215
|
+
hotkey: str,
|
|
216
|
+
miner_slot: str | None = None,
|
|
217
|
+
uid: str | None = None,
|
|
218
|
+
) -> dict[str, Any]:
|
|
219
|
+
"""Check if a hotkey is registered on subnet 35.
|
|
220
|
+
|
|
221
|
+
Returns: {registered: bool, uid: int | None}
|
|
222
|
+
"""
|
|
223
|
+
params: dict[str, Any] = {"hotkey": hotkey}
|
|
224
|
+
if miner_slot is not None:
|
|
225
|
+
params["minerSlot"] = miner_slot
|
|
226
|
+
if uid is not None:
|
|
227
|
+
params["uid"] = uid
|
|
228
|
+
return _request(
|
|
229
|
+
"GET",
|
|
230
|
+
"/subnet/check-registration",
|
|
231
|
+
params=params,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def verify_hotkey(
|
|
236
|
+
*,
|
|
237
|
+
hotkey: str,
|
|
238
|
+
signature: str,
|
|
239
|
+
message: str,
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
"""Verify Bittensor hotkey signature and get session token.
|
|
242
|
+
|
|
243
|
+
Returns: {verified: bool, session_token: str, expires_at: int}
|
|
244
|
+
"""
|
|
245
|
+
payload = {
|
|
246
|
+
"hotkey": hotkey,
|
|
247
|
+
"signature": signature,
|
|
248
|
+
"message": message,
|
|
249
|
+
}
|
|
250
|
+
return _request(
|
|
251
|
+
"POST",
|
|
252
|
+
"/auth/verify-hotkey",
|
|
253
|
+
json_data=payload,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def request_lock_signature(
|
|
258
|
+
*,
|
|
259
|
+
session_token: str,
|
|
260
|
+
pool_id: str,
|
|
261
|
+
amount: int,
|
|
262
|
+
lock_days: int,
|
|
263
|
+
hotkey: str,
|
|
264
|
+
miner_slot: str | None,
|
|
265
|
+
uid: str | None,
|
|
266
|
+
owner: str,
|
|
267
|
+
chain_id: int,
|
|
268
|
+
vault_address: str,
|
|
269
|
+
) -> dict[str, Any]:
|
|
270
|
+
"""Request EIP-712 LockRequest signature from verifier.
|
|
271
|
+
|
|
272
|
+
Returns: {signature, timestamp, nonce, expiresAt, approveTx, lockTx}
|
|
273
|
+
"""
|
|
274
|
+
payload = {
|
|
275
|
+
"poolId": pool_id,
|
|
276
|
+
"amount": amount,
|
|
277
|
+
"lockDays": lock_days,
|
|
278
|
+
"hotkey": hotkey,
|
|
279
|
+
"owner": owner,
|
|
280
|
+
"chainId": chain_id,
|
|
281
|
+
"vaultAddress": vault_address,
|
|
282
|
+
}
|
|
283
|
+
if miner_slot is not None:
|
|
284
|
+
payload["minerSlot"] = miner_slot
|
|
285
|
+
if uid is not None:
|
|
286
|
+
payload["uid"] = uid
|
|
287
|
+
|
|
288
|
+
headers = {"Authorization": f"Bearer {session_token}"}
|
|
289
|
+
return _request(
|
|
290
|
+
"POST",
|
|
291
|
+
"/lock/request-signature",
|
|
292
|
+
json_data=payload,
|
|
293
|
+
headers=headers,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_lock_status(
|
|
298
|
+
*,
|
|
299
|
+
tx_hash: str,
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
"""Check status of a lock transaction.
|
|
302
|
+
|
|
303
|
+
Returns: {verified: bool, lockId: str | None, addedToEpoch: str | None, message: str | None}
|
|
304
|
+
"""
|
|
305
|
+
return _request(
|
|
306
|
+
"GET",
|
|
307
|
+
"/lock/status",
|
|
308
|
+
params={"tx_hash": tx_hash}, # Match endpoint parameter name (snake_case)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def process_lock_transaction(
|
|
313
|
+
*,
|
|
314
|
+
tx_hash: str,
|
|
315
|
+
) -> dict[str, Any]:
|
|
316
|
+
"""Trigger immediate processing of a lock transaction.
|
|
317
|
+
|
|
318
|
+
Returns: {success: bool, action: str, hotkey: str, slot: str, ...}
|
|
319
|
+
"""
|
|
320
|
+
return _request(
|
|
321
|
+
"POST",
|
|
322
|
+
"/lock/process",
|
|
323
|
+
json_data={"tx_hash": tx_hash},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# REMOVED: Old endpoints - replaced by new lock flow
|
|
328
|
+
# fetch_pair_password, register_pair_password, submit_lock_proof removed
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
__all__ = [
|
|
332
|
+
"VerifierError",
|
|
333
|
+
"_build_url",
|
|
334
|
+
"_request",
|
|
335
|
+
"fetch_pair_status",
|
|
336
|
+
"fetch_miner_status",
|
|
337
|
+
"check_registration",
|
|
338
|
+
"verify_hotkey",
|
|
339
|
+
"request_lock_signature",
|
|
340
|
+
"get_lock_status",
|
|
341
|
+
"process_lock_transaction",
|
|
342
|
+
]
|
cartha_cli/wallet.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Wallet handling utilities for the Cartha CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import bittensor as bt
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from .bt import get_wallet
|
|
10
|
+
from .utils import format_timestamp
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
CHALLENGE_PREFIX = "cartha-pair-auth"
|
|
15
|
+
CHALLENGE_TTL_SECONDS = 120
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_wallet(
|
|
19
|
+
wallet_name: str, wallet_hotkey: str, expected_hotkey: str | None = None
|
|
20
|
+
) -> bt.wallet:
|
|
21
|
+
"""Load a Bittensor wallet.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
wallet_name: Coldkey wallet name
|
|
25
|
+
wallet_hotkey: Hotkey name
|
|
26
|
+
expected_hotkey: Optional expected hotkey SS58 address to validate
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Loaded wallet object
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
typer.Exit: If wallet cannot be loaded or hotkey mismatch
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
wallet = get_wallet(wallet_name, wallet_hotkey)
|
|
36
|
+
except bt.KeyFileError as exc:
|
|
37
|
+
detail = str(exc).strip()
|
|
38
|
+
name = wallet_name or "<unknown>"
|
|
39
|
+
hotkey = wallet_hotkey or "<unknown>"
|
|
40
|
+
message = (
|
|
41
|
+
f"Unable to open coldkey '{name}' hotkey '{hotkey}'. "
|
|
42
|
+
"Ensure the wallet exists, hotkey files are present, and the key is unlocked."
|
|
43
|
+
)
|
|
44
|
+
if detail:
|
|
45
|
+
message += f" ({detail})"
|
|
46
|
+
console.print(f"[bold red]{message}[/]")
|
|
47
|
+
raise typer.Exit(code=1) from exc
|
|
48
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
49
|
+
console.print(f"[bold red]Failed to load wallet '{wallet_name}/{wallet_hotkey}': {exc}[/]")
|
|
50
|
+
raise typer.Exit(code=1) from exc
|
|
51
|
+
|
|
52
|
+
if expected_hotkey and wallet.hotkey.ss58_address != expected_hotkey:
|
|
53
|
+
console.print(
|
|
54
|
+
"[bold red]Hotkey mismatch: loaded wallet hotkey does not match the supplied address.[/]"
|
|
55
|
+
)
|
|
56
|
+
raise typer.Exit(code=1)
|
|
57
|
+
|
|
58
|
+
return wallet
|
|
59
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cartha-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI utilities for Cartha subnet miners.
|
|
5
|
+
Project-URL: Homepage, https://cartha.finance
|
|
6
|
+
Project-URL: Repository, https://github.com/General-Tao-Ventures/cartha-cli
|
|
7
|
+
Project-URL: Documentation, https://github.com/General-Tao-Ventures/cartha-cli#readme
|
|
8
|
+
Author: Cartha Contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: 0xmarkets,bittensor,cartha,cli,mining,subnet
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: <3.12,>=3.11
|
|
21
|
+
Requires-Dist: bittensor<10,>=9.12.2
|
|
22
|
+
Requires-Dist: eth-account<0.11,>=0.10
|
|
23
|
+
Requires-Dist: pydantic-settings<3,>=2.6
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0
|
|
26
|
+
Requires-Dist: requests>=2.32
|
|
27
|
+
Requires-Dist: torch<3,>=2.2
|
|
28
|
+
Requires-Dist: typer[all]>=0.12
|
|
29
|
+
Requires-Dist: web3<7,>=6.11
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Cartha CLI
|
|
35
|
+
|
|
36
|
+
**The official command-line tool for Cartha subnet miners.** Cartha is the Liquidity Provider for 0xMarkets DEX. A simple, powerful way to manage your mining operations—from registration to tracking your locked funds.
|
|
37
|
+
|
|
38
|
+
## Why Cartha CLI?
|
|
39
|
+
|
|
40
|
+
Cartha CLI makes mining on the Cartha subnet effortless. As the Liquidity Provider for 0xMarkets DEX, Cartha enables miners to provide liquidity and earn rewards:
|
|
41
|
+
|
|
42
|
+
- **🔐 One-Click Registration** - Get started mining in minutes
|
|
43
|
+
- **📊 Instant Status Updates** - See all your pools, balances, and expiration dates at a glance
|
|
44
|
+
- **⏰ Smart Expiration Warnings** - Never miss a renewal with color-coded countdowns
|
|
45
|
+
- **💼 Multi-Pool Management** - Track multiple trading pairs in one place
|
|
46
|
+
- **🔑 Secure by Default** - Your password stays hidden until you actually need it
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Install dependencies
|
|
52
|
+
uv sync
|
|
53
|
+
|
|
54
|
+
# Show available commands
|
|
55
|
+
uv run cartha
|
|
56
|
+
|
|
57
|
+
# Get started with registration
|
|
58
|
+
uv run cartha miner register --help
|
|
59
|
+
|
|
60
|
+
# Check your miner status (no authentication needed)
|
|
61
|
+
uv run cartha miner status --help
|
|
62
|
+
|
|
63
|
+
# Check CLI health and connectivity
|
|
64
|
+
uv run cartha health
|
|
65
|
+
|
|
66
|
+
# Or use short aliases
|
|
67
|
+
uv run cartha m status
|
|
68
|
+
uv run cartha v lock
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
- Python 3.11
|
|
74
|
+
- Bittensor wallet
|
|
75
|
+
- [`uv`](https://github.com/astral-sh/uv) package manager (or pip)
|
|
76
|
+
|
|
77
|
+
## What You Can Do
|
|
78
|
+
|
|
79
|
+
### Get Started
|
|
80
|
+
|
|
81
|
+
**Register your miner:**
|
|
82
|
+
```bash
|
|
83
|
+
cartha miner register --wallet-name your-wallet --wallet-hotkey your-hotkey
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Check your status anytime:**
|
|
87
|
+
```bash
|
|
88
|
+
cartha miner status --wallet-name your-wallet --wallet-hotkey your-hotkey
|
|
89
|
+
# Or use the short alias: cartha m status
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Track Your Pools
|
|
93
|
+
|
|
94
|
+
See all your active trading pairs, balances, and when they expire—all in one command. The CLI shows you:
|
|
95
|
+
- Which pools are active and earning rewards
|
|
96
|
+
- How much you have locked in each pool
|
|
97
|
+
- Days remaining before expiration (with helpful warnings)
|
|
98
|
+
- Which pools are included in the next reward epoch
|
|
99
|
+
|
|
100
|
+
### View Available Pools
|
|
101
|
+
|
|
102
|
+
See all available pools with their pool IDs and vault addresses:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
cartha vault pools
|
|
106
|
+
# Or use: cartha v pools
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This shows you which pools are available, their full pool IDs, vault contract addresses, and chain IDs.
|
|
110
|
+
|
|
111
|
+
### Lock Your Funds
|
|
112
|
+
|
|
113
|
+
Create a new lock position with the streamlined lock flow:
|
|
114
|
+
```bash
|
|
115
|
+
cartha vault lock \
|
|
116
|
+
--coldkey your-wallet \
|
|
117
|
+
--hotkey your-hotkey \
|
|
118
|
+
--pool-id "BTCUSD" \
|
|
119
|
+
--amount 1000.0 \
|
|
120
|
+
--lock-days 30 \
|
|
121
|
+
--owner-evm 0xYourEVMAddress \
|
|
122
|
+
--chain 8453 \
|
|
123
|
+
--vault-address 0xVaultAddress
|
|
124
|
+
# Or use: cartha v lock
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Parameter Notes:**
|
|
128
|
+
- `--owner` and `--owner-evm` are interchangeable (EVM address that will own the lock)
|
|
129
|
+
- `--vault` and `--vault-address` are interchangeable (vault contract address)
|
|
130
|
+
- `--network` accepts `test` (netuid 78) or `finney` (netuid 35, default)
|
|
131
|
+
- `--chain` or `--chain-id` are interchangeable (EVM chain ID: 84532 for Base Sepolia testnet)
|
|
132
|
+
|
|
133
|
+
The CLI will:
|
|
134
|
+
1. Check your registration on the specified network (subnet 35 for finney, subnet 78 for test)
|
|
135
|
+
2. Authenticate with your Bittensor hotkey
|
|
136
|
+
3. Request a signed LockRequest from the verifier
|
|
137
|
+
4. Automatically open the Cartha Lock UI in your browser with all parameters pre-filled
|
|
138
|
+
5. Guide you through Phase 1 (Approve USDC) and Phase 2 (Lock Position) via the web interface
|
|
139
|
+
6. Automatically detect when approval completes and proceed to Phase 2
|
|
140
|
+
7. The verifier automatically detects your lock and adds you to the upcoming epoch
|
|
141
|
+
|
|
142
|
+
**Managing Positions**: Visit https://cartha.finance/manage to view all your positions, extend locks, or top up existing positions.
|
|
143
|
+
|
|
144
|
+
### View Your Password
|
|
145
|
+
|
|
146
|
+
When you need your password (like for signing transactions):
|
|
147
|
+
```bash
|
|
148
|
+
cartha miner password --wallet-name your-wallet --wallet-hotkey your-hotkey
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Tip:** Use `miner status` for daily checks—it's faster and doesn't require signing. Only use `miner password` when you actually need it.
|
|
152
|
+
|
|
153
|
+
### Check Your Setup
|
|
154
|
+
|
|
155
|
+
Verify your CLI is configured correctly and can reach all services:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
cartha health
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This checks:
|
|
162
|
+
- Verifier connectivity and latency
|
|
163
|
+
- Bittensor network connectivity
|
|
164
|
+
- Configuration validation
|
|
165
|
+
|
|
166
|
+
Use `cartha health --verbose` for detailed troubleshooting information.
|
|
167
|
+
|
|
168
|
+
## Need Help?
|
|
169
|
+
|
|
170
|
+
- **[Full Command Reference](docs/COMMANDS.md)** - Complete guide to all commands
|
|
171
|
+
- **[Testnet Guide](testnet/README.md)** - Getting started on testnet
|
|
172
|
+
- **[Feedback & Support](docs/FEEDBACK.md)** - Questions or suggestions?
|
|
173
|
+
|
|
174
|
+
## Contributing
|
|
175
|
+
|
|
176
|
+
We welcome contributions! Please see our [Feedback & Support](docs/FEEDBACK.md) page for ways to get involved.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
**Made with ❤ by GTV**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
cartha_cli/__init__.py,sha256=cSKsPAfHW8_hqJkGMXUh4mFNz9HXsq-UcgRl8G_2KhM,720
|
|
2
|
+
cartha_cli/bt.py,sha256=GGW8QvOgFPFNrIghu7JNygRmTgBeMt2vhEh26ZiPe4Q,6933
|
|
3
|
+
cartha_cli/config.py,sha256=MSHNiRow1tzHUJeMOvQRQsTslntV6DecLTQ-Y_CmB4A,2294
|
|
4
|
+
cartha_cli/display.py,sha256=Krim69DLgiCHSYDuWNjnOep25IAHUebRAceePHfsiRA,2040
|
|
5
|
+
cartha_cli/eth712.py,sha256=5NU0MnvOk89mxWnkDHzoOaSHN8TJGRAHVLGXmCq8jhM,241
|
|
6
|
+
cartha_cli/main.py,sha256=0-G2syhsj2okLblX5WYb5NGqWjOwF5eW2tDn9AF5vaw,6756
|
|
7
|
+
cartha_cli/pair.py,sha256=Y-TAjAK5FeWqKlUK52dHZLdBhDxx1CJbea8mlsQXkqc,6245
|
|
8
|
+
cartha_cli/utils.py,sha256=LWlJXzWNkIpInWclCJ2PObkG--JEN1mIcvVXsHmFllE,8429
|
|
9
|
+
cartha_cli/verifier.py,sha256=JFtDA9bhecsAUCQEp0SBrZ1Z-48EkDT1Q6y7X4eTK60,10548
|
|
10
|
+
cartha_cli/wallet.py,sha256=Jha1pONa4Hy2XsHMTrk60eBqdkwJyzsQYgPuI-cxxFo,1770
|
|
11
|
+
cartha_cli/commands/__init__.py,sha256=8CYMVEWJmg2qjLyE3ZeheQtS-E9doltDSfyyumOsET4,345
|
|
12
|
+
cartha_cli/commands/common.py,sha256=tpxKsdzjFtcb0ae7J-J-zxnh32qVXZd8jwjP-frFoio,2099
|
|
13
|
+
cartha_cli/commands/config.py,sha256=XT1LQo7b9vVm_mgwUTJRLCBCHJhtGclBzh_CTBAd280,10539
|
|
14
|
+
cartha_cli/commands/health.py,sha256=NRwmIultxUzAe7udOOBIdkcdGG5KsE_kuCs43LZObGk,20717
|
|
15
|
+
cartha_cli/commands/help.py,sha256=6ubfWtmjXfCtp6L_PYvn7rR7m5C_pp-iEjtRc6BS0GA,1721
|
|
16
|
+
cartha_cli/commands/miner_password.py,sha256=7cbcyrJ9KzCyJ68_174U_CXjBUt9BaYhgKAycRpv7AE,11078
|
|
17
|
+
cartha_cli/commands/miner_status.py,sha256=tWlCWcuwm9NEd4USuCLp_6qObikX2odc2e7m6Y_vNrU,21873
|
|
18
|
+
cartha_cli/commands/pair_status.py,sha256=Sk6-bIAcgAH3KxGg0Hw3PofLW_4eyonLUmJOkJ8gem0,19821
|
|
19
|
+
cartha_cli/commands/pools.py,sha256=LkFJlur1T5lxV2qz7DeYYj9GbyRiy0wjb1mUNsV1zQg,4104
|
|
20
|
+
cartha_cli/commands/prove_lock.py,sha256=rNxuKupTGyJ-SzKA5Yil6t6fPmLuzbX4jG4D4ZzVo4c,60562
|
|
21
|
+
cartha_cli/commands/register.py,sha256=m7BLTTmiEbeXRrStUSNQbwI79IN4KOBEjU-nN6cSU4k,10109
|
|
22
|
+
cartha_cli/commands/shared_options.py,sha256=itHzJSgxuKQxUVOh1_jVTcMQXjI3PPzexQyhqIbabxc,5874
|
|
23
|
+
cartha_cli/commands/version.py,sha256=u5oeccQzK0LLcCbgZm0U8-Vslk5vB_lVvW3xT5HPeTg,456
|
|
24
|
+
cartha_cli-1.0.0.dist-info/METADATA,sha256=W_n3aBURJG5xcgP-r9HF9grG5stONJLHYRsrUgaOZcg,5875
|
|
25
|
+
cartha_cli-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
+
cartha_cli-1.0.0.dist-info/entry_points.txt,sha256=sTYVMgb9l0fuJibUtWpGnIoDmgHinne97G4Y_cCwC-U,43
|
|
27
|
+
cartha_cli-1.0.0.dist-info/licenses/LICENSE,sha256=B4UCiDn13m4xYwIl4TMKfbuKw7kh9pg4c81rJecxHSo,1076
|
|
28
|
+
cartha_cli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Cartha Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|