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
|
@@ -0,0 +1,2855 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ...dependencies import get_client
|
|
6
|
+
from ...errors.base import SubnetActivationError, SubnetError, SubnetRegistrationError
|
|
7
|
+
from ...errors.handlers import (
|
|
8
|
+
handle_and_display_error,
|
|
9
|
+
handle_and_display_node_error,
|
|
10
|
+
handle_substrate_error,
|
|
11
|
+
)
|
|
12
|
+
from ...ui.colors import success
|
|
13
|
+
from ...ui.components import HTCLILoadingContext
|
|
14
|
+
from ...ui.display import print_error, print_info
|
|
15
|
+
from ...utils import retrieve_wallet_with_validation
|
|
16
|
+
from ...utils.constants import (
|
|
17
|
+
MAX_SUBNET_PENALTY_COUNT,
|
|
18
|
+
MIN_SUBNET_DELEGATE_STAKE_FACTOR_PERCENT,
|
|
19
|
+
MIN_SUBNET_NODES,
|
|
20
|
+
format_percentage,
|
|
21
|
+
)
|
|
22
|
+
from ...utils.logging import get_logger
|
|
23
|
+
from .error_handling import handle_subnet_error
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
from .display import (
|
|
27
|
+
display_activation_requirements, # New display functions
|
|
28
|
+
display_bootnodes_rpc,
|
|
29
|
+
display_generic_success,
|
|
30
|
+
display_subnet_info,
|
|
31
|
+
display_subnet_info_rpc,
|
|
32
|
+
display_subnet_list,
|
|
33
|
+
display_subnet_list_with_coldkey,
|
|
34
|
+
display_subnet_nodes,
|
|
35
|
+
display_subnet_nodes_rpc,
|
|
36
|
+
display_subnets_overview,
|
|
37
|
+
)
|
|
38
|
+
from .prompts import (
|
|
39
|
+
prompt_activate_subnet,
|
|
40
|
+
prompt_bootnode_access,
|
|
41
|
+
prompt_check_activation,
|
|
42
|
+
prompt_clear_emergency_validator_set,
|
|
43
|
+
prompt_get_subnet,
|
|
44
|
+
prompt_list_nodes,
|
|
45
|
+
prompt_owner_accept,
|
|
46
|
+
prompt_owner_remove,
|
|
47
|
+
prompt_owner_transfer,
|
|
48
|
+
prompt_owner_update,
|
|
49
|
+
prompt_owner_update_extended,
|
|
50
|
+
prompt_owner_update_name,
|
|
51
|
+
prompt_owner_update_repo,
|
|
52
|
+
prompt_pause_subnet,
|
|
53
|
+
prompt_register_subnet,
|
|
54
|
+
prompt_set_emergency_validator_set,
|
|
55
|
+
prompt_unpause_subnet,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
DELEGATE_STAKE_FACTOR_DISPLAY = format_percentage(
|
|
59
|
+
MIN_SUBNET_DELEGATE_STAKE_FACTOR_PERCENT, precision=3
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
ACTIVATION_REQUIREMENTS_TEXT = f"""📋 Activation requirements:
|
|
63
|
+
|
|
64
|
+
1. Minimum nodes:
|
|
65
|
+
• Register at least {MIN_SUBNET_NODES} active/electable nodes
|
|
66
|
+
|
|
67
|
+
2. Delegate stake:
|
|
68
|
+
• Total delegate stake across your nodes must meet the floating minimum
|
|
69
|
+
(~{DELEGATE_STAKE_FACTOR_DISPLAY} of total supply × node-count multiplier)
|
|
70
|
+
|
|
71
|
+
3. Penalties:
|
|
72
|
+
• Subnet penalty count must stay at or below {MAX_SUBNET_PENALTY_COUNT}
|
|
73
|
+
|
|
74
|
+
4. Timing:
|
|
75
|
+
• Make sure the minimum registration epochs have elapsed before retrying"""
|
|
76
|
+
|
|
77
|
+
ACTIVATION_NEXT_STEPS_TEXT = """💡 Next Steps:
|
|
78
|
+
|
|
79
|
+
• Register additional nodes if you're below the minimum
|
|
80
|
+
• Delegate more stake (or attract delegations) to exceed the floating threshold
|
|
81
|
+
• Resolve any penalties affecting the subnet before retrying
|
|
82
|
+
• Retry activation once the conditions above are comfortably met"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Read Handlers
|
|
86
|
+
def registration_cost_handler():
|
|
87
|
+
"""Handle subnet registration cost query."""
|
|
88
|
+
try:
|
|
89
|
+
client = get_client()
|
|
90
|
+
|
|
91
|
+
# Ensure client is connected and RPC is available
|
|
92
|
+
if not client.rpc:
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
"Client RPC layer not initialized. Connection may have failed."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with HTCLILoadingContext("Querying subnet registration cost..."):
|
|
98
|
+
current_cost = client.rpc.chain.get_subnet_registration_cost()
|
|
99
|
+
block_number = client.rpc.chain.get_block_number()
|
|
100
|
+
current_epoch = client.rpc.chain.get_current_epoch()
|
|
101
|
+
|
|
102
|
+
# Display - provide helpful message if cost is None
|
|
103
|
+
if current_cost is None:
|
|
104
|
+
print_error("Failed to query registration cost from blockchain.")
|
|
105
|
+
print_info(
|
|
106
|
+
"This may indicate a connection issue or the storage values are not set yet."
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
from ...commands.chain.display import display_registration_cost
|
|
110
|
+
|
|
111
|
+
display_registration_cost(current_cost, block_number, current_epoch)
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
handle_and_display_error(e)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def check_registration_cost_handler():
|
|
118
|
+
"""Check subnet registration cost and compare with all wallet balances."""
|
|
119
|
+
try:
|
|
120
|
+
from ...ui.display import print_error, print_info, print_warning
|
|
121
|
+
from ...utils.wallet.crypto import list_keys
|
|
122
|
+
|
|
123
|
+
client = get_client()
|
|
124
|
+
|
|
125
|
+
print_info("💰 Fetching subnet registration cost...")
|
|
126
|
+
print_info("📡 Querying Network pallet storage values...")
|
|
127
|
+
|
|
128
|
+
# Get current registration cost
|
|
129
|
+
cost_wei = client.rpc.chain.get_subnet_registration_cost()
|
|
130
|
+
|
|
131
|
+
if cost_wei is None:
|
|
132
|
+
print_error("Failed to fetch registration cost from blockchain")
|
|
133
|
+
print_warning("\n💡 Possible causes:")
|
|
134
|
+
print_warning(" 1. Network pallet storage values not initialized")
|
|
135
|
+
print_warning(" 2. Blockchain connection issue")
|
|
136
|
+
print_warning(" 3. Storage value names may have changed")
|
|
137
|
+
print_warning("\n📋 Required storage values:")
|
|
138
|
+
print_warning(" - Network::LastRegistrationCost")
|
|
139
|
+
print_warning(" - Network::MinRegistrationCost")
|
|
140
|
+
print_warning(" - Network::LastSubnetRegistrationBlock")
|
|
141
|
+
print_warning(" - Network::RegistrationCostDecayBlocks")
|
|
142
|
+
print_warning(" - Network::RegistrationCostAlpha")
|
|
143
|
+
print_warning("\n🔧 Check the logs above to see which value is missing")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
cost_tensor = cost_wei / 1e18
|
|
147
|
+
|
|
148
|
+
# Get all coldkey wallets
|
|
149
|
+
all_wallets = list_keys()
|
|
150
|
+
coldkeys = [w for w in all_wallets if not w.get("is_hotkey", False)]
|
|
151
|
+
|
|
152
|
+
if not coldkeys:
|
|
153
|
+
from ...ui.display import print_warning
|
|
154
|
+
|
|
155
|
+
print_warning(
|
|
156
|
+
"No coldkey wallets found. Create one with: htcli wallet generate-coldkey"
|
|
157
|
+
)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
print_info(f"📊 Checking {len(coldkeys)} coldkey wallet balances...")
|
|
161
|
+
|
|
162
|
+
# Fetch balances for all coldkeys with progress indicator
|
|
163
|
+
from ...ui.components import HTCLIProgress
|
|
164
|
+
|
|
165
|
+
wallet_data = []
|
|
166
|
+
with HTCLIProgress() as progress:
|
|
167
|
+
task = progress.add_task("Fetching balances...", total=len(coldkeys))
|
|
168
|
+
|
|
169
|
+
for wallet in coldkeys:
|
|
170
|
+
wallet_name = wallet["name"]
|
|
171
|
+
address = wallet.get("ss58_address")
|
|
172
|
+
|
|
173
|
+
progress.update(
|
|
174
|
+
task,
|
|
175
|
+
advance=1,
|
|
176
|
+
description=f"Checking [bold]{wallet_name}[/bold]...",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
balance_response = client.rpc.wallet.get_balance(address)
|
|
181
|
+
if balance_response.success:
|
|
182
|
+
balance_wei = balance_response.balance
|
|
183
|
+
balance_tensor = balance_wei / 1e18
|
|
184
|
+
sufficient = balance_wei >= cost_wei
|
|
185
|
+
shortfall = (
|
|
186
|
+
(cost_wei - balance_wei) / 1e18 if not sufficient else 0
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
wallet_data.append(
|
|
190
|
+
{
|
|
191
|
+
"name": wallet_name,
|
|
192
|
+
"address": address,
|
|
193
|
+
"balance": balance_tensor,
|
|
194
|
+
"sufficient": sufficient,
|
|
195
|
+
"shortfall": shortfall,
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
wallet_data.append(
|
|
200
|
+
{
|
|
201
|
+
"name": wallet_name,
|
|
202
|
+
"address": address,
|
|
203
|
+
"balance": 0,
|
|
204
|
+
"sufficient": False,
|
|
205
|
+
"shortfall": cost_tensor,
|
|
206
|
+
"error": "Failed to fetch balance",
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
wallet_data.append(
|
|
211
|
+
{
|
|
212
|
+
"name": wallet_name,
|
|
213
|
+
"address": address,
|
|
214
|
+
"balance": 0,
|
|
215
|
+
"sufficient": False,
|
|
216
|
+
"shortfall": cost_tensor,
|
|
217
|
+
"error": str(e),
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Display results
|
|
222
|
+
from .display import display_registration_cost_comparison
|
|
223
|
+
|
|
224
|
+
display_registration_cost_comparison(cost_tensor, wallet_data)
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
handle_substrate_error(e)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def list_subnets_handler(
|
|
231
|
+
all_subnets: bool,
|
|
232
|
+
active_only: bool,
|
|
233
|
+
coldkey: Optional[str] = None,
|
|
234
|
+
):
|
|
235
|
+
"""Handle listing subnets with proper error handling and optional coldkey filtering."""
|
|
236
|
+
try:
|
|
237
|
+
client = get_client()
|
|
238
|
+
|
|
239
|
+
# Ensure connection is established
|
|
240
|
+
if not client.connect():
|
|
241
|
+
print_error("Failed to connect to blockchain")
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
# Normalize empty string from "--coldkey \"\"" into None to trigger prompting
|
|
245
|
+
# if isinstance(coldkey, str) and coldkey.strip() == "":
|
|
246
|
+
# coldkey = None
|
|
247
|
+
|
|
248
|
+
# If no mode specified, default to listing all subnets
|
|
249
|
+
if not all_subnets:
|
|
250
|
+
# Default to all subnets when no parameters are provided
|
|
251
|
+
all_subnets = True
|
|
252
|
+
|
|
253
|
+
# Handle coldkey filtering
|
|
254
|
+
if coldkey:
|
|
255
|
+
# Resolve coldkey and fetch subnets with spinner
|
|
256
|
+
subnet_data = None
|
|
257
|
+
with HTCLILoadingContext("Resolving coldkey and fetching subnets..."):
|
|
258
|
+
original_coldkey = coldkey
|
|
259
|
+
|
|
260
|
+
# If coldkey looks like a wallet name, resolve to address
|
|
261
|
+
if not (coldkey.startswith("0x") or len(coldkey) in (48, 49)):
|
|
262
|
+
# Treat as wallet name: resolve to its address if exists
|
|
263
|
+
try:
|
|
264
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
265
|
+
|
|
266
|
+
info = get_wallet_info_by_name(coldkey, is_hotkey=False)
|
|
267
|
+
# Prefer EVM address, fallback to ss58_address
|
|
268
|
+
coldkey = (
|
|
269
|
+
info.get("address") or info.get("ss58_address") or coldkey
|
|
270
|
+
)
|
|
271
|
+
logger.debug(
|
|
272
|
+
f"Resolved wallet name '{original_coldkey}' to address '{coldkey}'"
|
|
273
|
+
)
|
|
274
|
+
except FileNotFoundError:
|
|
275
|
+
# Wallet name not found - show error and exit
|
|
276
|
+
print_error(f"Wallet '{coldkey}' not found")
|
|
277
|
+
return
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning(f"Error resolving wallet name '{coldkey}': {e}")
|
|
280
|
+
# Leave as provided; will try to use as address
|
|
281
|
+
|
|
282
|
+
# Normalize Ethereum addresses to lowercase (addresses are case-insensitive)
|
|
283
|
+
if coldkey.startswith("0x"):
|
|
284
|
+
coldkey = coldkey.lower()
|
|
285
|
+
|
|
286
|
+
# Get subnet IDs for this coldkey using efficient storage queries
|
|
287
|
+
subnet_ids = client.rpc.subnet.get_subnet_ids_for_coldkey(coldkey)
|
|
288
|
+
|
|
289
|
+
if not subnet_ids:
|
|
290
|
+
# No subnets found - will show message after spinner clears
|
|
291
|
+
subnet_data = []
|
|
292
|
+
else:
|
|
293
|
+
# Fetch subnet info for the found subnet IDs
|
|
294
|
+
subnets_info = []
|
|
295
|
+
for subnet_id in sorted(subnet_ids):
|
|
296
|
+
try:
|
|
297
|
+
subnet_info = client.rpc.subnet.get_subnet_info(subnet_id)
|
|
298
|
+
if subnet_info:
|
|
299
|
+
subnets_info.append(subnet_info)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.warning(
|
|
302
|
+
f"Failed to get subnet {subnet_id} info: {e}"
|
|
303
|
+
)
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
if subnets_info:
|
|
307
|
+
# Convert SubnetInfo models to dicts for display
|
|
308
|
+
subnet_data = []
|
|
309
|
+
for subnet in subnets_info:
|
|
310
|
+
# Use model_dump() to get all fields
|
|
311
|
+
subnet_dict = subnet.model_dump()
|
|
312
|
+
|
|
313
|
+
# Decode byte fields to strings
|
|
314
|
+
for field in ["name", "repo", "description", "misc"]:
|
|
315
|
+
if field in subnet_dict and isinstance(
|
|
316
|
+
subnet_dict[field], bytes
|
|
317
|
+
):
|
|
318
|
+
try:
|
|
319
|
+
decoded = subnet_dict[field].decode("utf-8")
|
|
320
|
+
subnet_dict[field] = decoded if decoded else ""
|
|
321
|
+
except:
|
|
322
|
+
subnet_dict[field] = ""
|
|
323
|
+
|
|
324
|
+
# Convert state enum to string if needed
|
|
325
|
+
if "state" in subnet_dict and not isinstance(
|
|
326
|
+
subnet_dict["state"], str
|
|
327
|
+
):
|
|
328
|
+
if hasattr(subnet_dict["state"], "value"):
|
|
329
|
+
subnet_dict["state"] = subnet_dict["state"].value
|
|
330
|
+
elif hasattr(subnet_dict["state"], "name"):
|
|
331
|
+
subnet_dict["state"] = subnet_dict["state"].name
|
|
332
|
+
|
|
333
|
+
subnet_data.append(subnet_dict)
|
|
334
|
+
else:
|
|
335
|
+
subnet_data = []
|
|
336
|
+
|
|
337
|
+
# Display results AFTER spinner is cleared
|
|
338
|
+
if subnet_data is None:
|
|
339
|
+
# Error case - already handled
|
|
340
|
+
pass
|
|
341
|
+
elif len(subnet_data) == 0:
|
|
342
|
+
# No subnets found - informational message
|
|
343
|
+
print_info(f"No subnets found for coldkey: {coldkey}")
|
|
344
|
+
else:
|
|
345
|
+
# Display the subnet list
|
|
346
|
+
display_subnet_list(subnet_data)
|
|
347
|
+
elif all_subnets:
|
|
348
|
+
# Use RPC method to get all subnets info
|
|
349
|
+
with HTCLILoadingContext("Fetching all subnets"):
|
|
350
|
+
all_subnets_models = client.rpc.subnet.get_all_subnets_info()
|
|
351
|
+
|
|
352
|
+
logger.info(f"Retrieved {len(all_subnets_models)} subnets from RPC")
|
|
353
|
+
|
|
354
|
+
# Convert Pydantic models to dicts for display - include all fields for comprehensive table
|
|
355
|
+
all_subnets_data = []
|
|
356
|
+
for subnet in all_subnets_models:
|
|
357
|
+
subnet_id = (
|
|
358
|
+
subnet.id
|
|
359
|
+
if hasattr(subnet, "id")
|
|
360
|
+
else (
|
|
361
|
+
subnet.get("id", "unknown")
|
|
362
|
+
if isinstance(subnet, dict)
|
|
363
|
+
else "unknown"
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
logger.debug(f"Processing subnet ID: {subnet_id}")
|
|
367
|
+
# Use model_dump() to get all fields, then decode byte fields
|
|
368
|
+
subnet_dict = subnet.model_dump()
|
|
369
|
+
|
|
370
|
+
# Decode byte fields to strings
|
|
371
|
+
for field in ["name", "repo", "description", "misc"]:
|
|
372
|
+
if field in subnet_dict and isinstance(subnet_dict[field], bytes):
|
|
373
|
+
try:
|
|
374
|
+
decoded = subnet_dict[field].decode("utf-8")
|
|
375
|
+
subnet_dict[field] = decoded if decoded else ""
|
|
376
|
+
except:
|
|
377
|
+
subnet_dict[field] = ""
|
|
378
|
+
|
|
379
|
+
# Convert state enum to string if needed
|
|
380
|
+
state_value = subnet_dict.get("state")
|
|
381
|
+
if state_value and not isinstance(state_value, str):
|
|
382
|
+
if hasattr(state_value, "value"):
|
|
383
|
+
subnet_dict["state"] = state_value.value
|
|
384
|
+
elif hasattr(state_value, "name"):
|
|
385
|
+
subnet_dict["state"] = state_value.name
|
|
386
|
+
else:
|
|
387
|
+
subnet_dict["state"] = str(state_value)
|
|
388
|
+
elif not state_value:
|
|
389
|
+
subnet_dict["state"] = "Unknown"
|
|
390
|
+
|
|
391
|
+
# Filter by active_only if specified
|
|
392
|
+
if active_only:
|
|
393
|
+
state_str = subnet_dict.get("state", "").lower()
|
|
394
|
+
if state_str != "active":
|
|
395
|
+
continue # Skip non-active subnets
|
|
396
|
+
|
|
397
|
+
all_subnets_data.append(subnet_dict)
|
|
398
|
+
|
|
399
|
+
# Display the subnet list
|
|
400
|
+
display_subnet_list(all_subnets_data)
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
# Use specific subnet error handling
|
|
404
|
+
if "subnet" in str(e).lower() or "network" in str(e).lower():
|
|
405
|
+
error = handle_substrate_error(e)
|
|
406
|
+
if not isinstance(
|
|
407
|
+
error, (SubnetError, SubnetActivationError, SubnetRegistrationError)
|
|
408
|
+
):
|
|
409
|
+
error = SubnetError(f"Failed to list subnets: {str(e)}")
|
|
410
|
+
error.display()
|
|
411
|
+
else:
|
|
412
|
+
handle_and_display_error(e)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def get_subnet_handler(subnet_id: Optional[int]):
|
|
416
|
+
"""Handle getting subnet information with proper error handling."""
|
|
417
|
+
try:
|
|
418
|
+
if subnet_id is None:
|
|
419
|
+
subnet_id = prompt_get_subnet()
|
|
420
|
+
|
|
421
|
+
if subnet_id < 0:
|
|
422
|
+
raise ValueError("Subnet ID must be a non-negative integer")
|
|
423
|
+
|
|
424
|
+
client = get_client()
|
|
425
|
+
with HTCLILoadingContext(f"Fetching data for subnet {subnet_id}..."):
|
|
426
|
+
result = client.rpc.subnet.get_subnet_data(subnet_id)
|
|
427
|
+
display_subnet_info(result.data)
|
|
428
|
+
except ValueError as e:
|
|
429
|
+
# Handle validation errors
|
|
430
|
+
from ...ui.display import print_error
|
|
431
|
+
|
|
432
|
+
print_error(f"Invalid subnet ID: {str(e)}")
|
|
433
|
+
except Exception as e:
|
|
434
|
+
# Use specific subnet error handling
|
|
435
|
+
if "subnet" in str(e).lower() or "not found" in str(e).lower():
|
|
436
|
+
error = handle_substrate_error(e)
|
|
437
|
+
if not isinstance(
|
|
438
|
+
error, (SubnetError, SubnetActivationError, SubnetRegistrationError)
|
|
439
|
+
):
|
|
440
|
+
error = SubnetError(f"Failed to get subnet {subnet_id}: {str(e)}")
|
|
441
|
+
error.display()
|
|
442
|
+
else:
|
|
443
|
+
handle_and_display_error(e)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def list_nodes_handler(subnet_id: Optional[int]):
|
|
447
|
+
try:
|
|
448
|
+
if subnet_id is None:
|
|
449
|
+
subnet_id = prompt_list_nodes()
|
|
450
|
+
client = get_client()
|
|
451
|
+
with HTCLILoadingContext(f"Fetching nodes for subnet {subnet_id}..."):
|
|
452
|
+
result = client.rpc.subnet.get_subnet_nodes(subnet_id)
|
|
453
|
+
display_subnet_nodes(result["data"])
|
|
454
|
+
except Exception as e:
|
|
455
|
+
handle_and_display_error(e)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def check_activation_handler(subnet_id: Optional[int]):
|
|
459
|
+
try:
|
|
460
|
+
if subnet_id is None:
|
|
461
|
+
subnet_id = prompt_check_activation()
|
|
462
|
+
client = get_client()
|
|
463
|
+
with HTCLILoadingContext(f"Checking activation for subnet {subnet_id}..."):
|
|
464
|
+
result = client.rpc.subnet.check_subnet_activation_requirements(subnet_id)
|
|
465
|
+
display_activation_requirements(result["data"])
|
|
466
|
+
except Exception as e:
|
|
467
|
+
handle_and_display_error(e)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# Write Handlers
|
|
471
|
+
def register_subnet_handler(
|
|
472
|
+
coldkey: Optional[str] = None,
|
|
473
|
+
name: Optional[str] = None,
|
|
474
|
+
repo: Optional[str] = None,
|
|
475
|
+
description: Optional[str] = None,
|
|
476
|
+
misc: Optional[str] = None,
|
|
477
|
+
min_stake: Optional[float] = None,
|
|
478
|
+
max_stake: Optional[float] = None,
|
|
479
|
+
max_cost: Optional[float] = None,
|
|
480
|
+
delegate_stake_percentage: Optional[int] = None,
|
|
481
|
+
initial_coldkeys: Optional[str] = None,
|
|
482
|
+
key_types: Optional[str] = None,
|
|
483
|
+
bootnodes: Optional[str] = None,
|
|
484
|
+
):
|
|
485
|
+
"""Handle subnet registration with simplified flow."""
|
|
486
|
+
try:
|
|
487
|
+
# Collect missing parameters interactively
|
|
488
|
+
try:
|
|
489
|
+
# Note: Fields like churn_limit, queue_epochs, max_registered_nodes, etc.
|
|
490
|
+
# are NOT part of RegistrationSubnetData and are ignored during registration.
|
|
491
|
+
# They can be set separately after registration using owner update commands.
|
|
492
|
+
request = prompt_register_subnet(
|
|
493
|
+
name=name,
|
|
494
|
+
repo=repo,
|
|
495
|
+
description=description,
|
|
496
|
+
misc=misc,
|
|
497
|
+
min_stake=min_stake,
|
|
498
|
+
max_stake=max_stake,
|
|
499
|
+
max_cost=max_cost,
|
|
500
|
+
delegate_stake_percentage=delegate_stake_percentage,
|
|
501
|
+
initial_coldkeys=initial_coldkeys,
|
|
502
|
+
key_types=key_types,
|
|
503
|
+
bootnodes=bootnodes,
|
|
504
|
+
)
|
|
505
|
+
except (KeyboardInterrupt, typer.Abort):
|
|
506
|
+
# Re-raise cancellation exceptions to properly exit
|
|
507
|
+
raise
|
|
508
|
+
except Exception as ve:
|
|
509
|
+
# Handle Pydantic ValidationError specifically (must check before ValueError)
|
|
510
|
+
from pydantic import ValidationError
|
|
511
|
+
|
|
512
|
+
from ...errors.handlers import (
|
|
513
|
+
display_pydantic_validation_error,
|
|
514
|
+
get_validation_suggestions,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if isinstance(ve, ValidationError):
|
|
518
|
+
suggestions = get_validation_suggestions(ve)
|
|
519
|
+
display_pydantic_validation_error(
|
|
520
|
+
ve,
|
|
521
|
+
model_name="SubnetRegisterRequest",
|
|
522
|
+
suggestions=suggestions,
|
|
523
|
+
)
|
|
524
|
+
return
|
|
525
|
+
# Handle other ValueError exceptions
|
|
526
|
+
elif isinstance(ve, ValueError):
|
|
527
|
+
from ...ui.display import print_error
|
|
528
|
+
|
|
529
|
+
print_error(f"Invalid input: {str(ve)}")
|
|
530
|
+
return
|
|
531
|
+
# Re-raise if it's not a handled exception
|
|
532
|
+
raise
|
|
533
|
+
|
|
534
|
+
# Get client and check connection status
|
|
535
|
+
from ...ui.display import print_error, print_info
|
|
536
|
+
|
|
537
|
+
client = get_client()
|
|
538
|
+
|
|
539
|
+
# Check if substrate connection is available
|
|
540
|
+
if not client.substrate:
|
|
541
|
+
print_error("Blockchain connection failed!")
|
|
542
|
+
raise RuntimeError("Not connected to blockchain")
|
|
543
|
+
|
|
544
|
+
# Test basic connectivity
|
|
545
|
+
try:
|
|
546
|
+
_ = client.substrate.properties
|
|
547
|
+
except Exception as conn_error:
|
|
548
|
+
print_error(f"Blockchain connectivity failed: {str(conn_error)}")
|
|
549
|
+
raise RuntimeError(
|
|
550
|
+
f"Blockchain connectivity failed: {str(conn_error)}"
|
|
551
|
+
) from conn_error
|
|
552
|
+
|
|
553
|
+
# Get keypair using provided coldkey or prompt for it
|
|
554
|
+
if coldkey:
|
|
555
|
+
# Resolve coldkey from provided input (address or wallet name)
|
|
556
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
560
|
+
except ValueError as e:
|
|
561
|
+
print_error(f"Failed to resolve coldkey '{coldkey}': {str(e)}")
|
|
562
|
+
print_error("Please provide a valid coldkey wallet name or address.")
|
|
563
|
+
raise
|
|
564
|
+
else:
|
|
565
|
+
# Fall back to interactive prompting
|
|
566
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
567
|
+
wallet_type="coldkey",
|
|
568
|
+
purpose="sign the transaction",
|
|
569
|
+
only_existing_wallets=True,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
from ...ui.display import HTCLIConsole
|
|
573
|
+
|
|
574
|
+
console = HTCLIConsole()
|
|
575
|
+
|
|
576
|
+
# Query current registration cost
|
|
577
|
+
print_info("💰 Querying current subnet registration cost...")
|
|
578
|
+
current_cost = client.rpc.chain.get_subnet_registration_cost()
|
|
579
|
+
|
|
580
|
+
if current_cost:
|
|
581
|
+
cost_tensor = current_cost / 1e18
|
|
582
|
+
console.print("")
|
|
583
|
+
console.print("[bold yellow]" + "=" * 70 + "[/]")
|
|
584
|
+
console.print("[bold yellow]⚠️ SUBNET REGISTRATION COST NOTICE[/]")
|
|
585
|
+
console.print("[bold yellow]" + "=" * 70 + "[/]")
|
|
586
|
+
console.print(
|
|
587
|
+
f"[htcli.warning]Current registration cost: [bold]{cost_tensor:,.2f} TENSOR[/bold][/]"
|
|
588
|
+
)
|
|
589
|
+
console.print(
|
|
590
|
+
f"[htcli.info]Your max_cost: {request.max_cost / 1e18:,.2f} TENSOR[/]"
|
|
591
|
+
)
|
|
592
|
+
console.print("")
|
|
593
|
+
|
|
594
|
+
if request.max_cost < current_cost:
|
|
595
|
+
console.print(
|
|
596
|
+
"[bold red]❌ ERROR: Your max_cost is LOWER than current cost![/]"
|
|
597
|
+
)
|
|
598
|
+
console.print(f"[htcli.error]Required: {cost_tensor:,.2f} TENSOR[/]")
|
|
599
|
+
console.print(
|
|
600
|
+
f"[htcli.error]Your max: {request.max_cost / 1e18:,.2f} TENSOR[/]"
|
|
601
|
+
)
|
|
602
|
+
console.print("")
|
|
603
|
+
console.print("[htcli.info]💡 To proceed, increase --max-cost:[/]")
|
|
604
|
+
console.print(f"[htcli.info] --max-cost {int(current_cost * 1.1)}[/]")
|
|
605
|
+
console.print("[bold yellow]" + "=" * 70 + "[/]")
|
|
606
|
+
print_error("Registration aborted due to insufficient max_cost")
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
console.print("[htcli.success]✅ Your max_cost is sufficient[/]")
|
|
610
|
+
console.print(f"[htcli.info]Expected cost: ~{cost_tensor:,.2f} TENSOR[/]")
|
|
611
|
+
console.print(
|
|
612
|
+
f"[htcli.info]Buffer: {((request.max_cost - current_cost) / 1e18):,.2f} TENSOR[/]"
|
|
613
|
+
)
|
|
614
|
+
console.print("[bold yellow]" + "=" * 70 + "[/]")
|
|
615
|
+
console.print("")
|
|
616
|
+
|
|
617
|
+
# Check balance using wallet's stored address (not keypair's derived address)
|
|
618
|
+
# NOTE: Commented out to let the chain return its own error
|
|
619
|
+
# user_balance = client.rpc.chain.get_account_balance(wallet_address)
|
|
620
|
+
# if user_balance is not None:
|
|
621
|
+
# balance_tensor = user_balance / 1e18
|
|
622
|
+
# console.print(
|
|
623
|
+
# f"[htcli.info]Your balance: {balance_tensor:,.2f} TENSOR[/]"
|
|
624
|
+
# )
|
|
625
|
+
# if user_balance < current_cost:
|
|
626
|
+
# console.print("[bold red]❌ INSUFFICIENT BALANCE![/]")
|
|
627
|
+
# console.print(
|
|
628
|
+
# f"[htcli.error]Required: {cost_tensor:,.2f} TENSOR[/]"
|
|
629
|
+
# )
|
|
630
|
+
# console.print(
|
|
631
|
+
# f"[htcli.error]Your balance: {balance_tensor:,.2f} TENSOR[/]"
|
|
632
|
+
# )
|
|
633
|
+
# console.print(
|
|
634
|
+
# f"[htcli.error]Shortfall: {(cost_tensor - balance_tensor):,.2f} TENSOR[/]"
|
|
635
|
+
# )
|
|
636
|
+
# console.print("")
|
|
637
|
+
# console.print("[htcli.info]💡 To proceed with registration:[/htcli.info]")
|
|
638
|
+
# console.print(f"[htcli.info] • Transfer at least {(cost_tensor - balance_tensor):,.2f} TENSOR to this wallet[/htcli.info]")
|
|
639
|
+
# console.print(f"[htcli.info] • Or use a different wallet with sufficient balance[/htcli.info]")
|
|
640
|
+
# return
|
|
641
|
+
# console.print(
|
|
642
|
+
# "[htcli.success]✅ Sufficient balance for registration[/]"
|
|
643
|
+
# )
|
|
644
|
+
# console.print("")
|
|
645
|
+
|
|
646
|
+
# Show registration summary before submitting
|
|
647
|
+
from ...ui.components import HTCLIPanel
|
|
648
|
+
|
|
649
|
+
console = HTCLIConsole()
|
|
650
|
+
|
|
651
|
+
# Format initial coldkeys info with max nodes per coldkey
|
|
652
|
+
initial_coldkeys_info = ""
|
|
653
|
+
if request.initial_coldkeys:
|
|
654
|
+
coldkey_count = len(request.initial_coldkeys)
|
|
655
|
+
total_max_nodes = sum(request.initial_coldkeys.values())
|
|
656
|
+
|
|
657
|
+
# Show summary: count and total max nodes
|
|
658
|
+
initial_coldkeys_info = f"{coldkey_count} coldkey(s)"
|
|
659
|
+
|
|
660
|
+
# If all have same value (default case), show simplified
|
|
661
|
+
unique_values = set(request.initial_coldkeys.values())
|
|
662
|
+
if len(unique_values) == 1:
|
|
663
|
+
max_nodes = list(unique_values)[0]
|
|
664
|
+
initial_coldkeys_info += f" (each can register up to {max_nodes} node{'s' if max_nodes > 1 else ''})"
|
|
665
|
+
else:
|
|
666
|
+
# Show range or detailed breakdown
|
|
667
|
+
min_nodes = min(request.initial_coldkeys.values())
|
|
668
|
+
max_nodes = max(request.initial_coldkeys.values())
|
|
669
|
+
if min_nodes == max_nodes:
|
|
670
|
+
initial_coldkeys_info += f" (each can register up to {max_nodes} node{'s' if max_nodes > 1 else ''})"
|
|
671
|
+
else:
|
|
672
|
+
initial_coldkeys_info += f" (can register {min_nodes}-{max_nodes} nodes each, total: {total_max_nodes} max nodes)"
|
|
673
|
+
|
|
674
|
+
initial_coldkeys_info += " (becomes permissionless after activation)"
|
|
675
|
+
else:
|
|
676
|
+
initial_coldkeys_info = "0 (becomes permissionless after activation)"
|
|
677
|
+
|
|
678
|
+
summary = f"""[htcli.accent]Subnet Registration Summary[/htcli.accent]
|
|
679
|
+
|
|
680
|
+
[htcli.value]Name:[/htcli.value] {request.name}
|
|
681
|
+
[htcli.value]Repository:[/htcli.value] {request.repo}
|
|
682
|
+
[htcli.value]Min Stake:[/htcli.value] {request.min_stake / 1e18:,.2f} TENSOR
|
|
683
|
+
[htcli.value]Max Stake:[/htcli.value] {request.max_stake / 1e18:,.2f} TENSOR
|
|
684
|
+
[htcli.value]Max Cost:[/htcli.value] {request.max_cost / 1e18:,.2f} TENSOR
|
|
685
|
+
[htcli.value]Initial Coldkeys:[/htcli.value] {initial_coldkeys_info}
|
|
686
|
+
|
|
687
|
+
[htcli.info]💡 Submitting to blockchain...[/htcli.info]
|
|
688
|
+
[htcli.info]💡 Note: Configuration fields (churn_limit, queue_epochs, etc.) can be set after registration using owner update commands.[/htcli.info]
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
panel = HTCLIPanel(
|
|
692
|
+
summary,
|
|
693
|
+
title="🚀 Submitting Subnet Registration",
|
|
694
|
+
border_style="htcli.info",
|
|
695
|
+
highlight=True,
|
|
696
|
+
)
|
|
697
|
+
panel.render(console.console)
|
|
698
|
+
console.print("")
|
|
699
|
+
|
|
700
|
+
with HTCLILoadingContext("Registering subnet on blockchain..."):
|
|
701
|
+
result = client.extrinsics.subnet.register_subnet(request, keypair=keypair)
|
|
702
|
+
|
|
703
|
+
# Display result with clear messages (not JSON)
|
|
704
|
+
if result.success:
|
|
705
|
+
resolved_block_number = getattr(result, "block_number", None)
|
|
706
|
+
block_hash = getattr(result, "block_hash", None)
|
|
707
|
+
|
|
708
|
+
if not resolved_block_number and block_hash:
|
|
709
|
+
try:
|
|
710
|
+
resolved = client.rpc.chain.get_block_number(block_hash=block_hash)
|
|
711
|
+
if resolved is not None:
|
|
712
|
+
resolved_block_number = resolved
|
|
713
|
+
except Exception as fetch_error:
|
|
714
|
+
logger.debug(
|
|
715
|
+
f"Unable to resolve block number for hash {block_hash}: {fetch_error}"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
block_display = (
|
|
719
|
+
resolved_block_number if resolved_block_number else "Pending"
|
|
720
|
+
)
|
|
721
|
+
block_hash_display = block_hash or "N/A"
|
|
722
|
+
|
|
723
|
+
success_msg = f"""[htcli.success]✅ Subnet registered successfully![/htcli.success]
|
|
724
|
+
|
|
725
|
+
[htcli.value]Subnet ID:[/htcli.value] {result.subnet_id if hasattr(result, "subnet_id") else "Pending"}
|
|
726
|
+
[htcli.value]Transaction:[/htcli.value] {result.transaction_hash if hasattr(result, "transaction_hash") else "N/A"}
|
|
727
|
+
[htcli.value]Block:[/htcli.value] {block_display}
|
|
728
|
+
[htcli.value]Block Hash:[/htcli.value] {block_hash_display}
|
|
729
|
+
|
|
730
|
+
[htcli.info]💡 Your subnet is now registered! You can now register nodes.[/htcli.info]
|
|
731
|
+
"""
|
|
732
|
+
success_panel = HTCLIPanel(
|
|
733
|
+
success_msg,
|
|
734
|
+
title="🎉 Registration Complete",
|
|
735
|
+
border_style="htcli.success",
|
|
736
|
+
highlight=True,
|
|
737
|
+
)
|
|
738
|
+
success_panel.render(console.console)
|
|
739
|
+
else:
|
|
740
|
+
# Format error message beautifully like success message
|
|
741
|
+
error_msg = result.error if hasattr(result, "error") else "Unknown error"
|
|
742
|
+
|
|
743
|
+
# Parse blockchain error name for better display
|
|
744
|
+
error_name = "Registration Failed"
|
|
745
|
+
error_description = error_msg
|
|
746
|
+
if ":" in error_msg:
|
|
747
|
+
parts = error_msg.split(":", 1)
|
|
748
|
+
error_name = parts[0].strip()
|
|
749
|
+
error_description = parts[1].strip()
|
|
750
|
+
|
|
751
|
+
# Get helpful tips based on error type - COMPREHENSIVE ERROR COVERAGE
|
|
752
|
+
tips = []
|
|
753
|
+
|
|
754
|
+
# Balance and cost errors
|
|
755
|
+
if "NotEnoughBalanceToRegisterSubnet" in error_msg:
|
|
756
|
+
tips = [
|
|
757
|
+
f"Your wallet needs approximately {request.max_cost / 1e18:,.2f} TENSOR",
|
|
758
|
+
"Transfer more TENSOR to your wallet before retrying",
|
|
759
|
+
"Or use a different wallet with sufficient balance",
|
|
760
|
+
]
|
|
761
|
+
elif "CostGreaterThanMaxCost" in error_msg:
|
|
762
|
+
tips = [
|
|
763
|
+
"The registration cost exceeds your specified max_cost",
|
|
764
|
+
"Increase your --max-cost parameter (e.g., double it)",
|
|
765
|
+
"Registration costs increase as more subnets are created",
|
|
766
|
+
]
|
|
767
|
+
|
|
768
|
+
# Stake validation errors
|
|
769
|
+
elif "InvalidSubnetMinStake" in error_msg:
|
|
770
|
+
tips = [
|
|
771
|
+
"min_stake must be between 100-250 TENSOR",
|
|
772
|
+
f"Current value: {request.min_stake / 1e18:,.2f} TENSOR",
|
|
773
|
+
"Use --min-stake flag with a value in this range",
|
|
774
|
+
]
|
|
775
|
+
elif "InvalidSubnetMaxStake" in error_msg:
|
|
776
|
+
tips = [
|
|
777
|
+
"max_stake must be ≤ 1000 TENSOR (NetworkMaxStakeBalance)",
|
|
778
|
+
f"Current value: {request.max_stake / 1e18:,.2f} TENSOR",
|
|
779
|
+
"Use --max-stake flag with a lower value",
|
|
780
|
+
]
|
|
781
|
+
elif "InvalidSubnetStakeParameters" in error_msg:
|
|
782
|
+
tips = [
|
|
783
|
+
"min_stake must be less than or equal to max_stake",
|
|
784
|
+
f"Current min_stake: {request.min_stake / 1e18:,.2f} TENSOR",
|
|
785
|
+
f"Current max_stake: {request.max_stake / 1e18:,.2f} TENSOR",
|
|
786
|
+
"Adjust your stake parameters to satisfy: min_stake ≤ max_stake",
|
|
787
|
+
]
|
|
788
|
+
|
|
789
|
+
# Delegate percentage errors
|
|
790
|
+
elif (
|
|
791
|
+
"InvalidMinDelegateStakePercentage" in error_msg
|
|
792
|
+
or "InvalidDelegateStakePercentage" in error_msg
|
|
793
|
+
):
|
|
794
|
+
tips = [
|
|
795
|
+
"delegate_percentage must be between 5% and 95%",
|
|
796
|
+
"Use --delegate-percentage flag (e.g., 20 for 20%)",
|
|
797
|
+
"This controls how much of emissions go to delegate stakers",
|
|
798
|
+
]
|
|
799
|
+
|
|
800
|
+
# Node configuration errors
|
|
801
|
+
elif "InvalidMaxRegisteredNodes" in error_msg:
|
|
802
|
+
tips = [
|
|
803
|
+
"max_registered_nodes must be between 1 and 64",
|
|
804
|
+
f"Current value: {request.max_registered_nodes}",
|
|
805
|
+
"Blockchain enforces MaxMaxRegisteredNodes = 64",
|
|
806
|
+
]
|
|
807
|
+
elif "InvalidMaxSubnetNodePenalties" in error_msg:
|
|
808
|
+
tips = [
|
|
809
|
+
"max_node_penalties must be within the allowed range",
|
|
810
|
+
"Check MinMaxSubnetNodePenalties and MaxMaxSubnetNodePenalties",
|
|
811
|
+
"Adjust --max-node-penalties parameter",
|
|
812
|
+
]
|
|
813
|
+
|
|
814
|
+
# Bootnode errors
|
|
815
|
+
elif "BootnodesEmpty" in error_msg:
|
|
816
|
+
tips = [
|
|
817
|
+
"At least one bootnode is required for subnet registration",
|
|
818
|
+
"Use --bootnodes flag with valid multiaddr format",
|
|
819
|
+
"Example: /dns4/bootnode.example.com/tcp/30333/p2p/12D3KooW...",
|
|
820
|
+
]
|
|
821
|
+
elif "TooManyBootnodes" in error_msg:
|
|
822
|
+
tips = [
|
|
823
|
+
"Too many bootnodes provided",
|
|
824
|
+
"Maximum allowed bootnodes: 32 (MaxBootnodes)",
|
|
825
|
+
"Remove some bootnode entries and retry",
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
# Initial coldkeys errors
|
|
829
|
+
elif "InvalidSubnetRegistrationInitialColdkeys" in error_msg:
|
|
830
|
+
tips = [
|
|
831
|
+
"Initial coldkeys validation failed",
|
|
832
|
+
"Must provide at least 3 coldkeys (MinSubnetNodes = 3)",
|
|
833
|
+
"Each coldkey must have at least 1 registration slot",
|
|
834
|
+
"After subnet activation, it becomes permissionless automatically",
|
|
835
|
+
"The whitelist is removed and anyone can register nodes",
|
|
836
|
+
]
|
|
837
|
+
|
|
838
|
+
# Epoch configuration errors
|
|
839
|
+
elif "InvalidChurnLimit" in error_msg:
|
|
840
|
+
tips = [
|
|
841
|
+
"churn_limit is outside the allowed range",
|
|
842
|
+
"Must be between MinChurnLimit and MaxChurnLimit",
|
|
843
|
+
"Adjust --churn-limit parameter",
|
|
844
|
+
]
|
|
845
|
+
elif "InvalidRegistrationQueueEpochs" in error_msg:
|
|
846
|
+
tips = [
|
|
847
|
+
"subnet_node_queue_epochs is outside the allowed range",
|
|
848
|
+
"Must be between MinQueueEpochs and MaxQueueEpochs",
|
|
849
|
+
"Adjust --registration-queue-epochs parameter",
|
|
850
|
+
]
|
|
851
|
+
elif "InvalidIdleClassificationEpochs" in error_msg:
|
|
852
|
+
tips = [
|
|
853
|
+
"idle_classification_epochs is outside the allowed range",
|
|
854
|
+
"Must be between MinIdleClassificationEpochs and MaxIdleClassificationEpochs",
|
|
855
|
+
"Adjust --activation-grace-epochs parameter",
|
|
856
|
+
]
|
|
857
|
+
elif "InvalidIncludedClassificationEpochs" in error_msg:
|
|
858
|
+
tips = [
|
|
859
|
+
"included_classification_epochs is outside the allowed range",
|
|
860
|
+
"Must be between MinIncludedClassificationEpochs and MaxIncludedClassificationEpochs",
|
|
861
|
+
"Adjust --included-classification-epochs parameter",
|
|
862
|
+
]
|
|
863
|
+
|
|
864
|
+
# Uniqueness constraint errors
|
|
865
|
+
elif "SubnetNameExist" in error_msg:
|
|
866
|
+
tips = [
|
|
867
|
+
"This subnet name is already taken by another subnet",
|
|
868
|
+
"Choose a unique name for your subnet",
|
|
869
|
+
"Use --name flag with a different value",
|
|
870
|
+
]
|
|
871
|
+
elif "SubnetRepoExist" in error_msg:
|
|
872
|
+
tips = [
|
|
873
|
+
"This repository URL is already registered to another subnet",
|
|
874
|
+
"Use a different repository URL",
|
|
875
|
+
"Each subnet must have a unique repository",
|
|
876
|
+
]
|
|
877
|
+
|
|
878
|
+
# Capacity errors
|
|
879
|
+
elif "MaxSubnets" in error_msg:
|
|
880
|
+
tips = [
|
|
881
|
+
"Maximum number of subnets reached on the network",
|
|
882
|
+
"Wait for inactive subnets to be removed",
|
|
883
|
+
"Inactive subnets are periodically cleaned up by the protocol",
|
|
884
|
+
]
|
|
885
|
+
|
|
886
|
+
# Generic fallback
|
|
887
|
+
else:
|
|
888
|
+
tips = [
|
|
889
|
+
"Check all parameter values are within valid ranges",
|
|
890
|
+
"Ensure wallet has sufficient balance",
|
|
891
|
+
"Verify network connection is stable",
|
|
892
|
+
"Review the documentation for parameter requirements",
|
|
893
|
+
]
|
|
894
|
+
|
|
895
|
+
# Build formatted error message
|
|
896
|
+
tips_text = "\n".join([f" • {tip}" for tip in tips])
|
|
897
|
+
|
|
898
|
+
error_display = f"""[htcli.error]❌ {error_name}[/htcli.error]
|
|
899
|
+
|
|
900
|
+
[htcli.value]Error:[/htcli.value] {error_description}
|
|
901
|
+
|
|
902
|
+
[htcli.info]💡 How to fix this:[/htcli.info]
|
|
903
|
+
{tips_text}
|
|
904
|
+
"""
|
|
905
|
+
|
|
906
|
+
error_panel = HTCLIPanel(
|
|
907
|
+
error_display,
|
|
908
|
+
title="⚠️ Subnet Registration Failed",
|
|
909
|
+
border_style="htcli.error",
|
|
910
|
+
highlight=True,
|
|
911
|
+
)
|
|
912
|
+
error_panel.render(console.console)
|
|
913
|
+
console.print("")
|
|
914
|
+
except ValueError as e:
|
|
915
|
+
# Handle validation errors - provide more specific error messages
|
|
916
|
+
error_msg = str(e)
|
|
917
|
+
if "incompatible private key format" in error_msg:
|
|
918
|
+
from ...ui.display import print_error
|
|
919
|
+
|
|
920
|
+
print_error(f"Wallet format error: {error_msg}")
|
|
921
|
+
print_error(
|
|
922
|
+
"Please recreate your wallet or use a different wallet with compatible format."
|
|
923
|
+
)
|
|
924
|
+
else:
|
|
925
|
+
from ...ui.display import print_error
|
|
926
|
+
|
|
927
|
+
print_error(f"Validation error: {error_msg}")
|
|
928
|
+
except (KeyboardInterrupt, typer.Abort):
|
|
929
|
+
# Re-raise cancellation exceptions to properly exit
|
|
930
|
+
raise
|
|
931
|
+
except RuntimeError as e:
|
|
932
|
+
# Handle connection errors
|
|
933
|
+
from ...ui.display import print_error
|
|
934
|
+
|
|
935
|
+
print_error(f"Connection Error: {str(e)}")
|
|
936
|
+
except Exception as e:
|
|
937
|
+
# Check for wallet-related errors first and provide better context
|
|
938
|
+
error_str = str(e).lower()
|
|
939
|
+
if any(
|
|
940
|
+
pattern in error_str
|
|
941
|
+
for pattern in [
|
|
942
|
+
"wallet",
|
|
943
|
+
"key",
|
|
944
|
+
"signature",
|
|
945
|
+
"password",
|
|
946
|
+
"coldkey",
|
|
947
|
+
"hotkey",
|
|
948
|
+
]
|
|
949
|
+
):
|
|
950
|
+
from ...errors.handlers import handle_wallet_error
|
|
951
|
+
from ...ui.display import print_error
|
|
952
|
+
|
|
953
|
+
print_error(f"Wallet error: {str(e)}")
|
|
954
|
+
wallet_error = handle_wallet_error(e, operation="subnet registration")
|
|
955
|
+
wallet_error.display()
|
|
956
|
+
return
|
|
957
|
+
|
|
958
|
+
# Use specific subnet registration error handling
|
|
959
|
+
if "registration" in error_str or "subnet" in error_str:
|
|
960
|
+
error = handle_substrate_error(e)
|
|
961
|
+
if not isinstance(error, SubnetRegistrationError):
|
|
962
|
+
error = SubnetRegistrationError(f"Failed to register subnet: {str(e)}")
|
|
963
|
+
error.display()
|
|
964
|
+
else:
|
|
965
|
+
handle_and_display_error(e)
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def activate_subnet_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
|
|
969
|
+
"""Handle subnet activation with proper error handling."""
|
|
970
|
+
result_dict = None # Initialize to avoid UnboundLocalError
|
|
971
|
+
try:
|
|
972
|
+
if subnet_id is None:
|
|
973
|
+
subnet_id = prompt_activate_subnet()
|
|
974
|
+
|
|
975
|
+
if subnet_id < 0:
|
|
976
|
+
raise ValueError("Subnet ID must be a non-negative integer")
|
|
977
|
+
|
|
978
|
+
# Get signing wallet using provided coldkey or prompt for it
|
|
979
|
+
if coldkey:
|
|
980
|
+
# Resolve coldkey from provided input (address or wallet name)
|
|
981
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
982
|
+
|
|
983
|
+
try:
|
|
984
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
985
|
+
except ValueError as e:
|
|
986
|
+
from ...ui.display import print_error
|
|
987
|
+
|
|
988
|
+
print_error(f"Failed to resolve coldkey '{coldkey}': {str(e)}")
|
|
989
|
+
print_error("Please provide a valid coldkey wallet name or address.")
|
|
990
|
+
raise
|
|
991
|
+
else:
|
|
992
|
+
# Fall back to interactive prompting
|
|
993
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
994
|
+
wallet_type="coldkey", purpose="sign the activation transaction"
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
client = get_client()
|
|
998
|
+
|
|
999
|
+
# Ensure connection is established
|
|
1000
|
+
if not client.connect():
|
|
1001
|
+
from ...ui.display import print_error
|
|
1002
|
+
|
|
1003
|
+
print_error("Failed to connect to blockchain")
|
|
1004
|
+
return
|
|
1005
|
+
|
|
1006
|
+
# Check if substrate connection is available
|
|
1007
|
+
if not client.substrate:
|
|
1008
|
+
from ...ui.display import print_error
|
|
1009
|
+
|
|
1010
|
+
print_error("Blockchain connection failed!")
|
|
1011
|
+
raise RuntimeError("Not connected to blockchain")
|
|
1012
|
+
|
|
1013
|
+
# Check if extrinsics layer is initialized
|
|
1014
|
+
if not client.extrinsics:
|
|
1015
|
+
from ...ui.display import print_error
|
|
1016
|
+
|
|
1017
|
+
print_error("Extrinsics layer not initialized. Connection may have failed.")
|
|
1018
|
+
raise RuntimeError("Extrinsics layer not available")
|
|
1019
|
+
|
|
1020
|
+
from ...models.requests.subnet import SubnetActivateRequest
|
|
1021
|
+
|
|
1022
|
+
request = SubnetActivateRequest(subnet_id=subnet_id)
|
|
1023
|
+
|
|
1024
|
+
with HTCLILoadingContext(f"Activating subnet {subnet_id}..."):
|
|
1025
|
+
result = client.extrinsics.subnet.activate_subnet(request, keypair=keypair)
|
|
1026
|
+
|
|
1027
|
+
# Convert SimpleExtrinsicResponse to dict for display
|
|
1028
|
+
result_dict = {
|
|
1029
|
+
"message": result.message or f"Subnet {subnet_id} activated successfully",
|
|
1030
|
+
"transaction_hash": result.transaction_hash,
|
|
1031
|
+
"block_number": result.block_number,
|
|
1032
|
+
"block_hash": result.block_hash,
|
|
1033
|
+
"success": result.success,
|
|
1034
|
+
}
|
|
1035
|
+
if result.error:
|
|
1036
|
+
result_dict["error"] = result.error
|
|
1037
|
+
|
|
1038
|
+
if not result.success:
|
|
1039
|
+
error_msg = result_dict.get("error", "Activation failed")
|
|
1040
|
+
|
|
1041
|
+
# Handle specific activation errors with helpful messages
|
|
1042
|
+
from ...ui.components import HTCLIPanel
|
|
1043
|
+
from ...ui.display import HTCLIConsole, print_error, print_info
|
|
1044
|
+
|
|
1045
|
+
console = HTCLIConsole()
|
|
1046
|
+
|
|
1047
|
+
if "NotSubnetOwner" in error_msg:
|
|
1048
|
+
error_panel = HTCLIPanel(
|
|
1049
|
+
f"Only the subnet owner can activate subnet {subnet_id}.\n\n"
|
|
1050
|
+
f"💡 Make sure you're using the coldkey wallet that registered this subnet.",
|
|
1051
|
+
title="❌ Permission Denied",
|
|
1052
|
+
border_style="htcli.error",
|
|
1053
|
+
highlight=True,
|
|
1054
|
+
)
|
|
1055
|
+
error_panel.render(console.console)
|
|
1056
|
+
return
|
|
1057
|
+
elif "InvalidSubnetId" in error_msg:
|
|
1058
|
+
error_panel = HTCLIPanel(
|
|
1059
|
+
f"Subnet {subnet_id} does not exist.\n\n"
|
|
1060
|
+
f"💡 Check the subnet ID and try again.\n"
|
|
1061
|
+
f" Use: htcli subnet list to see available subnets",
|
|
1062
|
+
title="❌ Invalid Subnet ID",
|
|
1063
|
+
border_style="htcli.error",
|
|
1064
|
+
highlight=True,
|
|
1065
|
+
)
|
|
1066
|
+
error_panel.render(console.console)
|
|
1067
|
+
return
|
|
1068
|
+
elif "SubnetActivatedAlready" in error_msg:
|
|
1069
|
+
error_panel = HTCLIPanel(
|
|
1070
|
+
f"Subnet {subnet_id} is already activated.\n\n"
|
|
1071
|
+
f"💡 The subnet is already in active state.\n"
|
|
1072
|
+
f" Use: htcli subnet info --subnet-id {subnet_id} to check subnet status",
|
|
1073
|
+
title="✅ Already Activated",
|
|
1074
|
+
border_style="htcli.info",
|
|
1075
|
+
highlight=True,
|
|
1076
|
+
)
|
|
1077
|
+
error_panel.render(console.console)
|
|
1078
|
+
return
|
|
1079
|
+
elif "MinSubnetRegistrationEpochsNotMet" in error_msg:
|
|
1080
|
+
error_panel = HTCLIPanel(
|
|
1081
|
+
f"Subnet {subnet_id} cannot be activated yet.\n\n"
|
|
1082
|
+
f"⏱️ Minimum registration epochs not met.\n"
|
|
1083
|
+
f" Subnets must wait a minimum number of epochs after registration before activation.\n\n"
|
|
1084
|
+
f"💡 Wait a few more epochs and try again once the minimum has elapsed.",
|
|
1085
|
+
title="⏳ Too Early to Activate",
|
|
1086
|
+
border_style="htcli.warning",
|
|
1087
|
+
highlight=True,
|
|
1088
|
+
)
|
|
1089
|
+
error_panel.render(console.console)
|
|
1090
|
+
return
|
|
1091
|
+
elif "SubnetActivationConditionsNotMetYet" in error_msg:
|
|
1092
|
+
# Show error message and summarize requirements
|
|
1093
|
+
error_panel = HTCLIPanel(
|
|
1094
|
+
f"Subnet {subnet_id} cannot be activated yet because activation conditions are not met.",
|
|
1095
|
+
title="⚠️ Activation Conditions Not Met",
|
|
1096
|
+
border_style="htcli.error",
|
|
1097
|
+
highlight=True,
|
|
1098
|
+
)
|
|
1099
|
+
error_panel.render(console.console)
|
|
1100
|
+
console.print("")
|
|
1101
|
+
|
|
1102
|
+
requirements_panel = HTCLIPanel(
|
|
1103
|
+
ACTIVATION_REQUIREMENTS_TEXT,
|
|
1104
|
+
title="🧮 What You Need",
|
|
1105
|
+
border_style="htcli.info",
|
|
1106
|
+
highlight=True,
|
|
1107
|
+
)
|
|
1108
|
+
requirements_panel.render(console.console)
|
|
1109
|
+
|
|
1110
|
+
next_steps_panel = HTCLIPanel(
|
|
1111
|
+
ACTIVATION_NEXT_STEPS_TEXT,
|
|
1112
|
+
title="🚀 Getting Ready",
|
|
1113
|
+
border_style="htcli.info",
|
|
1114
|
+
highlight=True,
|
|
1115
|
+
)
|
|
1116
|
+
console.print("")
|
|
1117
|
+
next_steps_panel.render(console.console)
|
|
1118
|
+
return
|
|
1119
|
+
elif "SubnetDeactivated" in error_msg:
|
|
1120
|
+
# Subnet was removed during activation attempt (instead of being activated)
|
|
1121
|
+
# This happens when activation conditions aren't met during enactment period
|
|
1122
|
+
removal_reason = (
|
|
1123
|
+
getattr(result, "removal_reason", None) if result else None
|
|
1124
|
+
)
|
|
1125
|
+
reason_display = removal_reason or "activation conditions not met"
|
|
1126
|
+
|
|
1127
|
+
error_panel = HTCLIPanel(
|
|
1128
|
+
f"⚠️ Subnet {subnet_id} was **removed** instead of being activated.\n\n"
|
|
1129
|
+
f"📋 **Reason**: {reason_display}\n\n"
|
|
1130
|
+
f"This typically happens when:\n"
|
|
1131
|
+
f" • The subnet didn't meet minimum node requirements\n"
|
|
1132
|
+
f" • The subnet didn't have enough delegate stake\n"
|
|
1133
|
+
f" • The enactment period expired\n\n"
|
|
1134
|
+
f"💡 To try again, you'll need to register a new subnet and ensure\n"
|
|
1135
|
+
f" all activation requirements are met before the enactment deadline.",
|
|
1136
|
+
title="❌ Subnet Removed During Activation",
|
|
1137
|
+
border_style="htcli.error",
|
|
1138
|
+
highlight=True,
|
|
1139
|
+
)
|
|
1140
|
+
error_panel.render(console.console)
|
|
1141
|
+
return
|
|
1142
|
+
else:
|
|
1143
|
+
# Generic error - show the error message
|
|
1144
|
+
from ...ui.display import print_error
|
|
1145
|
+
|
|
1146
|
+
print_error(error_msg)
|
|
1147
|
+
return
|
|
1148
|
+
|
|
1149
|
+
display_generic_success(result_dict)
|
|
1150
|
+
except ValueError as e:
|
|
1151
|
+
# Handle validation errors
|
|
1152
|
+
from ...ui.display import print_error
|
|
1153
|
+
|
|
1154
|
+
print_error(f"Invalid subnet ID: {str(e)}")
|
|
1155
|
+
except Exception as e:
|
|
1156
|
+
# Use specific subnet activation error handling
|
|
1157
|
+
error_msg = str(e)
|
|
1158
|
+
|
|
1159
|
+
from ...ui.components import HTCLIPanel
|
|
1160
|
+
from ...ui.display import HTCLIConsole, print_error, print_info
|
|
1161
|
+
|
|
1162
|
+
console = HTCLIConsole()
|
|
1163
|
+
|
|
1164
|
+
# Handle specific activation errors with helpful messages
|
|
1165
|
+
if "NotSubnetOwner" in error_msg:
|
|
1166
|
+
error_panel = HTCLIPanel(
|
|
1167
|
+
f"Only the subnet owner can activate subnet {subnet_id}.\n\n"
|
|
1168
|
+
f"💡 Make sure you're using the coldkey wallet that registered this subnet.",
|
|
1169
|
+
title="❌ Permission Denied",
|
|
1170
|
+
border_style="htcli.error",
|
|
1171
|
+
highlight=True,
|
|
1172
|
+
)
|
|
1173
|
+
error_panel.render(console.console)
|
|
1174
|
+
return
|
|
1175
|
+
elif "InvalidSubnetId" in error_msg:
|
|
1176
|
+
error_panel = HTCLIPanel(
|
|
1177
|
+
f"Subnet {subnet_id} does not exist.\n\n"
|
|
1178
|
+
f"💡 Check the subnet ID and try again.\n"
|
|
1179
|
+
f" Use: htcli subnet list to see available subnets",
|
|
1180
|
+
title="❌ Invalid Subnet ID",
|
|
1181
|
+
border_style="htcli.error",
|
|
1182
|
+
highlight=True,
|
|
1183
|
+
)
|
|
1184
|
+
error_panel.render(console.console)
|
|
1185
|
+
return
|
|
1186
|
+
elif "SubnetActivatedAlready" in error_msg:
|
|
1187
|
+
error_panel = HTCLIPanel(
|
|
1188
|
+
f"Subnet {subnet_id} is already activated.\n\n"
|
|
1189
|
+
f"💡 The subnet is already in active state.\n"
|
|
1190
|
+
f" Use: htcli subnet info --subnet-id {subnet_id} to check subnet status",
|
|
1191
|
+
title="✅ Already Activated",
|
|
1192
|
+
border_style="htcli.info",
|
|
1193
|
+
highlight=True,
|
|
1194
|
+
)
|
|
1195
|
+
error_panel.render(console.console)
|
|
1196
|
+
return
|
|
1197
|
+
elif "MinSubnetRegistrationEpochsNotMet" in error_msg:
|
|
1198
|
+
error_panel = HTCLIPanel(
|
|
1199
|
+
f"Subnet {subnet_id} cannot be activated yet.\n\n"
|
|
1200
|
+
f"⏱️ Minimum registration epochs not met.\n"
|
|
1201
|
+
f" Subnets must wait a minimum number of epochs after registration before activation.\n\n"
|
|
1202
|
+
f"💡 Wait a few more epochs and try again once the minimum has elapsed.",
|
|
1203
|
+
title="⏳ Too Early to Activate",
|
|
1204
|
+
border_style="htcli.warning",
|
|
1205
|
+
highlight=True,
|
|
1206
|
+
)
|
|
1207
|
+
error_panel.render(console.console)
|
|
1208
|
+
return
|
|
1209
|
+
elif "SubnetActivationConditionsNotMetYet" in error_msg:
|
|
1210
|
+
# Show error message and summarize requirements
|
|
1211
|
+
error_panel = HTCLIPanel(
|
|
1212
|
+
f"Subnet {subnet_id} cannot be activated yet because activation conditions are not met.",
|
|
1213
|
+
title="⚠️ Activation Conditions Not Met",
|
|
1214
|
+
border_style="htcli.error",
|
|
1215
|
+
highlight=True,
|
|
1216
|
+
)
|
|
1217
|
+
error_panel.render(console.console)
|
|
1218
|
+
console.print("")
|
|
1219
|
+
|
|
1220
|
+
requirements_panel = HTCLIPanel(
|
|
1221
|
+
ACTIVATION_REQUIREMENTS_TEXT,
|
|
1222
|
+
title="🧮 What You Need",
|
|
1223
|
+
border_style="htcli.info",
|
|
1224
|
+
highlight=True,
|
|
1225
|
+
)
|
|
1226
|
+
requirements_panel.render(console.console)
|
|
1227
|
+
|
|
1228
|
+
next_steps_panel = HTCLIPanel(
|
|
1229
|
+
ACTIVATION_NEXT_STEPS_TEXT,
|
|
1230
|
+
title="🚀 Getting Ready",
|
|
1231
|
+
border_style="htcli.info",
|
|
1232
|
+
highlight=True,
|
|
1233
|
+
)
|
|
1234
|
+
console.print("")
|
|
1235
|
+
next_steps_panel.render(console.console)
|
|
1236
|
+
return
|
|
1237
|
+
else:
|
|
1238
|
+
# Generic error - show the error message
|
|
1239
|
+
from ...ui.display import print_error
|
|
1240
|
+
|
|
1241
|
+
print_error(error_msg)
|
|
1242
|
+
handle_and_display_error(e)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def pause_subnet_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
|
|
1246
|
+
try:
|
|
1247
|
+
if subnet_id is None:
|
|
1248
|
+
subnet_id = prompt_pause_subnet()
|
|
1249
|
+
|
|
1250
|
+
# Get signing wallet using comprehensive wallet retrieval mechanism
|
|
1251
|
+
if coldkey:
|
|
1252
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
1253
|
+
|
|
1254
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
1255
|
+
else:
|
|
1256
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
1257
|
+
wallet_type="coldkey",
|
|
1258
|
+
purpose="sign the transaction",
|
|
1259
|
+
only_existing_wallets=True,
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
client = get_client()
|
|
1263
|
+
if not client.connect():
|
|
1264
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
1265
|
+
|
|
1266
|
+
# Create request object
|
|
1267
|
+
from ...models.requests.subnet import SubnetActivateRequest
|
|
1268
|
+
|
|
1269
|
+
request = SubnetActivateRequest(subnet_id=subnet_id)
|
|
1270
|
+
|
|
1271
|
+
with HTCLILoadingContext(f"Pausing subnet {subnet_id}..."):
|
|
1272
|
+
result = client.extrinsics.subnet.owner_pause_subnet(request, keypair)
|
|
1273
|
+
|
|
1274
|
+
# Check for errors in result
|
|
1275
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
1276
|
+
error_msg = result.get("error", "Pause failed")
|
|
1277
|
+
if handle_subnet_error(error_msg, subnet_id, "pause", client):
|
|
1278
|
+
return
|
|
1279
|
+
|
|
1280
|
+
display_generic_success(result)
|
|
1281
|
+
except Exception as e:
|
|
1282
|
+
error_msg = str(e)
|
|
1283
|
+
if not handle_subnet_error(error_msg, subnet_id, "pause"):
|
|
1284
|
+
handle_and_display_error(e)
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def unpause_subnet_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
|
|
1288
|
+
try:
|
|
1289
|
+
if subnet_id is None:
|
|
1290
|
+
subnet_id = prompt_unpause_subnet()
|
|
1291
|
+
# Get signing wallet using comprehensive wallet retrieval mechanism
|
|
1292
|
+
if coldkey:
|
|
1293
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
1294
|
+
|
|
1295
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
1296
|
+
else:
|
|
1297
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
1298
|
+
wallet_type="coldkey",
|
|
1299
|
+
purpose="sign the transaction",
|
|
1300
|
+
only_existing_wallets=True,
|
|
1301
|
+
)
|
|
1302
|
+
|
|
1303
|
+
client = get_client()
|
|
1304
|
+
if not client.connect():
|
|
1305
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
1306
|
+
|
|
1307
|
+
# Create request object
|
|
1308
|
+
from ...models.requests.subnet import SubnetActivateRequest
|
|
1309
|
+
|
|
1310
|
+
request = SubnetActivateRequest(subnet_id=subnet_id)
|
|
1311
|
+
|
|
1312
|
+
with HTCLILoadingContext(f"Unpausing subnet {subnet_id}..."):
|
|
1313
|
+
result = client.extrinsics.subnet.owner_unpause_subnet(request, keypair)
|
|
1314
|
+
|
|
1315
|
+
# Check for errors in result
|
|
1316
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
1317
|
+
error_msg = result.get("error", "Unpause failed")
|
|
1318
|
+
if handle_subnet_error(error_msg, subnet_id, "unpause", client):
|
|
1319
|
+
return
|
|
1320
|
+
|
|
1321
|
+
display_generic_success(result)
|
|
1322
|
+
except Exception as e:
|
|
1323
|
+
error_msg = str(e)
|
|
1324
|
+
if not handle_subnet_error(error_msg, subnet_id, "unpause"):
|
|
1325
|
+
handle_and_display_error(e)
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
# Owner Handlers
|
|
1329
|
+
def owner_update_handler(
|
|
1330
|
+
subnet_id: Optional[int] = None,
|
|
1331
|
+
new_name: Optional[str] = None,
|
|
1332
|
+
new_repo: Optional[str] = None,
|
|
1333
|
+
target_node_registrations: Optional[int] = None,
|
|
1334
|
+
node_burn_rate_alpha: Optional[int] = None,
|
|
1335
|
+
queue_immunity_epochs: Optional[int] = None,
|
|
1336
|
+
min_weight_decrease_threshold: Optional[int] = None,
|
|
1337
|
+
min_node_reputation: Optional[int] = None,
|
|
1338
|
+
absent_reputation_penalty: Optional[int] = None,
|
|
1339
|
+
included_reputation_boost: Optional[int] = None,
|
|
1340
|
+
below_min_weight_penalty: Optional[int] = None,
|
|
1341
|
+
non_attestor_penalty: Optional[int] = None,
|
|
1342
|
+
validator_absent_penalty: Optional[int] = None,
|
|
1343
|
+
validator_non_consensus_penalty: Optional[int] = None,
|
|
1344
|
+
non_consensus_attestor_penalty: Optional[int] = None,
|
|
1345
|
+
coldkey: Optional[str] = None,
|
|
1346
|
+
):
|
|
1347
|
+
"""Unified handler for updating subnet properties."""
|
|
1348
|
+
try:
|
|
1349
|
+
|
|
1350
|
+
# Prompt for missing information
|
|
1351
|
+
if subnet_id is None or (
|
|
1352
|
+
new_name is None
|
|
1353
|
+
and new_repo is None
|
|
1354
|
+
and target_node_registrations is None
|
|
1355
|
+
and node_burn_rate_alpha is None
|
|
1356
|
+
and queue_immunity_epochs is None
|
|
1357
|
+
and min_weight_decrease_threshold is None
|
|
1358
|
+
and min_node_reputation is None
|
|
1359
|
+
and absent_reputation_penalty is None
|
|
1360
|
+
and included_reputation_boost is None
|
|
1361
|
+
and below_min_weight_penalty is None
|
|
1362
|
+
and non_attestor_penalty is None
|
|
1363
|
+
and validator_absent_penalty is None
|
|
1364
|
+
and validator_non_consensus_penalty is None
|
|
1365
|
+
and non_consensus_attestor_penalty is None
|
|
1366
|
+
):
|
|
1367
|
+
from .prompts import prompt_owner_update_extended
|
|
1368
|
+
|
|
1369
|
+
prompt_result = prompt_owner_update_extended(
|
|
1370
|
+
subnet_id,
|
|
1371
|
+
new_name,
|
|
1372
|
+
new_repo,
|
|
1373
|
+
target_node_registrations,
|
|
1374
|
+
node_burn_rate_alpha,
|
|
1375
|
+
queue_immunity_epochs,
|
|
1376
|
+
min_weight_decrease_threshold,
|
|
1377
|
+
min_node_reputation,
|
|
1378
|
+
absent_reputation_penalty,
|
|
1379
|
+
included_reputation_boost,
|
|
1380
|
+
below_min_weight_penalty,
|
|
1381
|
+
non_attestor_penalty,
|
|
1382
|
+
validator_absent_penalty,
|
|
1383
|
+
validator_non_consensus_penalty,
|
|
1384
|
+
non_consensus_attestor_penalty,
|
|
1385
|
+
)
|
|
1386
|
+
subnet_id = prompt_result.get("subnet_id", subnet_id)
|
|
1387
|
+
new_name = prompt_result.get("new_name", new_name)
|
|
1388
|
+
new_repo = prompt_result.get("new_repo", new_repo)
|
|
1389
|
+
target_node_registrations = prompt_result.get(
|
|
1390
|
+
"target_node_registrations", target_node_registrations
|
|
1391
|
+
)
|
|
1392
|
+
node_burn_rate_alpha = prompt_result.get(
|
|
1393
|
+
"node_burn_rate_alpha", node_burn_rate_alpha
|
|
1394
|
+
)
|
|
1395
|
+
queue_immunity_epochs = prompt_result.get(
|
|
1396
|
+
"queue_immunity_epochs", queue_immunity_epochs
|
|
1397
|
+
)
|
|
1398
|
+
min_weight_decrease_threshold = prompt_result.get(
|
|
1399
|
+
"min_weight_decrease_threshold", min_weight_decrease_threshold
|
|
1400
|
+
)
|
|
1401
|
+
min_node_reputation = prompt_result.get(
|
|
1402
|
+
"min_node_reputation", min_node_reputation
|
|
1403
|
+
)
|
|
1404
|
+
absent_reputation_penalty = prompt_result.get(
|
|
1405
|
+
"absent_reputation_penalty", absent_reputation_penalty
|
|
1406
|
+
)
|
|
1407
|
+
included_reputation_boost = prompt_result.get(
|
|
1408
|
+
"included_reputation_boost", included_reputation_boost
|
|
1409
|
+
)
|
|
1410
|
+
below_min_weight_penalty = prompt_result.get(
|
|
1411
|
+
"below_min_weight_penalty", below_min_weight_penalty
|
|
1412
|
+
)
|
|
1413
|
+
non_attestor_penalty = prompt_result.get(
|
|
1414
|
+
"non_attestor_penalty", non_attestor_penalty
|
|
1415
|
+
)
|
|
1416
|
+
validator_absent_penalty = prompt_result.get(
|
|
1417
|
+
"validator_absent_penalty", validator_absent_penalty
|
|
1418
|
+
)
|
|
1419
|
+
validator_non_consensus_penalty = prompt_result.get(
|
|
1420
|
+
"validator_non_consensus_penalty", validator_non_consensus_penalty
|
|
1421
|
+
)
|
|
1422
|
+
non_consensus_attestor_penalty = prompt_result.get(
|
|
1423
|
+
"non_consensus_attestor_penalty", non_consensus_attestor_penalty
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
# Validate subnet_id is provided
|
|
1427
|
+
if subnet_id is None:
|
|
1428
|
+
print_error("Subnet ID is required. Exiting.")
|
|
1429
|
+
return
|
|
1430
|
+
|
|
1431
|
+
# Validate subnet_id is non-negative
|
|
1432
|
+
if subnet_id < 0:
|
|
1433
|
+
print_error("Subnet ID must be a non-negative integer.")
|
|
1434
|
+
return
|
|
1435
|
+
|
|
1436
|
+
# Check if all update fields are None (nothing to change)
|
|
1437
|
+
if (
|
|
1438
|
+
new_name is None
|
|
1439
|
+
and new_repo is None
|
|
1440
|
+
and target_node_registrations is None
|
|
1441
|
+
and node_burn_rate_alpha is None
|
|
1442
|
+
and queue_immunity_epochs is None
|
|
1443
|
+
and min_weight_decrease_threshold is None
|
|
1444
|
+
and min_node_reputation is None
|
|
1445
|
+
and absent_reputation_penalty is None
|
|
1446
|
+
and included_reputation_boost is None
|
|
1447
|
+
and below_min_weight_penalty is None
|
|
1448
|
+
and non_attestor_penalty is None
|
|
1449
|
+
and validator_absent_penalty is None
|
|
1450
|
+
and validator_non_consensus_penalty is None
|
|
1451
|
+
and non_consensus_attestor_penalty is None
|
|
1452
|
+
):
|
|
1453
|
+
print_info("No fields were provided to be changed. Exiting.")
|
|
1454
|
+
return
|
|
1455
|
+
|
|
1456
|
+
# Get signing wallet using comprehensive wallet retrieval mechanism
|
|
1457
|
+
if coldkey:
|
|
1458
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
1459
|
+
|
|
1460
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
1461
|
+
else:
|
|
1462
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
1463
|
+
wallet_type="coldkey",
|
|
1464
|
+
purpose="sign the transaction",
|
|
1465
|
+
only_existing_wallets=True,
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
client = get_client()
|
|
1469
|
+
if not client.connect():
|
|
1470
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
1471
|
+
|
|
1472
|
+
# Delegate all extrinsics to the client layer in one place
|
|
1473
|
+
with HTCLILoadingContext(f"Updating subnet {subnet_id} parameters..."):
|
|
1474
|
+
results = client.extrinsics.subnet.owner_update_parameters(
|
|
1475
|
+
subnet_id=subnet_id,
|
|
1476
|
+
new_name=new_name,
|
|
1477
|
+
new_repo=new_repo,
|
|
1478
|
+
target_node_registrations=target_node_registrations,
|
|
1479
|
+
node_burn_rate_alpha=node_burn_rate_alpha,
|
|
1480
|
+
queue_immunity_epochs=queue_immunity_epochs,
|
|
1481
|
+
min_weight_decrease_threshold=min_weight_decrease_threshold,
|
|
1482
|
+
min_node_reputation=min_node_reputation,
|
|
1483
|
+
absent_reputation_penalty=absent_reputation_penalty,
|
|
1484
|
+
included_reputation_boost=included_reputation_boost,
|
|
1485
|
+
below_min_weight_penalty=below_min_weight_penalty,
|
|
1486
|
+
non_attestor_penalty=non_attestor_penalty,
|
|
1487
|
+
validator_absent_penalty=validator_absent_penalty,
|
|
1488
|
+
validator_non_consensus_penalty=validator_non_consensus_penalty,
|
|
1489
|
+
non_consensus_attestor_penalty=non_consensus_attestor_penalty,
|
|
1490
|
+
keypair=keypair,
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
# Check for any errors and let the shared handler convert them
|
|
1494
|
+
for field, result in results:
|
|
1495
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
1496
|
+
error_msg = result.get("error", "Update failed")
|
|
1497
|
+
if handle_subnet_error(error_msg, subnet_id, f"update {field}", client):
|
|
1498
|
+
return
|
|
1499
|
+
|
|
1500
|
+
# Display consolidated results in a single panel
|
|
1501
|
+
if results:
|
|
1502
|
+
from .display import display_subnet_updates_summary
|
|
1503
|
+
|
|
1504
|
+
display_subnet_updates_summary(subnet_id, results, client)
|
|
1505
|
+
|
|
1506
|
+
except Exception as e:
|
|
1507
|
+
error_msg = str(e)
|
|
1508
|
+
if not handle_subnet_error(error_msg, subnet_id, "update"):
|
|
1509
|
+
handle_and_display_error(e)
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def owner_update_name_handler(
|
|
1513
|
+
subnet_id: Optional[int], new_name: Optional[str], coldkey: Optional[str] = None
|
|
1514
|
+
):
|
|
1515
|
+
try:
|
|
1516
|
+
from ...models.requests.subnet import SubnetUpdateRequest
|
|
1517
|
+
|
|
1518
|
+
if subnet_id is None or new_name is None:
|
|
1519
|
+
subnet_id, new_name = prompt_owner_update_name()
|
|
1520
|
+
|
|
1521
|
+
# Get signing wallet using comprehensive wallet retrieval mechanism
|
|
1522
|
+
if coldkey:
|
|
1523
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
1524
|
+
|
|
1525
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
1526
|
+
else:
|
|
1527
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
1528
|
+
wallet_type="coldkey",
|
|
1529
|
+
purpose="sign the transaction",
|
|
1530
|
+
only_existing_wallets=True,
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
client = get_client()
|
|
1534
|
+
if not client.connect():
|
|
1535
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
1536
|
+
|
|
1537
|
+
with HTCLILoadingContext(f"Updating name for subnet {subnet_id}..."):
|
|
1538
|
+
request = SubnetUpdateRequest(subnet_id=subnet_id, value=new_name)
|
|
1539
|
+
result = client.extrinsics.subnet.owner_update_name(
|
|
1540
|
+
request, keypair=keypair
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
# Check for errors in result
|
|
1544
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
1545
|
+
error_msg = result.get("error", "Update failed")
|
|
1546
|
+
if handle_subnet_error(error_msg, subnet_id, "update name", client):
|
|
1547
|
+
return
|
|
1548
|
+
|
|
1549
|
+
display_generic_success(result)
|
|
1550
|
+
except Exception as e:
|
|
1551
|
+
error_msg = str(e)
|
|
1552
|
+
if not handle_subnet_error(error_msg, subnet_id, "update name"):
|
|
1553
|
+
handle_and_display_error(e)
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
def owner_update_repo_handler(
|
|
1557
|
+
subnet_id: Optional[int], new_repo: Optional[str], coldkey: Optional[str] = None
|
|
1558
|
+
):
|
|
1559
|
+
try:
|
|
1560
|
+
from ...models.requests.subnet import SubnetUpdateRequest
|
|
1561
|
+
|
|
1562
|
+
if subnet_id is None or new_repo is None:
|
|
1563
|
+
subnet_id, new_repo = prompt_owner_update_repo()
|
|
1564
|
+
|
|
1565
|
+
# Get signing wallet using comprehensive wallet retrieval mechanism
|
|
1566
|
+
if coldkey:
|
|
1567
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
1568
|
+
|
|
1569
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
1570
|
+
else:
|
|
1571
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
1572
|
+
wallet_type="coldkey",
|
|
1573
|
+
purpose="sign the transaction",
|
|
1574
|
+
only_existing_wallets=True,
|
|
1575
|
+
)
|
|
1576
|
+
|
|
1577
|
+
client = get_client()
|
|
1578
|
+
if not client.connect():
|
|
1579
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
1580
|
+
|
|
1581
|
+
with HTCLILoadingContext(f"Updating repo for subnet {subnet_id}..."):
|
|
1582
|
+
request = SubnetUpdateRequest(subnet_id=subnet_id, value=new_repo)
|
|
1583
|
+
result = client.extrinsics.subnet.owner_update_repo(
|
|
1584
|
+
request, keypair=keypair
|
|
1585
|
+
)
|
|
1586
|
+
|
|
1587
|
+
# Check for errors in result
|
|
1588
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
1589
|
+
error_msg = result.get("error", "Update failed")
|
|
1590
|
+
if handle_subnet_error(error_msg, subnet_id, "update repository", client):
|
|
1591
|
+
return
|
|
1592
|
+
|
|
1593
|
+
display_generic_success(result)
|
|
1594
|
+
except Exception as e:
|
|
1595
|
+
error_msg = str(e)
|
|
1596
|
+
if not handle_subnet_error(error_msg, subnet_id, "update repository"):
|
|
1597
|
+
handle_and_display_error(e)
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
def owner_transfer_handler(
|
|
1601
|
+
subnet_id: Optional[int], new_owner: Optional[str], coldkey: Optional[str] = None
|
|
1602
|
+
):
|
|
1603
|
+
try:
|
|
1604
|
+
if subnet_id is None or new_owner is None:
|
|
1605
|
+
subnet_id, new_owner = prompt_owner_transfer()
|
|
1606
|
+
|
|
1607
|
+
# Resolve new_owner: can be either EVM address or wallet name
|
|
1608
|
+
from ...utils.blockchain.validation import validate_address
|
|
1609
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
1610
|
+
from ...utils.wallet.crypto import public_key_to_evm_address
|
|
1611
|
+
|
|
1612
|
+
if not new_owner:
|
|
1613
|
+
raise ValueError("New owner address cannot be empty")
|
|
1614
|
+
|
|
1615
|
+
new_owner = new_owner.strip()
|
|
1616
|
+
|
|
1617
|
+
# First, check if it's already a valid EVM address
|
|
1618
|
+
if validate_address(new_owner):
|
|
1619
|
+
# Normalize EVM addresses to lowercase (addresses are case-insensitive)
|
|
1620
|
+
new_owner = new_owner.lower()
|
|
1621
|
+
else:
|
|
1622
|
+
# Try to resolve as wallet name
|
|
1623
|
+
try:
|
|
1624
|
+
wallet_info = get_wallet_info_by_name(
|
|
1625
|
+
new_owner,
|
|
1626
|
+
is_hotkey=False, # Looking for coldkey wallets
|
|
1627
|
+
)
|
|
1628
|
+
|
|
1629
|
+
if wallet_info:
|
|
1630
|
+
# Try to get EVM address from wallet info
|
|
1631
|
+
resolved_address = wallet_info.get(
|
|
1632
|
+
"evm_address"
|
|
1633
|
+
) or wallet_info.get("address")
|
|
1634
|
+
|
|
1635
|
+
# If we have an address, check if it's EVM format
|
|
1636
|
+
if resolved_address and validate_address(resolved_address):
|
|
1637
|
+
new_owner = resolved_address.lower()
|
|
1638
|
+
else:
|
|
1639
|
+
# Try to derive from public key
|
|
1640
|
+
public_key_hex = wallet_info.get("public_key")
|
|
1641
|
+
if public_key_hex:
|
|
1642
|
+
try:
|
|
1643
|
+
resolved_address = public_key_to_evm_address(
|
|
1644
|
+
bytes.fromhex(public_key_hex)
|
|
1645
|
+
)
|
|
1646
|
+
if validate_address(resolved_address):
|
|
1647
|
+
new_owner = resolved_address.lower()
|
|
1648
|
+
else:
|
|
1649
|
+
raise ValueError(
|
|
1650
|
+
f"Wallet '{new_owner}' does not have a valid EVM address. "
|
|
1651
|
+
"Please use an EVM address (0x...) directly."
|
|
1652
|
+
)
|
|
1653
|
+
except Exception as e:
|
|
1654
|
+
raise ValueError(
|
|
1655
|
+
f"Failed to derive EVM address from wallet '{new_owner}': {str(e)}"
|
|
1656
|
+
) from e
|
|
1657
|
+
else:
|
|
1658
|
+
raise ValueError(
|
|
1659
|
+
f"Wallet '{new_owner}' does not have a valid EVM address. "
|
|
1660
|
+
"Please use an EVM address (0x...) directly."
|
|
1661
|
+
)
|
|
1662
|
+
else:
|
|
1663
|
+
raise ValueError(f"Wallet '{new_owner}' not found")
|
|
1664
|
+
except FileNotFoundError:
|
|
1665
|
+
raise ValueError(
|
|
1666
|
+
f"Wallet '{new_owner}' not found. "
|
|
1667
|
+
"Please provide either a wallet name or a valid EVM address (0x...)."
|
|
1668
|
+
)
|
|
1669
|
+
except ValueError:
|
|
1670
|
+
# Re-raise ValueError as-is (already has good error message)
|
|
1671
|
+
raise
|
|
1672
|
+
except Exception as e:
|
|
1673
|
+
raise ValueError(
|
|
1674
|
+
f"Failed to resolve new owner '{new_owner}': {str(e)}"
|
|
1675
|
+
) from e
|
|
1676
|
+
|
|
1677
|
+
# Get signing wallet using comprehensive wallet retrieval mechanism
|
|
1678
|
+
if coldkey:
|
|
1679
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
1680
|
+
|
|
1681
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
1682
|
+
else:
|
|
1683
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
1684
|
+
wallet_type="coldkey",
|
|
1685
|
+
purpose="sign the transfer transaction",
|
|
1686
|
+
only_existing_wallets=True,
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1689
|
+
client = get_client()
|
|
1690
|
+
if not client.connect():
|
|
1691
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
1692
|
+
|
|
1693
|
+
from ...models.requests.subnet import SubnetOwnershipTransferRequest
|
|
1694
|
+
|
|
1695
|
+
request = SubnetOwnershipTransferRequest(
|
|
1696
|
+
subnet_id=subnet_id, new_owner=new_owner
|
|
1697
|
+
)
|
|
1698
|
+
|
|
1699
|
+
# Use spinner with message that updates
|
|
1700
|
+
from ...ui.components import HTCLILoadingContext
|
|
1701
|
+
|
|
1702
|
+
with HTCLILoadingContext(
|
|
1703
|
+
f"Submitting transfer ownership transaction for subnet {subnet_id}..."
|
|
1704
|
+
):
|
|
1705
|
+
result = client.extrinsics.subnet.transfer_subnet_ownership(
|
|
1706
|
+
request, keypair=keypair
|
|
1707
|
+
)
|
|
1708
|
+
|
|
1709
|
+
# Debug: Log what we received
|
|
1710
|
+
from ...utils.logging import get_logger
|
|
1711
|
+
|
|
1712
|
+
logger = get_logger(__name__)
|
|
1713
|
+
logger.debug(f"Transfer ownership result: {result}")
|
|
1714
|
+
logger.debug(
|
|
1715
|
+
f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'not a dict'}"
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
# Extract transaction details first
|
|
1719
|
+
tx_hash = result.get("transaction_hash") or result.get("extrinsic_hash")
|
|
1720
|
+
block_number = result.get("block_number")
|
|
1721
|
+
block_hash = result.get("block_hash")
|
|
1722
|
+
|
|
1723
|
+
# CRITICAL: Check for errors in result OR missing transaction details
|
|
1724
|
+
# If success is False OR we don't have a transaction hash, treat as failure
|
|
1725
|
+
if isinstance(result, dict) and (
|
|
1726
|
+
not result.get("success", False) or not tx_hash
|
|
1727
|
+
):
|
|
1728
|
+
raw_error = result.get(
|
|
1729
|
+
"error", "Transfer failed - no transaction hash returned"
|
|
1730
|
+
)
|
|
1731
|
+
if not tx_hash and result.get("success", False):
|
|
1732
|
+
raw_error = "Transaction submitted but no transaction hash returned. The transaction may have failed."
|
|
1733
|
+
|
|
1734
|
+
# Try to parse error if it's a string representation of a dict
|
|
1735
|
+
error_msg = raw_error
|
|
1736
|
+
if (
|
|
1737
|
+
isinstance(raw_error, str)
|
|
1738
|
+
and raw_error.startswith("{")
|
|
1739
|
+
and "'code'" in raw_error
|
|
1740
|
+
):
|
|
1741
|
+
try:
|
|
1742
|
+
import ast
|
|
1743
|
+
|
|
1744
|
+
error_dict = ast.literal_eval(raw_error)
|
|
1745
|
+
if isinstance(error_dict, dict):
|
|
1746
|
+
# Extract the actual error message
|
|
1747
|
+
error_data = error_dict.get("data", "")
|
|
1748
|
+
error_message = error_dict.get("message", "")
|
|
1749
|
+
error_code = error_dict.get("code", "")
|
|
1750
|
+
error_msg = (
|
|
1751
|
+
f"{error_message}: {error_data}"
|
|
1752
|
+
if error_data
|
|
1753
|
+
else error_message
|
|
1754
|
+
)
|
|
1755
|
+
if error_code:
|
|
1756
|
+
error_msg = f"[Code {error_code}] {error_msg}"
|
|
1757
|
+
except Exception:
|
|
1758
|
+
# If parsing fails, use original
|
|
1759
|
+
pass
|
|
1760
|
+
|
|
1761
|
+
# Log the original error before parsing
|
|
1762
|
+
logger.error(
|
|
1763
|
+
f"Transfer ownership failed - ORIGINAL ERROR (raw): {raw_error}"
|
|
1764
|
+
)
|
|
1765
|
+
logger.error(f"Transfer ownership failed - PARSED ERROR: {error_msg}")
|
|
1766
|
+
logger.error(f"Full result dict: {result}")
|
|
1767
|
+
|
|
1768
|
+
# Display original error before parsing
|
|
1769
|
+
from ...ui.display import HTCLIConsole as DebugConsole
|
|
1770
|
+
|
|
1771
|
+
debug_console = DebugConsole()
|
|
1772
|
+
debug_console.print()
|
|
1773
|
+
debug_console.print(
|
|
1774
|
+
"[bold yellow]🔍 DEBUG: Original Error (before parsing):[/bold yellow]"
|
|
1775
|
+
)
|
|
1776
|
+
debug_console.print(f"[yellow]Raw: {raw_error}[/yellow]")
|
|
1777
|
+
debug_console.print(f"[yellow]Parsed: {error_msg}[/yellow]")
|
|
1778
|
+
debug_console.print(f"[dim]Full result: {result}[/dim]")
|
|
1779
|
+
debug_console.print()
|
|
1780
|
+
|
|
1781
|
+
if handle_subnet_error(error_msg, subnet_id, "transfer ownership", client):
|
|
1782
|
+
return
|
|
1783
|
+
# If handle_subnet_error didn't handle it, raise an exception to prevent showing success
|
|
1784
|
+
raise RuntimeError(error_msg)
|
|
1785
|
+
|
|
1786
|
+
# CRITICAL: Double-check we have transaction hash before showing success
|
|
1787
|
+
# This is a safety check - we should NEVER show success without a transaction hash
|
|
1788
|
+
if not tx_hash:
|
|
1789
|
+
error_msg = "Transaction failed - no transaction hash returned. The transaction may have been rejected by the chain."
|
|
1790
|
+
logger.error(f"Transfer ownership failed: {error_msg}")
|
|
1791
|
+
if handle_subnet_error(error_msg, subnet_id, "transfer ownership", client):
|
|
1792
|
+
return
|
|
1793
|
+
|
|
1794
|
+
# Try to resolve block number if missing but we have block hash
|
|
1795
|
+
if not block_number and block_hash:
|
|
1796
|
+
try:
|
|
1797
|
+
resolved = client.rpc.chain.get_block_number(block_hash=block_hash)
|
|
1798
|
+
if resolved is not None:
|
|
1799
|
+
block_number = resolved
|
|
1800
|
+
except Exception:
|
|
1801
|
+
pass # Ignore if we can't resolve
|
|
1802
|
+
|
|
1803
|
+
# Display detailed success message with transaction details
|
|
1804
|
+
from ...ui.components import HTCLIPanel
|
|
1805
|
+
from ...ui.display import HTCLIConsole
|
|
1806
|
+
|
|
1807
|
+
console = HTCLIConsole()
|
|
1808
|
+
console.print() # Blank line after spinner
|
|
1809
|
+
|
|
1810
|
+
block_display = block_number if block_number is not None else "Pending"
|
|
1811
|
+
block_hash_display = block_hash or "N/A"
|
|
1812
|
+
tx_hash_display = tx_hash or "N/A"
|
|
1813
|
+
|
|
1814
|
+
success_msg = f"""[htcli.success]✅ Subnet {subnet_id} ownership transfer initiated successfully![/htcli.success]
|
|
1815
|
+
|
|
1816
|
+
[htcli.value]Transaction Hash:[/htcli.value] {tx_hash_display}
|
|
1817
|
+
[htcli.value]Block Number:[/htcli.value] {block_display}
|
|
1818
|
+
[htcli.value]Block Hash:[/htcli.value] {block_hash_display}
|
|
1819
|
+
|
|
1820
|
+
[htcli.info]💡 The new owner must accept the transfer using: htcli subnet accept --subnet-id {subnet_id}[/htcli.info]
|
|
1821
|
+
"""
|
|
1822
|
+
|
|
1823
|
+
panel = HTCLIPanel(
|
|
1824
|
+
success_msg,
|
|
1825
|
+
title="✅ Transfer Initiated",
|
|
1826
|
+
border_style="htcli.success",
|
|
1827
|
+
highlight=True,
|
|
1828
|
+
)
|
|
1829
|
+
panel.render()
|
|
1830
|
+
except Exception as e:
|
|
1831
|
+
error_msg = str(e)
|
|
1832
|
+
if not handle_subnet_error(error_msg, subnet_id, "transfer ownership"):
|
|
1833
|
+
handle_and_display_error(e)
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
def owner_accept_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
|
|
1837
|
+
try:
|
|
1838
|
+
if subnet_id is None:
|
|
1839
|
+
subnet_id = prompt_owner_accept()
|
|
1840
|
+
# Get signing wallet using comprehensive wallet retrieval mechanism
|
|
1841
|
+
if coldkey:
|
|
1842
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
1843
|
+
|
|
1844
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
1845
|
+
else:
|
|
1846
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
1847
|
+
wallet_type="coldkey",
|
|
1848
|
+
purpose="sign the ownership acceptance transaction",
|
|
1849
|
+
only_existing_wallets=True,
|
|
1850
|
+
)
|
|
1851
|
+
|
|
1852
|
+
client = get_client()
|
|
1853
|
+
if not client.connect():
|
|
1854
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
1855
|
+
|
|
1856
|
+
from ...models.requests.subnet import SubnetOwnershipAcceptRequest
|
|
1857
|
+
|
|
1858
|
+
request = SubnetOwnershipAcceptRequest(subnet_id=subnet_id)
|
|
1859
|
+
|
|
1860
|
+
# Use the same simple pattern as other handlers
|
|
1861
|
+
# The spinner will handle any output suppression needed
|
|
1862
|
+
with HTCLILoadingContext(
|
|
1863
|
+
f"Submitting accept ownership transaction for subnet {subnet_id}..."
|
|
1864
|
+
):
|
|
1865
|
+
result = client.extrinsics.subnet.accept_subnet_ownership(
|
|
1866
|
+
request, keypair=keypair
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
# Debug: Log what we received
|
|
1870
|
+
from ...utils.logging import get_logger
|
|
1871
|
+
|
|
1872
|
+
logger = get_logger(__name__)
|
|
1873
|
+
logger.debug(f"Accept ownership result: {result}")
|
|
1874
|
+
logger.debug(
|
|
1875
|
+
f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'not a dict'}"
|
|
1876
|
+
)
|
|
1877
|
+
|
|
1878
|
+
# Extract transaction details first
|
|
1879
|
+
tx_hash = result.get("transaction_hash") or result.get("extrinsic_hash")
|
|
1880
|
+
block_number = result.get("block_number")
|
|
1881
|
+
block_hash = result.get("block_hash")
|
|
1882
|
+
|
|
1883
|
+
logger.debug(
|
|
1884
|
+
f"Extracted - tx_hash: {tx_hash}, block_number: {block_number}, block_hash: {block_hash}"
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
# CRITICAL: Check for errors in result OR missing transaction details
|
|
1888
|
+
# If success is False OR we don't have a transaction hash, treat as failure
|
|
1889
|
+
if isinstance(result, dict) and (
|
|
1890
|
+
not result.get("success", False) or not tx_hash
|
|
1891
|
+
):
|
|
1892
|
+
error_msg = result.get(
|
|
1893
|
+
"error", "Accept failed - no transaction hash returned"
|
|
1894
|
+
)
|
|
1895
|
+
if not tx_hash and result.get("success", False):
|
|
1896
|
+
error_msg = "Transaction submitted but no transaction hash returned. The transaction may have failed."
|
|
1897
|
+
|
|
1898
|
+
logger.error(f"Accept ownership failed: {error_msg}")
|
|
1899
|
+
if handle_subnet_error(error_msg, subnet_id, "accept ownership", client):
|
|
1900
|
+
return
|
|
1901
|
+
# If handle_subnet_error didn't handle it, raise an exception to prevent showing success
|
|
1902
|
+
raise RuntimeError(error_msg)
|
|
1903
|
+
|
|
1904
|
+
# CRITICAL: Double-check we have transaction hash before showing success
|
|
1905
|
+
# This is a safety check - we should NEVER show success without a transaction hash
|
|
1906
|
+
if not tx_hash:
|
|
1907
|
+
error_msg = "Transaction failed - no transaction hash returned. The transaction may have been rejected by the chain."
|
|
1908
|
+
logger.error(f"Accept ownership failed: {error_msg}")
|
|
1909
|
+
if handle_subnet_error(error_msg, subnet_id, "accept ownership", client):
|
|
1910
|
+
return
|
|
1911
|
+
raise RuntimeError(error_msg)
|
|
1912
|
+
|
|
1913
|
+
# Try to resolve block number if missing but we have block hash
|
|
1914
|
+
if not block_number and block_hash:
|
|
1915
|
+
try:
|
|
1916
|
+
resolved = client.rpc.chain.get_block_number(block_hash=block_hash)
|
|
1917
|
+
if resolved is not None:
|
|
1918
|
+
block_number = resolved
|
|
1919
|
+
except Exception:
|
|
1920
|
+
pass # Ignore if we can't resolve
|
|
1921
|
+
|
|
1922
|
+
# Display detailed success message with transaction details
|
|
1923
|
+
from ...ui.components import HTCLIPanel
|
|
1924
|
+
from ...ui.display import HTCLIConsole
|
|
1925
|
+
|
|
1926
|
+
console = HTCLIConsole()
|
|
1927
|
+
console.print() # Blank line after spinner
|
|
1928
|
+
|
|
1929
|
+
block_display = block_number if block_number is not None else "Pending"
|
|
1930
|
+
block_hash_display = block_hash or "N/A"
|
|
1931
|
+
tx_hash_display = tx_hash or "N/A"
|
|
1932
|
+
|
|
1933
|
+
success_msg = f"""[htcli.success]✅ Subnet {subnet_id} ownership accepted successfully![/htcli.success]
|
|
1934
|
+
|
|
1935
|
+
[htcli.value]Transaction Hash:[/htcli.value] {tx_hash_display}
|
|
1936
|
+
[htcli.value]Block Number:[/htcli.value] {block_display}
|
|
1937
|
+
[htcli.value]Block Hash:[/htcli.value] {block_hash_display}
|
|
1938
|
+
|
|
1939
|
+
[htcli.info]💡 You are now the owner of subnet {subnet_id}.[/htcli.info]
|
|
1940
|
+
"""
|
|
1941
|
+
|
|
1942
|
+
panel = HTCLIPanel(
|
|
1943
|
+
success_msg,
|
|
1944
|
+
title="✅ Ownership Accepted",
|
|
1945
|
+
border_style="htcli.success",
|
|
1946
|
+
highlight=True,
|
|
1947
|
+
)
|
|
1948
|
+
panel.render()
|
|
1949
|
+
except Exception as e:
|
|
1950
|
+
error_msg = str(e)
|
|
1951
|
+
if not handle_subnet_error(error_msg, subnet_id, "accept ownership"):
|
|
1952
|
+
handle_and_display_error(e)
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
def owner_remove_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
|
|
1956
|
+
"""Handle subnet removal with proper error handling and confirmation."""
|
|
1957
|
+
try:
|
|
1958
|
+
if subnet_id is None:
|
|
1959
|
+
subnet_id = prompt_owner_remove()
|
|
1960
|
+
|
|
1961
|
+
if subnet_id < 0:
|
|
1962
|
+
raise ValueError("Subnet ID must be a non-negative integer")
|
|
1963
|
+
|
|
1964
|
+
# Get signing wallet using comprehensive wallet retrieval mechanism
|
|
1965
|
+
if coldkey:
|
|
1966
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
1967
|
+
|
|
1968
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
1969
|
+
else:
|
|
1970
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
1971
|
+
wallet_type="coldkey",
|
|
1972
|
+
purpose="sign the transaction",
|
|
1973
|
+
only_existing_wallets=True,
|
|
1974
|
+
)
|
|
1975
|
+
|
|
1976
|
+
client = get_client()
|
|
1977
|
+
if not client.connect():
|
|
1978
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
1979
|
+
|
|
1980
|
+
from ...models.requests.subnet import SubnetRemoveRequest
|
|
1981
|
+
|
|
1982
|
+
request = SubnetRemoveRequest(subnet_id=subnet_id)
|
|
1983
|
+
|
|
1984
|
+
with HTCLILoadingContext(f"Removing subnet {subnet_id}..."):
|
|
1985
|
+
result = client.extrinsics.subnet.remove_subnet(request, keypair=keypair)
|
|
1986
|
+
|
|
1987
|
+
# Check for errors in result
|
|
1988
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
1989
|
+
error_msg = result.get("error", "Remove failed")
|
|
1990
|
+
if handle_subnet_error(error_msg, subnet_id, "remove", client):
|
|
1991
|
+
return
|
|
1992
|
+
|
|
1993
|
+
display_generic_success(result)
|
|
1994
|
+
except ValueError as e:
|
|
1995
|
+
# Handle validation errors and user cancellation
|
|
1996
|
+
if "cancelled" in str(e).lower():
|
|
1997
|
+
from ...ui.display import HTCLIConsole
|
|
1998
|
+
|
|
1999
|
+
console = HTCLIConsole()
|
|
2000
|
+
console.print("[htcli.warning]Subnet removal cancelled by user.[/]")
|
|
2001
|
+
else:
|
|
2002
|
+
from ...ui.display import print_error
|
|
2003
|
+
|
|
2004
|
+
print_error(f"Invalid subnet ID: {str(e)}")
|
|
2005
|
+
except Exception as e:
|
|
2006
|
+
error_msg = str(e)
|
|
2007
|
+
# Use specific subnet error handling
|
|
2008
|
+
if not handle_subnet_error(error_msg, subnet_id, "remove"):
|
|
2009
|
+
if "subnet" in error_msg.lower() or "remove" in error_msg.lower():
|
|
2010
|
+
error = handle_substrate_error(e)
|
|
2011
|
+
if not isinstance(error, SubnetError):
|
|
2012
|
+
error = SubnetError(
|
|
2013
|
+
f"Failed to remove subnet {subnet_id}: {error_msg}"
|
|
2014
|
+
)
|
|
2015
|
+
error.display()
|
|
2016
|
+
else:
|
|
2017
|
+
handle_and_display_error(e)
|
|
2018
|
+
|
|
2019
|
+
|
|
2020
|
+
def get_node_status_handler(subnet_id: Optional[int], node_id: Optional[int]):
|
|
2021
|
+
"""Handle getting node status with proper error handling."""
|
|
2022
|
+
try:
|
|
2023
|
+
if subnet_id is None:
|
|
2024
|
+
from .prompts import prompt_get_subnet
|
|
2025
|
+
|
|
2026
|
+
subnet_id = prompt_get_subnet()
|
|
2027
|
+
|
|
2028
|
+
if node_id is None:
|
|
2029
|
+
from ...ui.prompts import integer_prompt
|
|
2030
|
+
|
|
2031
|
+
node_id = integer_prompt(
|
|
2032
|
+
"Enter the node ID within the subnet",
|
|
2033
|
+
min_value=0,
|
|
2034
|
+
)
|
|
2035
|
+
|
|
2036
|
+
with HTCLILoadingContext(
|
|
2037
|
+
f"Fetching node {node_id} status from subnet {subnet_id}..."
|
|
2038
|
+
):
|
|
2039
|
+
client = get_client()
|
|
2040
|
+
result = client.rpc.node.get_subnet_node_info(subnet_id, node_id)
|
|
2041
|
+
|
|
2042
|
+
# Display the node status information
|
|
2043
|
+
if result:
|
|
2044
|
+
from ...ui.display import print_info, print_success
|
|
2045
|
+
|
|
2046
|
+
print_success(f"Node {node_id} status in subnet {subnet_id}")
|
|
2047
|
+
classification = getattr(result, "classification", {}) or {}
|
|
2048
|
+
status = classification.get("node_class", "Unknown")
|
|
2049
|
+
print_info(f"Status: {status}")
|
|
2050
|
+
print_info(f"Stake: {getattr(result, 'stake_balance', 0)}")
|
|
2051
|
+
print_info(f"Reputation: {getattr(result, 'subnet_node_reputation', 0)}")
|
|
2052
|
+
else:
|
|
2053
|
+
from ...ui.display import print_error
|
|
2054
|
+
|
|
2055
|
+
print_error(f"Node {node_id} not found in subnet {subnet_id}")
|
|
2056
|
+
|
|
2057
|
+
except Exception as e:
|
|
2058
|
+
handle_and_display_error(e)
|
|
2059
|
+
|
|
2060
|
+
|
|
2061
|
+
def get_node_staking_handler(subnet_id: Optional[int], node_id: Optional[int]):
|
|
2062
|
+
"""Handle getting node staking information with proper error handling."""
|
|
2063
|
+
try:
|
|
2064
|
+
if subnet_id is None:
|
|
2065
|
+
from .prompts import prompt_get_subnet
|
|
2066
|
+
|
|
2067
|
+
subnet_id = prompt_get_subnet()
|
|
2068
|
+
|
|
2069
|
+
if node_id is None:
|
|
2070
|
+
from ...ui.prompts import integer_prompt
|
|
2071
|
+
|
|
2072
|
+
node_id = integer_prompt(
|
|
2073
|
+
"Enter the node ID within the subnet",
|
|
2074
|
+
min_value=0,
|
|
2075
|
+
)
|
|
2076
|
+
|
|
2077
|
+
client = get_client()
|
|
2078
|
+
with HTCLILoadingContext(
|
|
2079
|
+
f"Fetching staking info for node {node_id} in subnet {subnet_id}..."
|
|
2080
|
+
):
|
|
2081
|
+
result = client.rpc.node.get_node_staking_info(subnet_id, node_id)
|
|
2082
|
+
|
|
2083
|
+
# Display the node staking information
|
|
2084
|
+
if result.get("success"):
|
|
2085
|
+
staking_data = result.get("data", {})
|
|
2086
|
+
from ...ui.display import print_info, print_success
|
|
2087
|
+
|
|
2088
|
+
print_success(f"Node {node_id} staking info in subnet {subnet_id}")
|
|
2089
|
+
print_info(f"Node Stake: {staking_data.get('node_stake', 0)}")
|
|
2090
|
+
print_info(
|
|
2091
|
+
f"Delegate Stake Shares: {staking_data.get('delegate_stake_shares', 0)}"
|
|
2092
|
+
)
|
|
2093
|
+
print_info(f"Reward Rate: {staking_data.get('reward_rate', 0)}")
|
|
2094
|
+
else:
|
|
2095
|
+
from ...ui.display import print_error
|
|
2096
|
+
|
|
2097
|
+
print_error(
|
|
2098
|
+
f"Failed to get node staking info: {result.get('message', 'Unknown error')}"
|
|
2099
|
+
)
|
|
2100
|
+
|
|
2101
|
+
except Exception as e:
|
|
2102
|
+
handle_and_display_error(e)
|
|
2103
|
+
|
|
2104
|
+
|
|
2105
|
+
# New RPC-based handlers
|
|
2106
|
+
def get_subnet_info_handler(subnet_id: Optional[int], all_subnets: bool = False):
|
|
2107
|
+
"""Handle getting subnet information using RPC."""
|
|
2108
|
+
try:
|
|
2109
|
+
if all_subnets and subnet_id is not None:
|
|
2110
|
+
print_error("Cannot use --all with --subnet-id. Please choose one.")
|
|
2111
|
+
return
|
|
2112
|
+
|
|
2113
|
+
if subnet_id is not None and subnet_id < 0:
|
|
2114
|
+
display_subnet_info_rpc(None)
|
|
2115
|
+
return
|
|
2116
|
+
|
|
2117
|
+
client = get_client()
|
|
2118
|
+
|
|
2119
|
+
if all_subnets:
|
|
2120
|
+
with HTCLILoadingContext("Fetching information for all subnets..."):
|
|
2121
|
+
subnet_models = client.rpc.subnet.get_all_subnets_info()
|
|
2122
|
+
|
|
2123
|
+
display_subnets_overview(subnet_models or [])
|
|
2124
|
+
return
|
|
2125
|
+
|
|
2126
|
+
if subnet_id is None:
|
|
2127
|
+
from ...utils.prompts import integer_prompt
|
|
2128
|
+
|
|
2129
|
+
subnet_id = integer_prompt("Enter subnet ID")
|
|
2130
|
+
|
|
2131
|
+
with HTCLILoadingContext(f"Fetching subnet {subnet_id} information..."):
|
|
2132
|
+
subnet_model = client.rpc.subnet.get_subnet_info(subnet_id)
|
|
2133
|
+
|
|
2134
|
+
# Convert Pydantic model to dict for display
|
|
2135
|
+
if subnet_model:
|
|
2136
|
+
# Use Pydantic's model_dump() to get all fields as dict
|
|
2137
|
+
subnet_dict = subnet_model.model_dump()
|
|
2138
|
+
|
|
2139
|
+
# Convert bytes fields to strings for display
|
|
2140
|
+
if isinstance(subnet_dict.get("name"), bytes):
|
|
2141
|
+
decoded_name = subnet_dict["name"].decode("utf-8")
|
|
2142
|
+
subnet_dict["name"] = decoded_name if decoded_name else "Not set"
|
|
2143
|
+
if isinstance(subnet_dict.get("description"), bytes):
|
|
2144
|
+
decoded_desc = subnet_dict["description"].decode("utf-8")
|
|
2145
|
+
subnet_dict["description"] = decoded_desc if decoded_desc else "Not set"
|
|
2146
|
+
if isinstance(subnet_dict.get("repo"), bytes):
|
|
2147
|
+
decoded_repo = subnet_dict["repo"].decode("utf-8")
|
|
2148
|
+
subnet_dict["repo"] = decoded_repo if decoded_repo else "Not set"
|
|
2149
|
+
if isinstance(subnet_dict.get("tags"), bytes):
|
|
2150
|
+
decoded_tags = subnet_dict["tags"].decode("utf-8")
|
|
2151
|
+
subnet_dict["tags"] = decoded_tags if decoded_tags else "Not set"
|
|
2152
|
+
|
|
2153
|
+
# Handle empty sets for initial_coldkeys
|
|
2154
|
+
if (
|
|
2155
|
+
isinstance(subnet_dict.get("initial_coldkeys"), set)
|
|
2156
|
+
and not subnet_dict["initial_coldkeys"]
|
|
2157
|
+
):
|
|
2158
|
+
subnet_dict["initial_coldkeys"] = (
|
|
2159
|
+
None # Will display as "None" or can be handled by display function
|
|
2160
|
+
)
|
|
2161
|
+
|
|
2162
|
+
# Convert state enum to string
|
|
2163
|
+
if hasattr(subnet_dict.get("state"), "value"):
|
|
2164
|
+
subnet_dict["state"] = subnet_dict["state"].value
|
|
2165
|
+
elif hasattr(subnet_dict.get("state"), "name"):
|
|
2166
|
+
subnet_dict["state"] = subnet_dict["state"].name
|
|
2167
|
+
|
|
2168
|
+
display_subnet_info_rpc(subnet_dict)
|
|
2169
|
+
else:
|
|
2170
|
+
display_subnet_info_rpc(None)
|
|
2171
|
+
|
|
2172
|
+
except Exception as e:
|
|
2173
|
+
handle_and_display_error(e)
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
def get_subnet_nodes_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
|
|
2177
|
+
"""Handle getting subnet nodes using RPC. Can filter by subnet_id or coldkey."""
|
|
2178
|
+
try:
|
|
2179
|
+
if subnet_id is not None and subnet_id < 0:
|
|
2180
|
+
print_info(f"No nodes found for subnet {subnet_id}")
|
|
2181
|
+
return
|
|
2182
|
+
|
|
2183
|
+
client = get_client()
|
|
2184
|
+
|
|
2185
|
+
# Prioritize subnet_id when both are provided to ensure consistency
|
|
2186
|
+
# This ensures we get all nodes for the subnet, then filter by coldkey
|
|
2187
|
+
if subnet_id is not None:
|
|
2188
|
+
# Use subnet_id to get nodes
|
|
2189
|
+
with HTCLILoadingContext(f"Fetching nodes for subnet {subnet_id}..."):
|
|
2190
|
+
all_nodes = client.rpc.node.get_subnet_nodes_info(subnet_id)
|
|
2191
|
+
|
|
2192
|
+
# If coldkey is also provided, filter by coldkey
|
|
2193
|
+
if coldkey:
|
|
2194
|
+
# Resolve coldkey if it's a wallet name
|
|
2195
|
+
if not (coldkey.startswith("0x") or len(coldkey) in (48, 49)):
|
|
2196
|
+
# Treat as wallet name: resolve to its address if exists
|
|
2197
|
+
try:
|
|
2198
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
2199
|
+
|
|
2200
|
+
info = get_wallet_info_by_name(coldkey, is_hotkey=False)
|
|
2201
|
+
# Prefer EVM address, fallback to ss58_address
|
|
2202
|
+
coldkey = (
|
|
2203
|
+
info.get("evm_address")
|
|
2204
|
+
or info.get("address")
|
|
2205
|
+
or info.get("ss58_address")
|
|
2206
|
+
or coldkey
|
|
2207
|
+
)
|
|
2208
|
+
except FileNotFoundError:
|
|
2209
|
+
# Wallet name not found - show error and exit
|
|
2210
|
+
print_error(f"Wallet '{coldkey}' not found")
|
|
2211
|
+
return
|
|
2212
|
+
except Exception:
|
|
2213
|
+
# Leave as provided; RPC layer will handle format checks
|
|
2214
|
+
pass
|
|
2215
|
+
|
|
2216
|
+
# Normalize Ethereum addresses to lowercase (addresses are case-insensitive)
|
|
2217
|
+
if coldkey and coldkey.startswith("0x"):
|
|
2218
|
+
coldkey = coldkey.lower()
|
|
2219
|
+
|
|
2220
|
+
# Filter nodes by coldkey
|
|
2221
|
+
nodes_models = [
|
|
2222
|
+
node
|
|
2223
|
+
for node in all_nodes
|
|
2224
|
+
if node.coldkey and node.coldkey.lower() == coldkey.lower()
|
|
2225
|
+
]
|
|
2226
|
+
logger.debug(
|
|
2227
|
+
f"After filtering by coldkey {coldkey}: {len(nodes_models)} nodes"
|
|
2228
|
+
)
|
|
2229
|
+
else:
|
|
2230
|
+
nodes_models = all_nodes
|
|
2231
|
+
|
|
2232
|
+
elif coldkey:
|
|
2233
|
+
# Only coldkey provided (no subnet_id) - use coldkey RPC method
|
|
2234
|
+
# Resolve coldkey and fetch nodes
|
|
2235
|
+
subnet_id = None # Ensure subnet_id is defined for display logic
|
|
2236
|
+
with HTCLILoadingContext("Resolving coldkey and fetching nodes..."):
|
|
2237
|
+
# If coldkey looks like a wallet name, resolve to address
|
|
2238
|
+
if not (coldkey.startswith("0x") or len(coldkey) in (48, 49)):
|
|
2239
|
+
# Treat as wallet name: resolve to its address if exists
|
|
2240
|
+
try:
|
|
2241
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
2242
|
+
|
|
2243
|
+
info = get_wallet_info_by_name(coldkey, is_hotkey=False)
|
|
2244
|
+
# Prefer EVM address, fallback to ss58_address
|
|
2245
|
+
coldkey = (
|
|
2246
|
+
info.get("evm_address")
|
|
2247
|
+
or info.get("address")
|
|
2248
|
+
or info.get("ss58_address")
|
|
2249
|
+
or coldkey
|
|
2250
|
+
)
|
|
2251
|
+
except FileNotFoundError:
|
|
2252
|
+
# Wallet name not found - show error and exit
|
|
2253
|
+
print_error(f"Wallet '{coldkey}' not found")
|
|
2254
|
+
return
|
|
2255
|
+
except Exception:
|
|
2256
|
+
# Leave as provided; RPC layer will handle format checks
|
|
2257
|
+
pass
|
|
2258
|
+
|
|
2259
|
+
# Normalize Ethereum addresses to lowercase (addresses are case-insensitive)
|
|
2260
|
+
if coldkey and coldkey.startswith("0x"):
|
|
2261
|
+
coldkey = coldkey.lower()
|
|
2262
|
+
|
|
2263
|
+
# Get nodes for this coldkey using RPC method
|
|
2264
|
+
nodes_models = client.rpc.node.get_coldkey_subnet_nodes_info(coldkey)
|
|
2265
|
+
logger.debug(
|
|
2266
|
+
f"Retrieved {len(nodes_models)} total nodes for coldkey {coldkey}"
|
|
2267
|
+
)
|
|
2268
|
+
if nodes_models:
|
|
2269
|
+
logger.debug(
|
|
2270
|
+
f"Node subnet IDs: {[node.subnet_id for node in nodes_models]}"
|
|
2271
|
+
)
|
|
2272
|
+
|
|
2273
|
+
else:
|
|
2274
|
+
# Neither provided - prompt for subnet_id
|
|
2275
|
+
from ...utils.prompts import integer_prompt
|
|
2276
|
+
|
|
2277
|
+
subnet_id = integer_prompt("Enter subnet ID")
|
|
2278
|
+
with HTCLILoadingContext(f"Fetching nodes for subnet {subnet_id}..."):
|
|
2279
|
+
nodes_models = client.rpc.node.get_subnet_nodes_info(subnet_id)
|
|
2280
|
+
|
|
2281
|
+
# Convert Pydantic models to dicts for display
|
|
2282
|
+
nodes_data = []
|
|
2283
|
+
for node in nodes_models:
|
|
2284
|
+
try:
|
|
2285
|
+
node_dict = {
|
|
2286
|
+
"id": node.subnet_node_id,
|
|
2287
|
+
"subnet_id": node.subnet_id,
|
|
2288
|
+
"coldkey": node.coldkey,
|
|
2289
|
+
"hotkey": node.hotkey,
|
|
2290
|
+
"stake_balance": node.stake_balance,
|
|
2291
|
+
"node_delegate_stake_balance": node.node_delegate_stake_balance,
|
|
2292
|
+
"status": (
|
|
2293
|
+
str(node.classification.get("node_class", "Unknown"))
|
|
2294
|
+
if isinstance(node.classification, dict)
|
|
2295
|
+
else str(node.classification)
|
|
2296
|
+
),
|
|
2297
|
+
}
|
|
2298
|
+
nodes_data.append(node_dict)
|
|
2299
|
+
logger.debug(
|
|
2300
|
+
f"Added node {node.subnet_node_id} (subnet {node.subnet_id}, coldkey {node.coldkey})"
|
|
2301
|
+
)
|
|
2302
|
+
except Exception as e:
|
|
2303
|
+
logger.warning(
|
|
2304
|
+
f"Failed to convert node {getattr(node, 'subnet_node_id', 'unknown')} to dict: {e}"
|
|
2305
|
+
)
|
|
2306
|
+
continue
|
|
2307
|
+
|
|
2308
|
+
# Display nodes (empty list if none)
|
|
2309
|
+
if not nodes_data:
|
|
2310
|
+
if coldkey:
|
|
2311
|
+
print_info(f"No nodes found for coldkey: {coldkey}")
|
|
2312
|
+
else:
|
|
2313
|
+
print_info(f"No nodes found for subnet {subnet_id}")
|
|
2314
|
+
return
|
|
2315
|
+
|
|
2316
|
+
# Use subnet_id if provided, otherwise None (for coldkey listing)
|
|
2317
|
+
display_subnet_nodes_rpc(nodes_data, subnet_id if subnet_id else None)
|
|
2318
|
+
|
|
2319
|
+
except Exception as e:
|
|
2320
|
+
handle_and_display_node_error(e, operation="list")
|
|
2321
|
+
|
|
2322
|
+
|
|
2323
|
+
def get_bootnodes_handler(subnet_id: Optional[int]):
|
|
2324
|
+
"""Handle getting subnet bootnodes using RPC."""
|
|
2325
|
+
try:
|
|
2326
|
+
if subnet_id is None:
|
|
2327
|
+
from ...utils.prompts import integer_prompt
|
|
2328
|
+
|
|
2329
|
+
subnet_id = integer_prompt("Enter subnet ID")
|
|
2330
|
+
|
|
2331
|
+
if subnet_id < 0:
|
|
2332
|
+
|
|
2333
|
+
class BootnodesData:
|
|
2334
|
+
def __init__(self):
|
|
2335
|
+
self.bootnodes = []
|
|
2336
|
+
self.node_bootnodes = []
|
|
2337
|
+
|
|
2338
|
+
display_bootnodes_rpc(BootnodesData(), subnet_id)
|
|
2339
|
+
return
|
|
2340
|
+
|
|
2341
|
+
client = get_client()
|
|
2342
|
+
|
|
2343
|
+
with HTCLILoadingContext(f"Fetching bootnodes for subnet {subnet_id}..."):
|
|
2344
|
+
bootnodes_dict = client.rpc.subnet.get_subnet_bootnodes(subnet_id)
|
|
2345
|
+
|
|
2346
|
+
# Helper to decode bytes to string
|
|
2347
|
+
def decode_list(items):
|
|
2348
|
+
decoded_items = []
|
|
2349
|
+
for item in items:
|
|
2350
|
+
if isinstance(item, bytes):
|
|
2351
|
+
try:
|
|
2352
|
+
decoded_items.append(item.decode("utf-8"))
|
|
2353
|
+
except Exception:
|
|
2354
|
+
decoded_items.append(str(item))
|
|
2355
|
+
else:
|
|
2356
|
+
decoded_items.append(str(item))
|
|
2357
|
+
return decoded_items
|
|
2358
|
+
|
|
2359
|
+
# Wrap bootnodes in object for display (display expects .bootnodes and .node_bootnodes)
|
|
2360
|
+
class BootnodesData:
|
|
2361
|
+
def __init__(self, bootnodes, node_bootnodes):
|
|
2362
|
+
self.bootnodes = decode_list(bootnodes)
|
|
2363
|
+
self.node_bootnodes = decode_list(node_bootnodes)
|
|
2364
|
+
|
|
2365
|
+
# Extract lists from the dictionary returned by get_subnet_bootnodes
|
|
2366
|
+
bootnodes_data = BootnodesData(
|
|
2367
|
+
bootnodes_dict.get("bootnodes", []),
|
|
2368
|
+
bootnodes_dict.get("node_bootnodes", []),
|
|
2369
|
+
)
|
|
2370
|
+
|
|
2371
|
+
# Display bootnodes
|
|
2372
|
+
display_bootnodes_rpc(bootnodes_data, subnet_id)
|
|
2373
|
+
|
|
2374
|
+
except Exception as e:
|
|
2375
|
+
handle_and_display_error(e)
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
def owner_set_emergency_validator_set_handler(
|
|
2379
|
+
subnet_id: Optional[int], node_ids: Optional[str], coldkey: Optional[str] = None
|
|
2380
|
+
):
|
|
2381
|
+
"""Handle setting emergency validator set."""
|
|
2382
|
+
try:
|
|
2383
|
+
from .prompts import prompt_set_emergency_validator_set
|
|
2384
|
+
|
|
2385
|
+
if subnet_id is None or node_ids is None:
|
|
2386
|
+
subnet_id, node_ids = prompt_set_emergency_validator_set(
|
|
2387
|
+
subnet_id, node_ids
|
|
2388
|
+
)
|
|
2389
|
+
|
|
2390
|
+
# Parse node IDs from comma-separated string
|
|
2391
|
+
node_ids_list = [int(nid.strip()) for nid in node_ids.split(",") if nid.strip()]
|
|
2392
|
+
|
|
2393
|
+
# Get signing wallet
|
|
2394
|
+
if coldkey:
|
|
2395
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
2396
|
+
|
|
2397
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
2398
|
+
else:
|
|
2399
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
2400
|
+
wallet_type="coldkey",
|
|
2401
|
+
purpose="sign the transaction",
|
|
2402
|
+
only_existing_wallets=True,
|
|
2403
|
+
)
|
|
2404
|
+
|
|
2405
|
+
client = get_client()
|
|
2406
|
+
if not client.connect():
|
|
2407
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
2408
|
+
|
|
2409
|
+
with HTCLILoadingContext(
|
|
2410
|
+
f"Setting emergency validator set for subnet {subnet_id}..."
|
|
2411
|
+
):
|
|
2412
|
+
result = client.extrinsics.subnet.owner_set_emergency_validator_set(
|
|
2413
|
+
subnet_id=subnet_id, subnet_node_ids=node_ids_list, keypair=keypair
|
|
2414
|
+
)
|
|
2415
|
+
|
|
2416
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
2417
|
+
error_msg = result.get("error", "Operation failed")
|
|
2418
|
+
if handle_subnet_error(
|
|
2419
|
+
error_msg, subnet_id, "set emergency validator set", client
|
|
2420
|
+
):
|
|
2421
|
+
return
|
|
2422
|
+
|
|
2423
|
+
from .display import display_emergency_validator_set_result
|
|
2424
|
+
|
|
2425
|
+
display_emergency_validator_set_result(
|
|
2426
|
+
result, subnet_id, node_ids_list, action="set"
|
|
2427
|
+
)
|
|
2428
|
+
|
|
2429
|
+
except Exception as e:
|
|
2430
|
+
error_msg = str(e)
|
|
2431
|
+
if not handle_subnet_error(error_msg, subnet_id, "set emergency validator set"):
|
|
2432
|
+
handle_and_display_error(e)
|
|
2433
|
+
|
|
2434
|
+
|
|
2435
|
+
def owner_clear_emergency_validator_set_handler(
|
|
2436
|
+
subnet_id: Optional[int], coldkey: Optional[str] = None
|
|
2437
|
+
):
|
|
2438
|
+
"""Handle clearing emergency validator set."""
|
|
2439
|
+
try:
|
|
2440
|
+
from .prompts import prompt_clear_emergency_validator_set
|
|
2441
|
+
|
|
2442
|
+
if subnet_id is None:
|
|
2443
|
+
subnet_id = prompt_clear_emergency_validator_set()
|
|
2444
|
+
|
|
2445
|
+
# Get signing wallet
|
|
2446
|
+
if coldkey:
|
|
2447
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
2448
|
+
|
|
2449
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
2450
|
+
else:
|
|
2451
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
2452
|
+
wallet_type="coldkey",
|
|
2453
|
+
purpose="sign the transaction",
|
|
2454
|
+
only_existing_wallets=True,
|
|
2455
|
+
)
|
|
2456
|
+
|
|
2457
|
+
client = get_client()
|
|
2458
|
+
if not client.connect():
|
|
2459
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
2460
|
+
|
|
2461
|
+
# Clear by setting empty list
|
|
2462
|
+
with HTCLILoadingContext(
|
|
2463
|
+
f"Clearing emergency validator set for subnet {subnet_id}..."
|
|
2464
|
+
):
|
|
2465
|
+
result = client.extrinsics.subnet.owner_set_emergency_validator_set(
|
|
2466
|
+
subnet_id=subnet_id, subnet_node_ids=[], keypair=keypair
|
|
2467
|
+
)
|
|
2468
|
+
|
|
2469
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
2470
|
+
error_msg = result.get("error", "Operation failed")
|
|
2471
|
+
if handle_subnet_error(
|
|
2472
|
+
error_msg, subnet_id, "clear emergency validator set", client
|
|
2473
|
+
):
|
|
2474
|
+
return
|
|
2475
|
+
|
|
2476
|
+
from .display import display_emergency_validator_set_result
|
|
2477
|
+
|
|
2478
|
+
display_emergency_validator_set_result(result, subnet_id, [], action="clear")
|
|
2479
|
+
|
|
2480
|
+
except Exception as e:
|
|
2481
|
+
error_msg = str(e)
|
|
2482
|
+
if not handle_subnet_error(
|
|
2483
|
+
error_msg, subnet_id, "clear emergency validator set"
|
|
2484
|
+
):
|
|
2485
|
+
handle_and_display_error(e)
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
def owner_add_bootnode_access_handler(
|
|
2489
|
+
subnet_id: Optional[int], account: Optional[str], coldkey: Optional[str] = None
|
|
2490
|
+
):
|
|
2491
|
+
"""Handle adding bootnode access."""
|
|
2492
|
+
try:
|
|
2493
|
+
from .prompts import prompt_bootnode_access
|
|
2494
|
+
from ...utils.blockchain.validation import validate_address
|
|
2495
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
2496
|
+
from ...utils.wallet.crypto import public_key_to_evm_address
|
|
2497
|
+
|
|
2498
|
+
if subnet_id is None or account is None:
|
|
2499
|
+
subnet_id, account = prompt_bootnode_access(
|
|
2500
|
+
subnet_id, account, action="add"
|
|
2501
|
+
)
|
|
2502
|
+
|
|
2503
|
+
# Resolve account: can be either EVM address or wallet name
|
|
2504
|
+
original_account = account
|
|
2505
|
+
if account.startswith("0x"):
|
|
2506
|
+
# Already an EVM address - normalize to lowercase
|
|
2507
|
+
account = account.lower()
|
|
2508
|
+
else:
|
|
2509
|
+
# Try to resolve as wallet name
|
|
2510
|
+
try:
|
|
2511
|
+
wallet_info = get_wallet_info_by_name(
|
|
2512
|
+
account,
|
|
2513
|
+
is_hotkey=False, # Looking for coldkey wallets
|
|
2514
|
+
)
|
|
2515
|
+
|
|
2516
|
+
if wallet_info:
|
|
2517
|
+
# Try to get EVM address from wallet info
|
|
2518
|
+
resolved_address = wallet_info.get(
|
|
2519
|
+
"evm_address"
|
|
2520
|
+
) or wallet_info.get("address")
|
|
2521
|
+
|
|
2522
|
+
# If we have an address, check if it's EVM format
|
|
2523
|
+
if resolved_address and validate_address(resolved_address):
|
|
2524
|
+
account = resolved_address.lower()
|
|
2525
|
+
else:
|
|
2526
|
+
# Try to derive from public key
|
|
2527
|
+
public_key_hex = wallet_info.get("public_key")
|
|
2528
|
+
if public_key_hex:
|
|
2529
|
+
try:
|
|
2530
|
+
resolved_address = public_key_to_evm_address(
|
|
2531
|
+
bytes.fromhex(public_key_hex)
|
|
2532
|
+
)
|
|
2533
|
+
if validate_address(resolved_address):
|
|
2534
|
+
account = resolved_address.lower()
|
|
2535
|
+
else:
|
|
2536
|
+
raise ValueError(
|
|
2537
|
+
f"Wallet '{original_account}' does not have a valid EVM address. "
|
|
2538
|
+
"Please use an EVM address (0x...) directly."
|
|
2539
|
+
)
|
|
2540
|
+
except Exception as e:
|
|
2541
|
+
raise ValueError(
|
|
2542
|
+
f"Failed to derive EVM address from wallet '{original_account}': {str(e)}"
|
|
2543
|
+
) from e
|
|
2544
|
+
else:
|
|
2545
|
+
raise ValueError(
|
|
2546
|
+
f"Wallet '{original_account}' does not have a valid EVM address. "
|
|
2547
|
+
"Please provide an EVM address (0x...) directly."
|
|
2548
|
+
)
|
|
2549
|
+
else:
|
|
2550
|
+
raise ValueError(f"Wallet '{account}' not found")
|
|
2551
|
+
except FileNotFoundError:
|
|
2552
|
+
raise ValueError(
|
|
2553
|
+
f"Wallet '{account}' not found. "
|
|
2554
|
+
"Please provide either a wallet name or a valid EVM address (0x...)."
|
|
2555
|
+
)
|
|
2556
|
+
except ValueError:
|
|
2557
|
+
# Re-raise ValueError as-is
|
|
2558
|
+
raise
|
|
2559
|
+
except Exception as e:
|
|
2560
|
+
# For other errors, provide helpful message
|
|
2561
|
+
raise ValueError(
|
|
2562
|
+
f"Failed to resolve account '{account}': {str(e)}. "
|
|
2563
|
+
"Please provide either a wallet name or a valid EVM address (0x...)."
|
|
2564
|
+
)
|
|
2565
|
+
|
|
2566
|
+
# Get signing wallet
|
|
2567
|
+
if coldkey:
|
|
2568
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
2569
|
+
|
|
2570
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
2571
|
+
else:
|
|
2572
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
2573
|
+
wallet_type="coldkey",
|
|
2574
|
+
purpose="sign the transaction",
|
|
2575
|
+
only_existing_wallets=True,
|
|
2576
|
+
)
|
|
2577
|
+
|
|
2578
|
+
client = get_client()
|
|
2579
|
+
if not client.connect():
|
|
2580
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
2581
|
+
|
|
2582
|
+
with HTCLILoadingContext(f"Adding bootnode access for subnet {subnet_id}..."):
|
|
2583
|
+
result = client.extrinsics.subnet.owner_add_bootnode_access(
|
|
2584
|
+
subnet_id=subnet_id, new_account=account, keypair=keypair
|
|
2585
|
+
)
|
|
2586
|
+
|
|
2587
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
2588
|
+
error_msg = result.get("error", "Operation failed")
|
|
2589
|
+
if handle_subnet_error(error_msg, subnet_id, "add bootnode access", client):
|
|
2590
|
+
return
|
|
2591
|
+
|
|
2592
|
+
from .display import display_bootnode_access_result
|
|
2593
|
+
|
|
2594
|
+
display_bootnode_access_result(result, subnet_id, account, action="add")
|
|
2595
|
+
|
|
2596
|
+
except Exception as e:
|
|
2597
|
+
error_msg = str(e)
|
|
2598
|
+
if not handle_subnet_error(error_msg, subnet_id, "add bootnode access"):
|
|
2599
|
+
handle_and_display_error(e)
|
|
2600
|
+
|
|
2601
|
+
|
|
2602
|
+
def owner_remove_bootnode_access_handler(
|
|
2603
|
+
subnet_id: Optional[int], account: Optional[str], coldkey: Optional[str] = None
|
|
2604
|
+
):
|
|
2605
|
+
"""Handle removing bootnode access."""
|
|
2606
|
+
try:
|
|
2607
|
+
from .prompts import prompt_bootnode_access
|
|
2608
|
+
from ...utils.blockchain.validation import validate_address
|
|
2609
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
2610
|
+
from ...utils.wallet.crypto import public_key_to_evm_address
|
|
2611
|
+
|
|
2612
|
+
if subnet_id is None or account is None:
|
|
2613
|
+
subnet_id, account = prompt_bootnode_access(
|
|
2614
|
+
subnet_id, account, action="remove"
|
|
2615
|
+
)
|
|
2616
|
+
|
|
2617
|
+
# Resolve account: can be either EVM address or wallet name
|
|
2618
|
+
original_account = account
|
|
2619
|
+
if account.startswith("0x"):
|
|
2620
|
+
# Already an EVM address - normalize to lowercase
|
|
2621
|
+
account = account.lower()
|
|
2622
|
+
else:
|
|
2623
|
+
# Try to resolve as wallet name
|
|
2624
|
+
try:
|
|
2625
|
+
wallet_info = get_wallet_info_by_name(
|
|
2626
|
+
account,
|
|
2627
|
+
is_hotkey=False, # Looking for coldkey wallets
|
|
2628
|
+
)
|
|
2629
|
+
|
|
2630
|
+
if wallet_info:
|
|
2631
|
+
# Try to get EVM address from wallet info
|
|
2632
|
+
resolved_address = wallet_info.get(
|
|
2633
|
+
"evm_address"
|
|
2634
|
+
) or wallet_info.get("address")
|
|
2635
|
+
|
|
2636
|
+
# If we have an address, check if it's EVM format
|
|
2637
|
+
if resolved_address and validate_address(resolved_address):
|
|
2638
|
+
account = resolved_address.lower()
|
|
2639
|
+
else:
|
|
2640
|
+
# Try to derive from public key
|
|
2641
|
+
public_key_hex = wallet_info.get("public_key")
|
|
2642
|
+
if public_key_hex:
|
|
2643
|
+
try:
|
|
2644
|
+
resolved_address = public_key_to_evm_address(
|
|
2645
|
+
bytes.fromhex(public_key_hex)
|
|
2646
|
+
)
|
|
2647
|
+
if validate_address(resolved_address):
|
|
2648
|
+
account = resolved_address.lower()
|
|
2649
|
+
else:
|
|
2650
|
+
raise ValueError(
|
|
2651
|
+
f"Wallet '{original_account}' does not have a valid EVM address. "
|
|
2652
|
+
"Please use an EVM address (0x...) directly."
|
|
2653
|
+
)
|
|
2654
|
+
except Exception as e:
|
|
2655
|
+
raise ValueError(
|
|
2656
|
+
f"Failed to derive EVM address from wallet '{original_account}': {str(e)}"
|
|
2657
|
+
) from e
|
|
2658
|
+
else:
|
|
2659
|
+
raise ValueError(
|
|
2660
|
+
f"Wallet '{original_account}' does not have a valid EVM address. "
|
|
2661
|
+
"Please use an EVM address (0x...) directly."
|
|
2662
|
+
)
|
|
2663
|
+
else:
|
|
2664
|
+
raise ValueError(f"Wallet '{account}' not found")
|
|
2665
|
+
except FileNotFoundError:
|
|
2666
|
+
raise ValueError(
|
|
2667
|
+
f"Wallet '{account}' not found. "
|
|
2668
|
+
"Please provide either a wallet name or a valid EVM address (0x...)."
|
|
2669
|
+
)
|
|
2670
|
+
except ValueError:
|
|
2671
|
+
# Re-raise ValueError as-is
|
|
2672
|
+
raise
|
|
2673
|
+
except Exception as e:
|
|
2674
|
+
# For other errors, provide helpful message
|
|
2675
|
+
raise ValueError(
|
|
2676
|
+
f"Failed to resolve account '{account}': {str(e)}. "
|
|
2677
|
+
"Please provide either a wallet name or a valid EVM address (0x...)."
|
|
2678
|
+
)
|
|
2679
|
+
|
|
2680
|
+
# Get signing wallet
|
|
2681
|
+
if coldkey:
|
|
2682
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
2683
|
+
|
|
2684
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
2685
|
+
else:
|
|
2686
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
2687
|
+
wallet_type="coldkey",
|
|
2688
|
+
purpose="sign the transaction",
|
|
2689
|
+
only_existing_wallets=True,
|
|
2690
|
+
)
|
|
2691
|
+
|
|
2692
|
+
client = get_client()
|
|
2693
|
+
if not client.connect():
|
|
2694
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
2695
|
+
|
|
2696
|
+
with HTCLILoadingContext(f"Removing bootnode access for subnet {subnet_id}..."):
|
|
2697
|
+
result = client.extrinsics.subnet.owner_remove_bootnode_access(
|
|
2698
|
+
subnet_id=subnet_id, remove_account=account, keypair=keypair
|
|
2699
|
+
)
|
|
2700
|
+
|
|
2701
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
2702
|
+
error_msg = result.get("error", "Operation failed")
|
|
2703
|
+
if handle_subnet_error(
|
|
2704
|
+
error_msg, subnet_id, "remove bootnode access", client
|
|
2705
|
+
):
|
|
2706
|
+
return
|
|
2707
|
+
|
|
2708
|
+
from .display import display_bootnode_access_result
|
|
2709
|
+
|
|
2710
|
+
display_bootnode_access_result(result, subnet_id, account, action="remove")
|
|
2711
|
+
|
|
2712
|
+
except Exception as e:
|
|
2713
|
+
error_msg = str(e)
|
|
2714
|
+
if not handle_subnet_error(error_msg, subnet_id, "remove bootnode access"):
|
|
2715
|
+
handle_and_display_error(e)
|
|
2716
|
+
|
|
2717
|
+
|
|
2718
|
+
def owner_add_bootnode_handler(
|
|
2719
|
+
subnet_id: Optional[int],
|
|
2720
|
+
bootnode_address: Optional[str],
|
|
2721
|
+
coldkey: Optional[str] = None,
|
|
2722
|
+
):
|
|
2723
|
+
"""Handle adding a bootnode address."""
|
|
2724
|
+
try:
|
|
2725
|
+
from ...ui.prompts import integer_prompt, text_prompt
|
|
2726
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
2727
|
+
|
|
2728
|
+
if subnet_id is None:
|
|
2729
|
+
subnet_id = integer_prompt("Enter the Subnet ID (UID)", min_value=0)
|
|
2730
|
+
|
|
2731
|
+
if bootnode_address is None:
|
|
2732
|
+
bootnode_address = text_prompt(
|
|
2733
|
+
"Enter the bootnode address (multiaddr)", required=True
|
|
2734
|
+
)
|
|
2735
|
+
|
|
2736
|
+
import re
|
|
2737
|
+
|
|
2738
|
+
# Regex for multiaddr: /ip4/IP/tcp/PORT/p2p/PEER_ID
|
|
2739
|
+
multiaddr_pattern = r"^/(ip4|ip6)/([^/]+)/(tcp|udp)/(\d+)/p2p/([a-zA-Z0-9]+)$"
|
|
2740
|
+
if not re.match(multiaddr_pattern, bootnode_address):
|
|
2741
|
+
from ...ui.display import print_error
|
|
2742
|
+
|
|
2743
|
+
print_error("Invalid bootnode address format.")
|
|
2744
|
+
print_error("Expected format: /ip4/<IP>/tcp/<PORT>/p2p/<PEER_ID>")
|
|
2745
|
+
print_error("Example: /ip4/127.0.0.1/tcp/30333/p2p/12D3KooW...")
|
|
2746
|
+
return
|
|
2747
|
+
|
|
2748
|
+
# Get keypair
|
|
2749
|
+
if coldkey:
|
|
2750
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
2751
|
+
else:
|
|
2752
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
2753
|
+
wallet_type="coldkey",
|
|
2754
|
+
purpose="sign the transaction",
|
|
2755
|
+
only_existing_wallets=True,
|
|
2756
|
+
)
|
|
2757
|
+
|
|
2758
|
+
client = get_client()
|
|
2759
|
+
if not client.connect():
|
|
2760
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
2761
|
+
|
|
2762
|
+
with HTCLILoadingContext(f"Adding bootnode to subnet {subnet_id}..."):
|
|
2763
|
+
# Ensure it is a list for the client method
|
|
2764
|
+
result = client.extrinsics.subnet.update_bootnodes(
|
|
2765
|
+
subnet_id=subnet_id,
|
|
2766
|
+
add_bootnodes=[bootnode_address],
|
|
2767
|
+
remove_bootnodes=[],
|
|
2768
|
+
keypair=keypair,
|
|
2769
|
+
)
|
|
2770
|
+
|
|
2771
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
2772
|
+
error_msg = result.get("error", "Operation failed")
|
|
2773
|
+
if handle_subnet_error(error_msg, subnet_id, "add bootnode", client):
|
|
2774
|
+
return
|
|
2775
|
+
|
|
2776
|
+
from ...ui.display import print_success
|
|
2777
|
+
|
|
2778
|
+
print_success(f"Successfully added bootnode to subnet {subnet_id}")
|
|
2779
|
+
if result.get("message"):
|
|
2780
|
+
print_info(result["message"])
|
|
2781
|
+
|
|
2782
|
+
except Exception as e:
|
|
2783
|
+
error_msg = str(e)
|
|
2784
|
+
if not handle_subnet_error(error_msg, subnet_id, "add bootnode"):
|
|
2785
|
+
handle_and_display_error(e)
|
|
2786
|
+
|
|
2787
|
+
|
|
2788
|
+
def owner_remove_bootnode_handler(
|
|
2789
|
+
subnet_id: Optional[int],
|
|
2790
|
+
bootnode_address: Optional[str],
|
|
2791
|
+
coldkey: Optional[str] = None,
|
|
2792
|
+
):
|
|
2793
|
+
"""Handle removing a bootnode address."""
|
|
2794
|
+
try:
|
|
2795
|
+
from ...ui.prompts import integer_prompt, text_prompt
|
|
2796
|
+
from ...utils.wallet.core import resolve_coldkey_and_get_keypair
|
|
2797
|
+
|
|
2798
|
+
if subnet_id is None:
|
|
2799
|
+
subnet_id = integer_prompt("Enter the Subnet ID (UID)", min_value=0)
|
|
2800
|
+
|
|
2801
|
+
if bootnode_address is None:
|
|
2802
|
+
bootnode_address = text_prompt(
|
|
2803
|
+
"Enter the bootnode address (multiaddr) to remove", required=True
|
|
2804
|
+
)
|
|
2805
|
+
|
|
2806
|
+
import re
|
|
2807
|
+
|
|
2808
|
+
# Regex for multiaddr: /ip4/IP/tcp/PORT/p2p/PEER_ID
|
|
2809
|
+
multiaddr_pattern = r"^/(ip4|ip6)/([^/]+)/(tcp|udp)/(\d+)/p2p/([a-zA-Z0-9]+)$"
|
|
2810
|
+
if not re.match(multiaddr_pattern, bootnode_address):
|
|
2811
|
+
from ...ui.display import print_error
|
|
2812
|
+
|
|
2813
|
+
print_error("Invalid bootnode address format.")
|
|
2814
|
+
print_error("Expected format: /ip4/<IP>/tcp/<PORT>/p2p/<PEER_ID>")
|
|
2815
|
+
print_error("Example: /ip4/127.0.0.1/tcp/30333/p2p/12D3KooW...")
|
|
2816
|
+
return
|
|
2817
|
+
|
|
2818
|
+
# Get keypair
|
|
2819
|
+
if coldkey:
|
|
2820
|
+
wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
|
|
2821
|
+
else:
|
|
2822
|
+
wallet_name, keypair = retrieve_wallet_with_validation(
|
|
2823
|
+
wallet_type="coldkey",
|
|
2824
|
+
purpose="sign the transaction",
|
|
2825
|
+
only_existing_wallets=True,
|
|
2826
|
+
)
|
|
2827
|
+
|
|
2828
|
+
client = get_client()
|
|
2829
|
+
if not client.connect():
|
|
2830
|
+
raise RuntimeError("Could not connect to blockchain")
|
|
2831
|
+
|
|
2832
|
+
with HTCLILoadingContext(f"Removing bootnode from subnet {subnet_id}..."):
|
|
2833
|
+
# Ensure it is a list for the client method
|
|
2834
|
+
result = client.extrinsics.subnet.update_bootnodes(
|
|
2835
|
+
subnet_id=subnet_id,
|
|
2836
|
+
add_bootnodes=[],
|
|
2837
|
+
remove_bootnodes=[bootnode_address],
|
|
2838
|
+
keypair=keypair,
|
|
2839
|
+
)
|
|
2840
|
+
|
|
2841
|
+
if isinstance(result, dict) and not result.get("success", False):
|
|
2842
|
+
error_msg = result.get("error", "Operation failed")
|
|
2843
|
+
if handle_subnet_error(error_msg, subnet_id, "remove bootnode", client):
|
|
2844
|
+
return
|
|
2845
|
+
|
|
2846
|
+
from ...ui.display import print_success
|
|
2847
|
+
|
|
2848
|
+
print_success(f"Successfully removed bootnode from subnet {subnet_id}")
|
|
2849
|
+
if result.get("message"):
|
|
2850
|
+
print_info(result["message"])
|
|
2851
|
+
|
|
2852
|
+
except Exception as e:
|
|
2853
|
+
error_msg = str(e)
|
|
2854
|
+
if not handle_subnet_error(error_msg, subnet_id, "remove bootnode"):
|
|
2855
|
+
handle_and_display_error(e)
|