htcli 1.1.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.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- src/htcli/utils/wallet/migration.py +159 -0
src/htcli/config.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for the Hypertensor CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NetworkConfig(BaseModel):
|
|
13
|
+
"""Network configuration."""
|
|
14
|
+
|
|
15
|
+
endpoint: str = Field("wss://rpc.htcli.io/", description="RPC endpoint")
|
|
16
|
+
ws_endpoint: str = Field("wss://rpc.htcli.io/", description="WebSocket endpoint")
|
|
17
|
+
timeout: int = Field(30, description="Connection timeout in seconds")
|
|
18
|
+
retry_attempts: int = Field(3, description="Number of retry attempts")
|
|
19
|
+
network_type: str = Field(
|
|
20
|
+
"mainnet", description="Network type (mainnet/testnet/devnet)"
|
|
21
|
+
)
|
|
22
|
+
evm_compatible: bool = Field(
|
|
23
|
+
True, description="Whether network supports EVM compatibility"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
NETWORK_PROFILES: dict[str, dict[str, object]] = {
|
|
28
|
+
"mainnet": {
|
|
29
|
+
"endpoint": "wss://rpc.htcli.io/",
|
|
30
|
+
"ws_endpoint": "wss://rpc.htcli.io/",
|
|
31
|
+
"network_type": "mainnet",
|
|
32
|
+
"evm_compatible": True,
|
|
33
|
+
},
|
|
34
|
+
"testnet": {
|
|
35
|
+
"endpoint": "wss://testnet-rpc.hypertensor.io",
|
|
36
|
+
"ws_endpoint": "wss://testnet-ws.hypertensor.io",
|
|
37
|
+
"network_type": "testnet",
|
|
38
|
+
"evm_compatible": False,
|
|
39
|
+
},
|
|
40
|
+
"local": {
|
|
41
|
+
"endpoint": "ws://127.0.0.1:9944",
|
|
42
|
+
"ws_endpoint": "ws://127.0.0.1:9944",
|
|
43
|
+
"network_type": "devnet",
|
|
44
|
+
"evm_compatible": False,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
NETWORK_ALIASES = {
|
|
49
|
+
"main": "mainnet",
|
|
50
|
+
"mainnet": "mainnet",
|
|
51
|
+
"test": "testnet",
|
|
52
|
+
"testnet": "testnet",
|
|
53
|
+
"local": "local",
|
|
54
|
+
"dev": "local",
|
|
55
|
+
"devnet": "local",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def apply_network_profile(config: "Config", network_name: str) -> "Config":
|
|
60
|
+
"""Apply a named network profile to the active config."""
|
|
61
|
+
normalized = NETWORK_ALIASES.get(network_name.strip().lower())
|
|
62
|
+
if normalized is None:
|
|
63
|
+
valid = ", ".join(sorted(NETWORK_ALIASES))
|
|
64
|
+
raise ValueError(f"Unknown network '{network_name}'. Expected one of: {valid}")
|
|
65
|
+
|
|
66
|
+
profile = NETWORK_PROFILES[normalized]
|
|
67
|
+
config.network.endpoint = str(profile["endpoint"])
|
|
68
|
+
config.network.ws_endpoint = str(profile["ws_endpoint"])
|
|
69
|
+
config.network.network_type = str(profile["network_type"])
|
|
70
|
+
config.network.evm_compatible = bool(profile["evm_compatible"])
|
|
71
|
+
return config
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class OutputConfig(BaseModel):
|
|
75
|
+
"""Output configuration."""
|
|
76
|
+
|
|
77
|
+
format: str = Field("table", description="Output format (table/json/csv)")
|
|
78
|
+
verbose: bool = Field(False, description="Verbose output")
|
|
79
|
+
color: bool = Field(True, description="Enable colored output")
|
|
80
|
+
color_scheme: str = Field(
|
|
81
|
+
"default",
|
|
82
|
+
description="Color scheme (default/dark/light/high_contrast/soft)",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FilterConfig(BaseModel):
|
|
87
|
+
"""Filter configuration."""
|
|
88
|
+
|
|
89
|
+
mine: bool = Field(False, description="Filter results to show only user assets")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class WalletConfig(BaseModel):
|
|
93
|
+
"""Wallet configuration."""
|
|
94
|
+
|
|
95
|
+
path: str = Field("~/.htcli/wallets", description="Wallet storage path")
|
|
96
|
+
default_name: str = Field("default", description="Default wallet name")
|
|
97
|
+
encryption_enabled: bool = Field(True, description="Enable wallet encryption")
|
|
98
|
+
default_key_type: str = Field(
|
|
99
|
+
"ecdsa",
|
|
100
|
+
description="Default key type for new wallets (ecdsa/sr25519/ed25519)",
|
|
101
|
+
)
|
|
102
|
+
mainnet_key_type: str = Field(
|
|
103
|
+
"ecdsa", description="Required key type for mainnet (EVM compatibility)"
|
|
104
|
+
)
|
|
105
|
+
testnet_key_type: str = Field("sr25519", description="Default key type for testnet")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Config(BaseModel):
|
|
109
|
+
"""Main configuration."""
|
|
110
|
+
|
|
111
|
+
network: NetworkConfig = Field(default_factory=NetworkConfig)
|
|
112
|
+
output: OutputConfig = Field(default_factory=OutputConfig)
|
|
113
|
+
wallet: WalletConfig = Field(default_factory=WalletConfig)
|
|
114
|
+
filter: FilterConfig = Field(default_factory=FilterConfig)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_config(config_file: Optional[Path] = None) -> Config:
|
|
118
|
+
"""Load configuration from file or use defaults."""
|
|
119
|
+
if config_file and config_file.exists():
|
|
120
|
+
# Load from file
|
|
121
|
+
import yaml
|
|
122
|
+
|
|
123
|
+
with open(config_file) as f:
|
|
124
|
+
config_data = yaml.safe_load(f)
|
|
125
|
+
return Config(**config_data)
|
|
126
|
+
else:
|
|
127
|
+
# Try to load from YAML config file created by config commands
|
|
128
|
+
yaml_config_path = Path.home() / ".htcli" / "config.yaml"
|
|
129
|
+
if yaml_config_path.exists():
|
|
130
|
+
try:
|
|
131
|
+
import yaml
|
|
132
|
+
|
|
133
|
+
with open(yaml_config_path) as f:
|
|
134
|
+
yaml_config_data = yaml.safe_load(f)
|
|
135
|
+
# Load directly as YAML already matches Pydantic Config structure
|
|
136
|
+
return Config(**yaml_config_data)
|
|
137
|
+
except Exception:
|
|
138
|
+
# If YAML config is invalid or can't be read, fall through to env vars/defaults
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# Use environment variables or defaults
|
|
142
|
+
network_config = NetworkConfig(
|
|
143
|
+
endpoint=os.getenv("HTCLI_NETWORK_ENDPOINT", "wss://rpc.htcli.io/"),
|
|
144
|
+
ws_endpoint=os.getenv("HTCLI_NETWORK_WS_ENDPOINT", "wss://rpc.htcli.io/"),
|
|
145
|
+
timeout=int(os.getenv("HTCLI_NETWORK_TIMEOUT", "30")),
|
|
146
|
+
retry_attempts=int(os.getenv("HTCLI_NETWORK_RETRY_ATTEMPTS", "3")),
|
|
147
|
+
network_type=os.getenv("HTCLI_NETWORK_TYPE", "mainnet"),
|
|
148
|
+
evm_compatible=os.getenv("HTCLI_NETWORK_EVM_COMPATIBLE", "true").lower()
|
|
149
|
+
== "true",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
output_config = OutputConfig(
|
|
153
|
+
format=os.getenv("HTCLI_OUTPUT_FORMAT", "table"),
|
|
154
|
+
verbose=os.getenv("HTCLI_OUTPUT_VERBOSE", "false").lower() == "true",
|
|
155
|
+
color=os.getenv("HTCLI_OUTPUT_COLOR", "true").lower() == "true",
|
|
156
|
+
color_scheme=os.getenv("HTCLI_OUTPUT_COLOR_SCHEME", "default"),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
wallet_config = WalletConfig(
|
|
160
|
+
path=os.getenv("HTCLI_WALLET_PATH", "~/.htcli/wallets"),
|
|
161
|
+
default_name=os.getenv("HTCLI_WALLET_DEFAULT_NAME", "default"),
|
|
162
|
+
encryption_enabled=os.getenv(
|
|
163
|
+
"HTCLI_WALLET_ENCRYPTION_ENABLED", "true"
|
|
164
|
+
).lower()
|
|
165
|
+
== "true",
|
|
166
|
+
default_key_type=os.getenv("HTCLI_WALLET_DEFAULT_KEY_TYPE", "ecdsa"),
|
|
167
|
+
mainnet_key_type=os.getenv("HTCLI_WALLET_MAINNET_KEY_TYPE", "ecdsa"),
|
|
168
|
+
testnet_key_type=os.getenv("HTCLI_WALLET_TESTNET_KEY_TYPE", "sr25519"),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
filter_config = FilterConfig(
|
|
172
|
+
mine=os.getenv("HTCLI_FILTER_MINE", "false").lower() == "true"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return Config(
|
|
176
|
+
network=network_config,
|
|
177
|
+
output=output_config,
|
|
178
|
+
wallet=wallet_config,
|
|
179
|
+
filter=filter_config,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Global configuration instance
|
|
184
|
+
config_instance = load_config()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dependencies for the Hypertensor CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .client import HypertensorClient
|
|
9
|
+
from .config import Config
|
|
10
|
+
from .utils.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
# Global client instance and config
|
|
15
|
+
_client: Optional[HypertensorClient] = None
|
|
16
|
+
_config: Optional[Config] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _retry_delay_seconds(attempt: int) -> int:
|
|
20
|
+
"""Return bounded exponential backoff delay for a retry attempt."""
|
|
21
|
+
return min(2 ** attempt, 5)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_closed_websocket_error(error: Exception) -> bool:
|
|
25
|
+
"""Classify common closed WebSocket failures without depending on one package."""
|
|
26
|
+
error_name = error.__class__.__name__
|
|
27
|
+
error_message = str(error).lower()
|
|
28
|
+
return (
|
|
29
|
+
error_name == "WebSocketConnectionClosedException"
|
|
30
|
+
or "websocket" in error_message
|
|
31
|
+
and ("closed" in error_message or "not connected" in error_message)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _websocket_is_connected(substrate) -> bool:
|
|
36
|
+
"""Best-effort connection-state check that avoids network calls."""
|
|
37
|
+
websocket = getattr(substrate, "websocket", None)
|
|
38
|
+
if not websocket:
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
if hasattr(websocket, "connected"):
|
|
42
|
+
return bool(websocket.connected)
|
|
43
|
+
|
|
44
|
+
sock = getattr(websocket, "sock", None)
|
|
45
|
+
if sock is None:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
if hasattr(sock, "connected"):
|
|
49
|
+
return bool(sock.connected)
|
|
50
|
+
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _client_needs_reconnect(client: HypertensorClient) -> bool:
|
|
55
|
+
"""Check whether the global client should reconnect before use."""
|
|
56
|
+
if not client.substrate or not client.rpc or not client.extrinsics:
|
|
57
|
+
logger.debug("Connection layers missing, reconnecting...")
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
if not _websocket_is_connected(client.substrate):
|
|
62
|
+
logger.debug("WebSocket connection appears closed, reconnecting...")
|
|
63
|
+
return True
|
|
64
|
+
except Exception as check_error:
|
|
65
|
+
logger.debug(
|
|
66
|
+
f"Lightweight connection check failed: {check_error}, will reconnect"
|
|
67
|
+
)
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _connect_with_retries(
|
|
74
|
+
client: HypertensorClient,
|
|
75
|
+
max_retries: int,
|
|
76
|
+
*,
|
|
77
|
+
force_reconnect: bool = False,
|
|
78
|
+
operation: str = "Connection",
|
|
79
|
+
) -> tuple[bool, Optional[Exception]]:
|
|
80
|
+
"""Connect or reconnect with bounded exponential backoff."""
|
|
81
|
+
retry_count = 0
|
|
82
|
+
last_error = None
|
|
83
|
+
|
|
84
|
+
while retry_count < max_retries:
|
|
85
|
+
try:
|
|
86
|
+
if client.connect(force_reconnect=force_reconnect):
|
|
87
|
+
logger.debug(
|
|
88
|
+
f"{operation} succeeded (attempt {retry_count + 1})"
|
|
89
|
+
)
|
|
90
|
+
return True, None
|
|
91
|
+
|
|
92
|
+
retry_count += 1
|
|
93
|
+
if retry_count < max_retries:
|
|
94
|
+
logger.warning(
|
|
95
|
+
f"{operation} attempt {retry_count} failed, retrying..."
|
|
96
|
+
)
|
|
97
|
+
time.sleep(_retry_delay_seconds(retry_count))
|
|
98
|
+
except Exception as error:
|
|
99
|
+
last_error = error
|
|
100
|
+
retry_count += 1
|
|
101
|
+
if retry_count < max_retries:
|
|
102
|
+
if _is_closed_websocket_error(error):
|
|
103
|
+
logger.warning(
|
|
104
|
+
f"{operation} attempt {retry_count} hit a closed WebSocket, retrying..."
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
logger.warning(
|
|
108
|
+
f"{operation} attempt {retry_count} failed: {error}, retrying..."
|
|
109
|
+
)
|
|
110
|
+
time.sleep(_retry_delay_seconds(retry_count))
|
|
111
|
+
else:
|
|
112
|
+
logger.error(f"All {max_retries} {operation.lower()} attempts failed")
|
|
113
|
+
|
|
114
|
+
return False, last_error
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def set_client(client: HypertensorClient):
|
|
118
|
+
"""Set the global client instance."""
|
|
119
|
+
global _client
|
|
120
|
+
_client = client
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def set_config(config: Config):
|
|
124
|
+
"""Set the global config instance for lazy client initialization."""
|
|
125
|
+
global _config
|
|
126
|
+
_config = config
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_client() -> HypertensorClient:
|
|
130
|
+
"""Get the global client instance, initializing it lazily if needed."""
|
|
131
|
+
global _client, _config
|
|
132
|
+
|
|
133
|
+
if _client is None:
|
|
134
|
+
if _config is None:
|
|
135
|
+
raise RuntimeError(
|
|
136
|
+
"Configuration not set. Please ensure config is loaded before using client."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Initialize client only when first requested
|
|
140
|
+
_client = HypertensorClient(_config)
|
|
141
|
+
|
|
142
|
+
# Connect to blockchain when client is first accessed with retry logic
|
|
143
|
+
max_retries = _config.network.retry_attempts or 3
|
|
144
|
+
_, last_error = _connect_with_retries(
|
|
145
|
+
_client,
|
|
146
|
+
max_retries,
|
|
147
|
+
operation="Connection",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not _client.is_connected():
|
|
151
|
+
error_msg = str(last_error) if last_error else "Connection failed after retries"
|
|
152
|
+
raise RuntimeError(
|
|
153
|
+
f"Failed to connect to blockchain after {max_retries} attempts. "
|
|
154
|
+
f"Please check your network connection and configuration. Last error: {error_msg}"
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
needs_reconnect = _client_needs_reconnect(_client)
|
|
158
|
+
|
|
159
|
+
# Only reconnect if lightweight check indicates we need to
|
|
160
|
+
if needs_reconnect:
|
|
161
|
+
logger.debug("Connection appears lost, attempting to reconnect...")
|
|
162
|
+
# Try to reconnect with force flag to close old connection
|
|
163
|
+
max_retries = _config.network.retry_attempts or 3
|
|
164
|
+
_, last_error = _connect_with_retries(
|
|
165
|
+
_client,
|
|
166
|
+
max_retries,
|
|
167
|
+
force_reconnect=True,
|
|
168
|
+
operation="Reconnection",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Final verification: do a lightweight check, not expensive query
|
|
172
|
+
if _client_needs_reconnect(_client):
|
|
173
|
+
error_msg = str(last_error) if last_error else "Reconnection failed after retries"
|
|
174
|
+
# Reset client to allow fresh connection on next call
|
|
175
|
+
_client = None
|
|
176
|
+
raise RuntimeError(
|
|
177
|
+
f"Blockchain connection lost. Failed to reconnect after {max_retries} attempts. "
|
|
178
|
+
f"Please check your network connection. Last error: {error_msg}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return _client
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_config() -> Optional[Config]:
|
|
185
|
+
"""Get the global config instance."""
|
|
186
|
+
return _config
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error handling module for htcli.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import (
|
|
6
|
+
AddressValidationError,
|
|
7
|
+
BalanceError,
|
|
8
|
+
HTCLIError,
|
|
9
|
+
KeyDeletionError,
|
|
10
|
+
KeyGenerationError,
|
|
11
|
+
NodeError,
|
|
12
|
+
NodeOperationError,
|
|
13
|
+
NodeRegistrationError,
|
|
14
|
+
SubnetActivationError,
|
|
15
|
+
SubnetError,
|
|
16
|
+
SubnetRegistrationError,
|
|
17
|
+
SubnetValidationError,
|
|
18
|
+
TransferError,
|
|
19
|
+
WalletError,
|
|
20
|
+
)
|
|
21
|
+
from .display import display_balance_info
|
|
22
|
+
from .handlers import (
|
|
23
|
+
display_pydantic_validation_error,
|
|
24
|
+
format_pydantic_validation_error,
|
|
25
|
+
get_validation_suggestions,
|
|
26
|
+
handle_and_display_error,
|
|
27
|
+
handle_and_display_node_error,
|
|
28
|
+
handle_and_display_subnet_error,
|
|
29
|
+
handle_blockchain_error,
|
|
30
|
+
handle_node_error,
|
|
31
|
+
handle_subnet_error,
|
|
32
|
+
handle_substrate_error,
|
|
33
|
+
handle_wallet_error,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"HTCLIError",
|
|
38
|
+
"WalletError",
|
|
39
|
+
"TransferError",
|
|
40
|
+
"BalanceError",
|
|
41
|
+
"KeyGenerationError",
|
|
42
|
+
"KeyDeletionError",
|
|
43
|
+
"NodeError",
|
|
44
|
+
"NodeOperationError",
|
|
45
|
+
"NodeRegistrationError",
|
|
46
|
+
"SubnetError",
|
|
47
|
+
"SubnetRegistrationError",
|
|
48
|
+
"SubnetActivationError",
|
|
49
|
+
"SubnetValidationError",
|
|
50
|
+
"AddressValidationError",
|
|
51
|
+
"handle_blockchain_error",
|
|
52
|
+
"handle_wallet_error",
|
|
53
|
+
"handle_and_display_error",
|
|
54
|
+
"handle_node_error",
|
|
55
|
+
"handle_and_display_node_error",
|
|
56
|
+
"handle_subnet_error",
|
|
57
|
+
"handle_and_display_subnet_error",
|
|
58
|
+
"handle_substrate_error",
|
|
59
|
+
"display_balance_info",
|
|
60
|
+
"display_pydantic_validation_error",
|
|
61
|
+
"format_pydantic_validation_error",
|
|
62
|
+
"get_validation_suggestions",
|
|
63
|
+
]
|
src/htcli/errors/base.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base error classes for htcli.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HTCLIError(Exception):
|
|
13
|
+
"""Base exception class for htcli errors."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, suggestions: Optional[list[str]] = None):
|
|
16
|
+
self.message = message
|
|
17
|
+
self.suggestions = suggestions or []
|
|
18
|
+
super().__init__(self.message)
|
|
19
|
+
|
|
20
|
+
def display(self):
|
|
21
|
+
"""Display the error with suggestions."""
|
|
22
|
+
console.print(f"❌ {self.message}")
|
|
23
|
+
|
|
24
|
+
if self.suggestions:
|
|
25
|
+
console.print("\n[bold yellow]💡 Helpful Suggestions:[/bold yellow]")
|
|
26
|
+
for suggestion in self.suggestions:
|
|
27
|
+
console.print(f"• {suggestion}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WalletError(HTCLIError):
|
|
31
|
+
"""Base class for wallet-related errors."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TransferError(HTCLIError):
|
|
37
|
+
"""Base class for transfer-related errors."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BalanceError(HTCLIError):
|
|
43
|
+
"""Base class for balance-related errors."""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class KeyGenerationError(HTCLIError):
|
|
49
|
+
"""Base class for key generation errors."""
|
|
50
|
+
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class KeyDeletionError(HTCLIError):
|
|
55
|
+
"""Base class for key deletion errors."""
|
|
56
|
+
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SubnetError(HTCLIError):
|
|
61
|
+
"""Base class for subnet-related errors."""
|
|
62
|
+
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SubnetRegistrationError(HTCLIError):
|
|
67
|
+
"""Custom exception for subnet registration errors."""
|
|
68
|
+
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class InvalidPasswordError(HTCLIError):
|
|
73
|
+
"""Custom exception for invalid wallet password."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SubnetActivationError(SubnetError):
|
|
77
|
+
"""Error during subnet activation."""
|
|
78
|
+
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SubnetValidationError(SubnetError):
|
|
83
|
+
"""Error during subnet input validation."""
|
|
84
|
+
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AddressValidationError(HTCLIError):
|
|
89
|
+
"""Error during address validation."""
|
|
90
|
+
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class NodeError(HTCLIError):
|
|
95
|
+
"""Base class for node-related errors."""
|
|
96
|
+
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class NodeRegistrationError(NodeError):
|
|
101
|
+
"""Custom exception for node registration errors."""
|
|
102
|
+
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class NodeOperationError(NodeError):
|
|
107
|
+
"""Error during node operations."""
|
|
108
|
+
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class StakeError(HTCLIError):
|
|
113
|
+
"""Base class for stake-related errors."""
|
|
114
|
+
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DelegateStakeError(StakeError):
|
|
119
|
+
"""Error during delegate stake operations."""
|
|
120
|
+
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Add the custom exception to errors/base.py
|
|
125
|
+
class AmbiguousWalletError(HTCLIError):
|
|
126
|
+
"""Raised when multiple wallets match the given criteria."""
|
|
127
|
+
def __init__(self, name: str, matches: list[dict], is_hotkey: bool = False):
|
|
128
|
+
self.name = name
|
|
129
|
+
self.matches = matches
|
|
130
|
+
self.is_hotkey = is_hotkey
|
|
131
|
+
error_msg = (
|
|
132
|
+
f"Wallet '{name}' is ambiguous. Found {len(matches)} hotkey wallets "
|
|
133
|
+
f"with this name. Please specify owner_address or owner_coldkey_name to disambiguate:\n"
|
|
134
|
+
)
|
|
135
|
+
for match in matches:
|
|
136
|
+
owner_addr = match.get("owner_address", "Unknown")
|
|
137
|
+
owner_ck = match.get("owner_coldkey_name", "Unknown")
|
|
138
|
+
error_msg += (
|
|
139
|
+
f" - Owner coldkey: {owner_ck} (address: {owner_addr})\n"
|
|
140
|
+
)
|
|
141
|
+
super().__init__(error_msg)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Display utility functions for error handling.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def display_balance_info(address: str, balance: float = 0):
|
|
11
|
+
"""Display helpful information when balance is 0."""
|
|
12
|
+
if balance == 0:
|
|
13
|
+
console.print(
|
|
14
|
+
"\n[bold yellow]💡 Note:[/bold yellow] This wallet has no balance."
|
|
15
|
+
)
|
|
16
|
+
console.print(f"• To receive funds, share this address: [bold]{address}[/bold]")
|
|
17
|
+
console.print(
|
|
18
|
+
"• You can transfer funds from another wallet using: [bold]htcli wallet transfer[/bold]"
|
|
19
|
+
)
|
|
20
|
+
console.print("• Transaction fees are typically around 0.001-0.01 TENSOR")
|