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.
Files changed (140) hide show
  1. htcli-1.1.0.dist-info/METADATA +509 -0
  2. htcli-1.1.0.dist-info/RECORD +140 -0
  3. htcli-1.1.0.dist-info/WHEEL +4 -0
  4. htcli-1.1.0.dist-info/entry_points.txt +2 -0
  5. htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
  6. src/__init__.py +0 -0
  7. src/htcli/__init__.py +5 -0
  8. src/htcli/client/__init__.py +338 -0
  9. src/htcli/client/extrinsics/__init__.py +26 -0
  10. src/htcli/client/extrinsics/base.py +487 -0
  11. src/htcli/client/extrinsics/consensus.py +79 -0
  12. src/htcli/client/extrinsics/governance.py +714 -0
  13. src/htcli/client/extrinsics/identity.py +490 -0
  14. src/htcli/client/extrinsics/node.py +1054 -0
  15. src/htcli/client/extrinsics/overwatch.py +401 -0
  16. src/htcli/client/extrinsics/staking.py +1504 -0
  17. src/htcli/client/extrinsics/subnet.py +2218 -0
  18. src/htcli/client/extrinsics/validator.py +203 -0
  19. src/htcli/client/extrinsics/wallet.py +323 -0
  20. src/htcli/client/offchain/__init__.py +10 -0
  21. src/htcli/client/offchain/backup.py +385 -0
  22. src/htcli/client/offchain/config.py +541 -0
  23. src/htcli/client/offchain/wallet.py +839 -0
  24. src/htcli/client/rpc/__init__.py +20 -0
  25. src/htcli/client/rpc/chain.py +568 -0
  26. src/htcli/client/rpc/node.py +783 -0
  27. src/htcli/client/rpc/overwatch.py +680 -0
  28. src/htcli/client/rpc/staking.py +216 -0
  29. src/htcli/client/rpc/subnet.py +2104 -0
  30. src/htcli/client/rpc/wallet.py +912 -0
  31. src/htcli/commands/__init__.py +31 -0
  32. src/htcli/commands/chain/__init__.py +66 -0
  33. src/htcli/commands/chain/display.py +204 -0
  34. src/htcli/commands/chain/handlers.py +260 -0
  35. src/htcli/commands/config/__init__.py +158 -0
  36. src/htcli/commands/config/display.py +353 -0
  37. src/htcli/commands/config/handlers.py +347 -0
  38. src/htcli/commands/config/prompts.py +357 -0
  39. src/htcli/commands/consensus/__init__.py +61 -0
  40. src/htcli/commands/consensus/handlers.py +100 -0
  41. src/htcli/commands/governance/__init__.py +49 -0
  42. src/htcli/commands/governance/handlers.py +81 -0
  43. src/htcli/commands/node/__init__.py +304 -0
  44. src/htcli/commands/node/display.py +749 -0
  45. src/htcli/commands/node/error_handling.py +470 -0
  46. src/htcli/commands/node/handlers.py +844 -0
  47. src/htcli/commands/node/prompts.py +346 -0
  48. src/htcli/commands/overwatch/__init__.py +219 -0
  49. src/htcli/commands/overwatch/display.py +396 -0
  50. src/htcli/commands/overwatch/error_handling.py +276 -0
  51. src/htcli/commands/overwatch/handlers.py +443 -0
  52. src/htcli/commands/overwatch/prompts.py +359 -0
  53. src/htcli/commands/stake/__init__.py +736 -0
  54. src/htcli/commands/stake/display.py +1103 -0
  55. src/htcli/commands/stake/error_handling.py +425 -0
  56. src/htcli/commands/stake/handlers.py +1902 -0
  57. src/htcli/commands/stake/prompts.py +1080 -0
  58. src/htcli/commands/subnet/__init__.py +639 -0
  59. src/htcli/commands/subnet/display.py +801 -0
  60. src/htcli/commands/subnet/error_handling.py +524 -0
  61. src/htcli/commands/subnet/handlers.py +2855 -0
  62. src/htcli/commands/subnet/prompts.py +1225 -0
  63. src/htcli/commands/validator/__init__.py +192 -0
  64. src/htcli/commands/validator/display.py +54 -0
  65. src/htcli/commands/validator/handlers.py +340 -0
  66. src/htcli/commands/wallet/__init__.py +546 -0
  67. src/htcli/commands/wallet/display.py +806 -0
  68. src/htcli/commands/wallet/error_handling.py +210 -0
  69. src/htcli/commands/wallet/handlers.py +3040 -0
  70. src/htcli/commands/wallet/prompts.py +1518 -0
  71. src/htcli/config.py +184 -0
  72. src/htcli/dependencies.py +186 -0
  73. src/htcli/errors/__init__.py +63 -0
  74. src/htcli/errors/base.py +141 -0
  75. src/htcli/errors/display.py +20 -0
  76. src/htcli/errors/handlers.py +710 -0
  77. src/htcli/main.py +343 -0
  78. src/htcli/models/__init__.py +21 -0
  79. src/htcli/models/enums/enum_types.py +35 -0
  80. src/htcli/models/errors.py +103 -0
  81. src/htcli/models/requests/__init__.py +197 -0
  82. src/htcli/models/requests/config.py +70 -0
  83. src/htcli/models/requests/consensus.py +19 -0
  84. src/htcli/models/requests/governance.py +38 -0
  85. src/htcli/models/requests/identity.py +51 -0
  86. src/htcli/models/requests/key.py +22 -0
  87. src/htcli/models/requests/node.py +91 -0
  88. src/htcli/models/requests/overwatch.py +64 -0
  89. src/htcli/models/requests/staking.py +580 -0
  90. src/htcli/models/requests/subnet.py +195 -0
  91. src/htcli/models/requests/validator.py +139 -0
  92. src/htcli/models/requests/wallet.py +118 -0
  93. src/htcli/models/responses/__init__.py +147 -0
  94. src/htcli/models/responses/base.py +18 -0
  95. src/htcli/models/responses/chain.py +39 -0
  96. src/htcli/models/responses/config.py +58 -0
  97. src/htcli/models/responses/identity.py +102 -0
  98. src/htcli/models/responses/overwatch.py +51 -0
  99. src/htcli/models/responses/staking.py +502 -0
  100. src/htcli/models/responses/subnet.py +856 -0
  101. src/htcli/models/responses/wallet.py +185 -0
  102. src/htcli/ui/__init__.py +87 -0
  103. src/htcli/ui/colors.py +309 -0
  104. src/htcli/ui/components/__init__.py +60 -0
  105. src/htcli/ui/components/panels.py +174 -0
  106. src/htcli/ui/components/progress.py +166 -0
  107. src/htcli/ui/components/spinners.py +92 -0
  108. src/htcli/ui/components/tables.py +809 -0
  109. src/htcli/ui/components/trees.py +721 -0
  110. src/htcli/ui/display.py +336 -0
  111. src/htcli/ui/prompts.py +870 -0
  112. src/htcli/utils/__init__.py +76 -0
  113. src/htcli/utils/blockchain/__init__.py +75 -0
  114. src/htcli/utils/blockchain/formatting.py +368 -0
  115. src/htcli/utils/blockchain/patches.py +286 -0
  116. src/htcli/utils/blockchain/peer_id.py +186 -0
  117. src/htcli/utils/blockchain/staking.py +448 -0
  118. src/htcli/utils/blockchain/type_registry.py +1373 -0
  119. src/htcli/utils/blockchain/validation.py +179 -0
  120. src/htcli/utils/cache.py +613 -0
  121. src/htcli/utils/constants.py +38 -0
  122. src/htcli/utils/legacy/__init__.py +12 -0
  123. src/htcli/utils/legacy/colors.py +311 -0
  124. src/htcli/utils/legacy/crypto.py +1176 -0
  125. src/htcli/utils/legacy/formatting.py +452 -0
  126. src/htcli/utils/legacy/interactive.py +306 -0
  127. src/htcli/utils/legacy/subnet_manifest.py +265 -0
  128. src/htcli/utils/legacy/validation.py +488 -0
  129. src/htcli/utils/logging.py +183 -0
  130. src/htcli/utils/network/__init__.py +20 -0
  131. src/htcli/utils/network/subnet.py +344 -0
  132. src/htcli/utils/prompts.py +27 -0
  133. src/htcli/utils/scale_codec.py +155 -0
  134. src/htcli/utils/validation/__init__.py +57 -0
  135. src/htcli/utils/validation/prompt_validators.py +267 -0
  136. src/htcli/utils/wallet/__init__.py +65 -0
  137. src/htcli/utils/wallet/auth.py +151 -0
  138. src/htcli/utils/wallet/core.py +1069 -0
  139. src/htcli/utils/wallet/crypto.py +1615 -0
  140. src/htcli/utils/wallet/migration.py +159 -0
@@ -0,0 +1,2218 @@
1
+ """
2
+ Subnet management extrinsics for HTCLI.
3
+ """
4
+
5
+ import ipaddress
6
+ from typing import Any, Optional
7
+
8
+ import base58
9
+ from substrateinterface import Keypair, SubstrateInterface
10
+ from substrateinterface.exceptions import SubstrateRequestException
11
+
12
+ from ...models.enums.enum_types import KeyType
13
+ from ...models.requests.subnet import (
14
+ SubnetActivateRequest,
15
+ SubnetConfigUpdateRequest,
16
+ SubnetOwnershipAcceptRequest,
17
+ SubnetOwnershipTransferRequest,
18
+ SubnetRegisterRequest,
19
+ SubnetRemoveRequest,
20
+ SubnetUpdateRequest,
21
+ )
22
+ from ...models.responses.subnet import SubnetInfo, SubnetRegisterResponse
23
+ from ...utils.blockchain.patches import patch_btreemap_accountid_u32_encoder
24
+ from ...utils.blockchain.peer_id import encode_peer_id
25
+ from ...utils.blockchain.validation import validate_address
26
+ from ...utils.logging import get_logger
27
+ from .base import BaseExtrinsicClient
28
+
29
+
30
+ # Simple response wrapper for methods that don't have specific response models
31
+ class SimpleExtrinsicResponse:
32
+ """Simple wrapper for extrinsic responses."""
33
+
34
+ def __init__(self, response_dict: dict):
35
+ self.success = response_dict.get("success", False)
36
+ self.error = response_dict.get("error")
37
+ self.transaction_hash = response_dict.get("extrinsic_hash")
38
+ self.block_number = response_dict.get("block_number")
39
+ self.block_hash = response_dict.get("block_hash")
40
+ self.message = response_dict.get("message")
41
+
42
+
43
+ logger = get_logger(__name__)
44
+
45
+
46
+ def _encode_unsigned_varint(value: int) -> bytes:
47
+ if value < 0:
48
+ raise ValueError("varint value cannot be negative")
49
+
50
+ encoded = bytearray()
51
+ while value >= 0x80:
52
+ encoded.append((value & 0x7F) | 0x80)
53
+ value >>= 7
54
+ encoded.append(value)
55
+ return bytes(encoded)
56
+
57
+
58
+ def _encode_multiaddr(multiaddr: str) -> bytes:
59
+ """Encode a supported multiaddr string into libp2p multiaddr bytes."""
60
+ protocols = {
61
+ "ip4": 4,
62
+ "tcp": 6,
63
+ "udp": 273,
64
+ "ip6": 41,
65
+ "dns": 53,
66
+ "dns4": 54,
67
+ "dns6": 55,
68
+ "dnsaddr": 56,
69
+ "p2p": 421,
70
+ }
71
+ parts = [part for part in multiaddr.split("/") if part]
72
+ encoded = bytearray()
73
+ index = 0
74
+
75
+ while index < len(parts):
76
+ proto = parts[index].lower()
77
+ if proto not in protocols:
78
+ raise ValueError(f"Unsupported multiaddr protocol: {proto}")
79
+ if index + 1 >= len(parts):
80
+ raise ValueError(f"Missing value for multiaddr protocol: {proto}")
81
+
82
+ value = parts[index + 1]
83
+ encoded += _encode_unsigned_varint(protocols[proto])
84
+
85
+ if proto == "ip4":
86
+ encoded += ipaddress.IPv4Address(value).packed
87
+ elif proto == "ip6":
88
+ encoded += ipaddress.IPv6Address(value).packed
89
+ elif proto in {"tcp", "udp"}:
90
+ port = int(value)
91
+ if not 0 <= port <= 65535:
92
+ raise ValueError(f"Invalid {proto} port: {port}")
93
+ encoded += port.to_bytes(2, byteorder="big", signed=False)
94
+ elif proto == "p2p":
95
+ peer_bytes = base58.b58decode(value)
96
+ encoded += _encode_unsigned_varint(len(peer_bytes))
97
+ encoded += peer_bytes
98
+ else:
99
+ value_bytes = value.encode("utf-8")
100
+ encoded += _encode_unsigned_varint(len(value_bytes))
101
+ encoded += value_bytes
102
+
103
+ index += 2
104
+
105
+ return bytes(encoded)
106
+
107
+
108
+ class SubnetExtrinsics(BaseExtrinsicClient):
109
+ """Client for subnet-related extrinsics."""
110
+
111
+ def __init__(self, substrate: Optional[SubstrateInterface] = None):
112
+ """Initialize the subnet client."""
113
+ super().__init__(substrate)
114
+
115
+ # ============================================================================
116
+ # SUBNET REGISTRATION
117
+ # ============================================================================
118
+
119
+ def register_subnet(
120
+ self, request: SubnetRegisterRequest, keypair: Keypair
121
+ ) -> SubnetRegisterResponse:
122
+ """
123
+ Register a new subnet on the network.
124
+
125
+ Args:
126
+ request: Subnet registration request
127
+ keypair: Keypair for signing the transaction
128
+
129
+ Returns:
130
+ SubnetRegisterResponse with registration result
131
+ """
132
+ try:
133
+ logger.info(f"Registering subnet: {request.name}")
134
+
135
+ # Validate inputs
136
+ self._validate_registration_request(request)
137
+
138
+ # Get current registration cost
139
+ current_cost = self._get_current_registration_cost()
140
+ logger.info(f"Current registration cost: {current_cost}")
141
+
142
+ # Ensure max_cost covers the current cost
143
+ if request.max_cost < current_cost:
144
+ return SubnetRegisterResponse(
145
+ success=False,
146
+ error=f"Max cost {request.max_cost} is less than current cost {current_cost}",
147
+ )
148
+
149
+ # Prepare subnet data for the extrinsic
150
+ subnet_data = self._prepare_subnet_data(request)
151
+
152
+ # Compose the extrinsic call
153
+ # Note: register_subnet does NOT take a hotkey parameter
154
+ # The owner is derived from the origin (signer)
155
+ logger.info("Submitting subnet registration extrinsic...")
156
+ with patch_btreemap_accountid_u32_encoder():
157
+ call = self.substrate.compose_call(
158
+ call_module="Network",
159
+ call_function="register_subnet",
160
+ call_params={
161
+ "max_cost": request.max_cost,
162
+ "subnet_data": subnet_data,
163
+ },
164
+ )
165
+
166
+ # Submit the extrinsic using base class method with retry logic
167
+ result = self._submit_extrinsic(call, keypair)
168
+
169
+ if result["success"]:
170
+ # Debug: print result keys
171
+ logger.info(f"Registration result keys: {result.keys()}")
172
+ logger.info(f"Events in result: {len(result.get('events', []))}")
173
+
174
+ # Extract event data
175
+ event_data = self._extract_registration_event_from_result(result)
176
+ logger.info(f"Extracted event data: {event_data}")
177
+
178
+ return SubnetRegisterResponse(
179
+ success=True,
180
+ subnet_id=event_data.get("subnet_id"),
181
+ transaction_hash=result.get("extrinsic_hash"),
182
+ block_hash=result.get("block_hash"),
183
+ cost_paid=current_cost,
184
+ owner=event_data.get("owner"),
185
+ name=event_data.get("name"),
186
+ block_number=result.get("block_number"),
187
+ epoch=self._get_current_epoch(),
188
+ )
189
+ else:
190
+ error_msg = result.get("error", "Unknown error")
191
+ return SubnetRegisterResponse(success=False, error=error_msg)
192
+
193
+ except SubstrateRequestException as e:
194
+ logger.error(f"Substrate request error: {e}")
195
+ logger.debug(
196
+ f"SubstrateRequestException details - type: {type(e)}, message: {str(e)}"
197
+ )
198
+ return SubnetRegisterResponse(
199
+ success=False, error=f"Network error: {str(e)}"
200
+ )
201
+ except Exception as e:
202
+ logger.error(f"Unexpected error during subnet registration: {e}")
203
+ logger.debug(
204
+ f"Exception details - type: {type(e)}, message: {str(e)}", exc_info=True
205
+ )
206
+ return SubnetRegisterResponse(
207
+ success=False, error=f"Unexpected error: {str(e)}"
208
+ )
209
+
210
+ # ============================================================================
211
+ # SUBNET ACTIVATION/DEACTIVATION
212
+ # ============================================================================
213
+
214
+ def activate_subnet(
215
+ self, request: SubnetActivateRequest, keypair: Keypair
216
+ ) -> SimpleExtrinsicResponse:
217
+ """
218
+ Activate a subnet.
219
+
220
+ This extrinsic can result in two different outcomes:
221
+ 1. SubnetActivated event - subnet successfully activated
222
+ 2. SubnetDeactivated event - subnet was removed (failed to meet activation criteria)
223
+
224
+ Args:
225
+ request: Subnet activation request
226
+ keypair: Keypair for signing the transaction
227
+
228
+ Returns:
229
+ SimpleExtrinsicResponse with operation result including actual outcome
230
+ """
231
+ try:
232
+ logger.info(f"Activating subnet: {request.subnet_id}")
233
+
234
+ call = self.substrate.compose_call(
235
+ call_module="Network",
236
+ call_function="activate_subnet",
237
+ call_params={"subnet_id": request.subnet_id},
238
+ )
239
+
240
+ response = self._submit_extrinsic(call, keypair)
241
+
242
+ if response["success"]:
243
+ events = response.get("events", [])
244
+
245
+ # Check for SubnetActivated event (success case)
246
+ activated_event = self._extract_event_by_type(
247
+ events, "Network", "SubnetActivated"
248
+ )
249
+ if activated_event:
250
+ response["message"] = (
251
+ f"Subnet {request.subnet_id} activated successfully"
252
+ )
253
+ response["actual_result"] = "activated"
254
+ return SimpleExtrinsicResponse(response)
255
+
256
+ # Check for SubnetDeactivated event (removal case)
257
+ deactivated_event = self._extract_event_by_type(
258
+ events, "Network", "SubnetDeactivated"
259
+ )
260
+ if deactivated_event:
261
+ # Extract removal reason from event attributes
262
+ reason = self._extract_attribute_value(
263
+ deactivated_event, 1, "Unknown"
264
+ )
265
+ reason_name = self._get_removal_reason_name(reason)
266
+ reason_message = self._get_subnet_removal_reason_message(
267
+ reason_name
268
+ )
269
+
270
+ # Mark as not successful since subnet was removed, not activated
271
+ response["success"] = False
272
+ response["actual_result"] = "removed"
273
+ response["removal_reason"] = reason_name
274
+ response["message"] = (
275
+ f"Subnet {request.subnet_id} was removed: {reason_message}"
276
+ )
277
+ response["error"] = f"SubnetDeactivated: {reason_message}"
278
+
279
+ logger.warning(
280
+ f"Subnet {request.subnet_id} was removed instead of activated. "
281
+ f"Reason: {reason_name}"
282
+ )
283
+ return SimpleExtrinsicResponse(response)
284
+
285
+ # Fallback: Transaction succeeded but we couldn't identify the outcome from events
286
+ # This shouldn't happen normally, but handle it gracefully
287
+ response["message"] = f"Subnet {request.subnet_id} activation completed"
288
+ response["actual_result"] = "unknown"
289
+ logger.warning(
290
+ f"Subnet {request.subnet_id} activation completed but could not "
291
+ "determine actual outcome from events"
292
+ )
293
+
294
+ return SimpleExtrinsicResponse(response)
295
+
296
+ except Exception as e:
297
+ logger.error(f"Error activating subnet: {e}")
298
+ return SimpleExtrinsicResponse({"success": False, "error": str(e)})
299
+
300
+ def _get_removal_reason_name(self, reason: Any) -> str:
301
+ """
302
+ Extract the removal reason name from event attribute.
303
+
304
+ The reason can be a string, dict with 'type' key, or other format.
305
+ """
306
+ if isinstance(reason, str):
307
+ return reason
308
+ if isinstance(reason, dict):
309
+ # Handle enum format: {'type': 'EnactmentPeriod'} or {'__variant__': 'EnactmentPeriod'}
310
+ return (
311
+ reason.get("type")
312
+ or reason.get("__variant__")
313
+ or reason.get("name", str(reason))
314
+ )
315
+ return str(reason)
316
+
317
+ def _get_subnet_removal_reason_message(self, reason: str) -> str:
318
+ """
319
+ Convert SubnetRemovalReason enum to user-friendly message.
320
+
321
+ Args:
322
+ reason: The removal reason enum variant name
323
+
324
+ Returns:
325
+ User-friendly explanation of the removal reason
326
+ """
327
+ REASONS = {
328
+ "MinReputation": "Minimum reputation requirements not met",
329
+ "MinSubnetNodes": "Minimum node count not met - need more nodes registered",
330
+ "MinSubnetDelegateStake": "Minimum delegate stake not met - need more stake delegated to subnet",
331
+ "Council": "Removed by governance council",
332
+ "EnactmentPeriod": "Enactment period expired without meeting activation requirements",
333
+ "MaxSubnets": "Maximum subnet limit reached on the network",
334
+ "Owner": "Deactivated by subnet owner",
335
+ "PauseExpired": "Pause period expired",
336
+ }
337
+ return REASONS.get(reason, f"Removed: {reason}")
338
+
339
+ def remove_subnet(
340
+ self, request: SubnetRemoveRequest, keypair: Keypair
341
+ ) -> dict[str, Any]:
342
+ """
343
+ Remove a subnet.
344
+
345
+ Args:
346
+ request: Subnet removal request
347
+ keypair: Keypair for signing the transaction
348
+
349
+ Returns:
350
+ dictionary with operation result
351
+ """
352
+ try:
353
+ logger.info(f"Removing subnet: {request.subnet_id}")
354
+
355
+ call = self.substrate.compose_call(
356
+ call_module="Network",
357
+ call_function="owner_deactivate_subnet",
358
+ call_params={"subnet_id": request.subnet_id},
359
+ )
360
+
361
+ response = self._submit_extrinsic(call, keypair)
362
+
363
+ if response["success"]:
364
+ response["message"] = f"Subnet {request.subnet_id} removed successfully"
365
+
366
+ return response
367
+
368
+ except Exception as e:
369
+ logger.error(f"Error removing subnet: {e}")
370
+ return {"success": False, "error": str(e)}
371
+
372
+ # ============================================================================
373
+ # SUBNET PAUSE/UNPAUSE
374
+ # ============================================================================
375
+
376
+ def owner_pause_subnet(
377
+ self, request: SubnetActivateRequest, keypair: Keypair
378
+ ) -> dict[str, Any]:
379
+ """
380
+ Pause a subnet (owner only).
381
+
382
+ Args:
383
+ request: Subnet pause request
384
+ keypair: Keypair for signing the transaction
385
+
386
+ Returns:
387
+ dictionary with operation result
388
+ """
389
+ try:
390
+ logger.info(f"Pausing subnet: {request.subnet_id}")
391
+
392
+ call = self.substrate.compose_call(
393
+ call_module="Network",
394
+ call_function="owner_pause_subnet",
395
+ call_params={"subnet_id": request.subnet_id},
396
+ )
397
+
398
+ response = self._submit_extrinsic(call, keypair)
399
+
400
+ if response["success"]:
401
+ response["message"] = f"Subnet {request.subnet_id} paused successfully"
402
+
403
+ return response
404
+
405
+ except Exception as e:
406
+ logger.error(f"Error pausing subnet: {e}")
407
+ return {"success": False, "error": str(e)}
408
+
409
+ def owner_unpause_subnet(
410
+ self, request: SubnetActivateRequest, keypair: Keypair
411
+ ) -> dict[str, Any]:
412
+ """
413
+ Unpause a subnet (owner only).
414
+
415
+ Args:
416
+ request: Subnet unpause request
417
+ keypair: Keypair for signing the transaction
418
+
419
+ Returns:
420
+ dictionary with operation result
421
+ """
422
+ try:
423
+ logger.info(f"Unpausing subnet: {request.subnet_id}")
424
+
425
+ call = self.substrate.compose_call(
426
+ call_module="Network",
427
+ call_function="owner_unpause_subnet",
428
+ call_params={"subnet_id": request.subnet_id},
429
+ )
430
+
431
+ response = self._submit_extrinsic(call, keypair)
432
+
433
+ if response["success"]:
434
+ response["message"] = (
435
+ f"Subnet {request.subnet_id} unpaused successfully"
436
+ )
437
+
438
+ return response
439
+
440
+ except Exception as e:
441
+ logger.error(f"Error unpausing subnet: {e}")
442
+ return {"success": False, "error": str(e)}
443
+
444
+ def owner_deactivate_subnet(
445
+ self, request: SubnetActivateRequest, keypair: Keypair
446
+ ) -> dict[str, Any]:
447
+ """
448
+ Deactivate a subnet (owner only).
449
+
450
+ Args:
451
+ request: Subnet deactivation request
452
+ keypair: Keypair for signing the transaction
453
+
454
+ Returns:
455
+ dictionary with operation result
456
+ """
457
+ try:
458
+ logger.info(f"Deactivating subnet: {request.subnet_id}")
459
+
460
+ call = self.substrate.compose_call(
461
+ call_module="Network",
462
+ call_function="owner_deactivate_subnet",
463
+ call_params={"subnet_id": request.subnet_id},
464
+ )
465
+
466
+ response = self._submit_extrinsic(call, keypair)
467
+
468
+ if response["success"]:
469
+ response["message"] = (
470
+ f"Subnet {request.subnet_id} deactivated successfully"
471
+ )
472
+
473
+ return response
474
+
475
+ except Exception as e:
476
+ logger.error(f"Error deactivating subnet: {e}")
477
+ return {"success": False, "error": str(e)}
478
+
479
+ # ============================================================================
480
+ # SUBNET METADATA UPDATES
481
+ # ============================================================================
482
+
483
+ def owner_update_name(
484
+ self, request: SubnetUpdateRequest, keypair: Keypair
485
+ ) -> dict[str, Any]:
486
+ """
487
+ Update subnet name (owner only).
488
+
489
+ Args:
490
+ request: Subnet name update request
491
+ keypair: Keypair for signing the transaction
492
+
493
+ Returns:
494
+ dictionary with operation result
495
+ """
496
+ try:
497
+ logger.info(f"Updating subnet {request.subnet_id} name")
498
+
499
+ call = self.substrate.compose_call(
500
+ call_module="Network",
501
+ call_function="owner_update_name",
502
+ call_params={
503
+ "subnet_id": request.subnet_id,
504
+ "value": request.value.encode("utf-8"),
505
+ },
506
+ )
507
+
508
+ response = self._submit_extrinsic(call, keypair)
509
+
510
+ if response["success"]:
511
+ response["message"] = (
512
+ f"Subnet {request.subnet_id} name updated successfully"
513
+ )
514
+
515
+ return response
516
+
517
+ except Exception as e:
518
+ logger.error(f"Error updating subnet name: {e}")
519
+ return {"success": False, "error": str(e)}
520
+
521
+ def owner_update_repo(
522
+ self, request: SubnetUpdateRequest, keypair: Keypair
523
+ ) -> dict[str, Any]:
524
+ """
525
+ Update subnet repository URL (owner only).
526
+
527
+ Args:
528
+ request: Subnet repo update request
529
+ keypair: Keypair for signing the transaction
530
+
531
+ Returns:
532
+ dictionary with operation result
533
+ """
534
+ try:
535
+ logger.info(f"Updating subnet {request.subnet_id} repository")
536
+
537
+ call = self.substrate.compose_call(
538
+ call_module="Network",
539
+ call_function="owner_update_repo",
540
+ call_params={
541
+ "subnet_id": request.subnet_id,
542
+ "value": request.value.encode("utf-8"),
543
+ },
544
+ )
545
+
546
+ response = self._submit_extrinsic(call, keypair)
547
+
548
+ if response["success"]:
549
+ response["message"] = (
550
+ f"Subnet {request.subnet_id} repository updated successfully"
551
+ )
552
+
553
+ return response
554
+
555
+ except Exception as e:
556
+ logger.error(f"Error updating subnet repository: {e}")
557
+ return {"success": False, "error": str(e)}
558
+
559
+ def owner_update_description(
560
+ self, request: SubnetUpdateRequest, keypair: Keypair
561
+ ) -> dict[str, Any]:
562
+ """
563
+ Update subnet description (owner only).
564
+
565
+ Args:
566
+ request: Subnet description update request
567
+ keypair: Keypair for signing the transaction
568
+
569
+ Returns:
570
+ dictionary with operation result
571
+ """
572
+ try:
573
+ logger.info(f"Updating subnet {request.subnet_id} description")
574
+
575
+ call = self.substrate.compose_call(
576
+ call_module="Network",
577
+ call_function="owner_update_description",
578
+ call_params={
579
+ "subnet_id": request.subnet_id,
580
+ "value": request.value.encode("utf-8"),
581
+ },
582
+ )
583
+
584
+ response = self._submit_extrinsic(call, keypair)
585
+
586
+ if response["success"]:
587
+ response["message"] = (
588
+ f"Subnet {request.subnet_id} description updated successfully"
589
+ )
590
+
591
+ return response
592
+
593
+ except Exception as e:
594
+ logger.error(f"Error updating subnet description: {e}")
595
+ return {"success": False, "error": str(e)}
596
+
597
+ def owner_update_misc(
598
+ self, request: SubnetUpdateRequest, keypair: Keypair
599
+ ) -> dict[str, Any]:
600
+ """
601
+ Update subnet miscellaneous info (owner only).
602
+
603
+ Args:
604
+ request: Subnet misc update request
605
+ keypair: Keypair for signing the transaction
606
+
607
+ Returns:
608
+ dictionary with operation result
609
+ """
610
+ try:
611
+ logger.info(f"Updating subnet {request.subnet_id} misc info")
612
+
613
+ call = self.substrate.compose_call(
614
+ call_module="Network",
615
+ call_function="owner_update_misc",
616
+ call_params={
617
+ "subnet_id": request.subnet_id,
618
+ "value": request.value.encode("utf-8"),
619
+ },
620
+ )
621
+
622
+ response = self._submit_extrinsic(call, keypair)
623
+
624
+ if response["success"]:
625
+ response["message"] = (
626
+ f"Subnet {request.subnet_id} misc info updated successfully"
627
+ )
628
+
629
+ return response
630
+
631
+ except Exception as e:
632
+ logger.error(f"Error updating subnet misc info: {e}")
633
+ return {"success": False, "error": str(e)}
634
+
635
+ # ============================================================================
636
+ # SUBNET CONFIGURATION UPDATES
637
+ # ============================================================================
638
+
639
+ def owner_update_parameters(
640
+ self,
641
+ subnet_id: int,
642
+ *,
643
+ new_name: Optional[str] = None,
644
+ new_repo: Optional[str] = None,
645
+ target_node_registrations: Optional[int] = None,
646
+ node_burn_rate_alpha: Optional[int] = None,
647
+ queue_immunity_epochs: Optional[int] = None,
648
+ min_weight_decrease_threshold: Optional[int] = None,
649
+ min_node_reputation: Optional[int] = None,
650
+ absent_reputation_penalty: Optional[int] = None,
651
+ included_reputation_boost: Optional[int] = None,
652
+ below_min_weight_penalty: Optional[int] = None,
653
+ non_attestor_penalty: Optional[int] = None,
654
+ validator_absent_penalty: Optional[int] = None,
655
+ validator_non_consensus_penalty: Optional[int] = None,
656
+ non_consensus_attestor_penalty: Optional[int] = None,
657
+ keypair: Keypair,
658
+ ) -> list[tuple[str, dict[str, Any]]]:
659
+ """
660
+ Perform one or more owner subnet parameter updates in sequence.
661
+
662
+ Returns a list of (field_name, result_dict) tuples.
663
+ """
664
+ results: list[tuple[str, dict[str, Any]]] = []
665
+
666
+ # Metadata updates
667
+ if new_name is not None:
668
+ try:
669
+ request = SubnetUpdateRequest(subnet_id=subnet_id, value=new_name)
670
+ result = self.owner_update_name(request, keypair)
671
+ except Exception as e:
672
+ logger.error(f"Error updating subnet name: {e}")
673
+ result = {"success": False, "error": str(e)}
674
+ results.append(("name", result))
675
+
676
+ if new_repo is not None:
677
+ try:
678
+ request = SubnetUpdateRequest(subnet_id=subnet_id, value=new_repo)
679
+ result = self.owner_update_repo(request, keypair)
680
+ except Exception as e:
681
+ logger.error(f"Error updating subnet repo: {e}")
682
+ result = {"success": False, "error": str(e)}
683
+ results.append(("repository", result))
684
+
685
+ # Config updates
686
+ if target_node_registrations is not None:
687
+ try:
688
+ request = SubnetConfigUpdateRequest(
689
+ subnet_id=subnet_id, value=target_node_registrations
690
+ )
691
+ result = self.owner_update_target_node_registrations_per_epoch(
692
+ request, keypair
693
+ )
694
+ except Exception as e:
695
+ logger.error(f"Error updating target node registrations per epoch: {e}")
696
+ result = {"success": False, "error": str(e)}
697
+ results.append(("target_node_registrations", result))
698
+
699
+ if node_burn_rate_alpha is not None:
700
+ try:
701
+ request = SubnetConfigUpdateRequest(
702
+ subnet_id=subnet_id, value=node_burn_rate_alpha
703
+ )
704
+ result = self.owner_update_node_burn_rate_alpha(request, keypair)
705
+ except Exception as e:
706
+ logger.error(f"Error updating node burn rate alpha: {e}")
707
+ result = {"success": False, "error": str(e)}
708
+ results.append(("node_burn_rate_alpha", result))
709
+
710
+ if queue_immunity_epochs is not None:
711
+ try:
712
+ request = SubnetConfigUpdateRequest(
713
+ subnet_id=subnet_id, value=queue_immunity_epochs
714
+ )
715
+ result = self.owner_update_queue_immunity_epochs(request, keypair)
716
+ except Exception as e:
717
+ logger.error(f"Error updating queue immunity epochs: {e}")
718
+ result = {"success": False, "error": str(e)}
719
+ results.append(("queue_immunity_epochs", result))
720
+
721
+ if min_weight_decrease_threshold is not None:
722
+ try:
723
+ request = SubnetConfigUpdateRequest(
724
+ subnet_id=subnet_id, value=min_weight_decrease_threshold
725
+ )
726
+ result = self.owner_update_subnet_node_min_weight_decrease_reputation_threshold(
727
+ request, keypair
728
+ )
729
+ except Exception as e:
730
+ logger.error(
731
+ f"Error updating min weight decrease reputation threshold: {e}"
732
+ )
733
+ result = {"success": False, "error": str(e)}
734
+ results.append(("min_weight_decrease_threshold", result))
735
+
736
+ if min_node_reputation is not None:
737
+ try:
738
+ request = SubnetConfigUpdateRequest(
739
+ subnet_id=subnet_id, value=min_node_reputation
740
+ )
741
+ result = self.owner_update_min_subnet_node_reputation(request, keypair)
742
+ except Exception as e:
743
+ logger.error(f"Error updating min node reputation: {e}")
744
+ result = {"success": False, "error": str(e)}
745
+ results.append(("min_node_reputation", result))
746
+
747
+ if absent_reputation_penalty is not None:
748
+ try:
749
+ request = SubnetConfigUpdateRequest(
750
+ subnet_id=subnet_id, value=absent_reputation_penalty
751
+ )
752
+ result = self.owner_update_absent_decrease_reputation_factor(
753
+ request, keypair
754
+ )
755
+ except Exception as e:
756
+ logger.error(f"Error updating absent reputation penalty: {e}")
757
+ result = {"success": False, "error": str(e)}
758
+ results.append(("absent_reputation_penalty", result))
759
+
760
+ if included_reputation_boost is not None:
761
+ try:
762
+ request = SubnetConfigUpdateRequest(
763
+ subnet_id=subnet_id, value=included_reputation_boost
764
+ )
765
+ result = self.owner_update_included_increase_reputation_factor(
766
+ request, keypair
767
+ )
768
+ except Exception as e:
769
+ logger.error(f"Error updating included reputation boost: {e}")
770
+ result = {"success": False, "error": str(e)}
771
+ results.append(("included_reputation_boost", result))
772
+
773
+ if below_min_weight_penalty is not None:
774
+ try:
775
+ request = SubnetConfigUpdateRequest(
776
+ subnet_id=subnet_id, value=below_min_weight_penalty
777
+ )
778
+ result = self.owner_update_below_min_weight_decrease_reputation_factor(
779
+ request, keypair
780
+ )
781
+ except Exception as e:
782
+ logger.error(f"Error updating below-min weight penalty: {e}")
783
+ result = {"success": False, "error": str(e)}
784
+ results.append(("below_min_weight_penalty", result))
785
+
786
+ if non_attestor_penalty is not None:
787
+ try:
788
+ request = SubnetConfigUpdateRequest(
789
+ subnet_id=subnet_id, value=non_attestor_penalty
790
+ )
791
+ result = self.owner_update_non_attestor_decrease_reputation_factor(
792
+ request, keypair
793
+ )
794
+ except Exception as e:
795
+ logger.error(f"Error updating non-attestor penalty: {e}")
796
+ result = {"success": False, "error": str(e)}
797
+ results.append(("non_attestor_penalty", result))
798
+
799
+ if validator_absent_penalty is not None:
800
+ try:
801
+ request = SubnetConfigUpdateRequest(
802
+ subnet_id=subnet_id, value=validator_absent_penalty
803
+ )
804
+ result = self.owner_update_validator_absent_decrease_reputation_factor(
805
+ request, keypair
806
+ )
807
+ except Exception as e:
808
+ logger.error(f"Error updating validator absent penalty: {e}")
809
+ result = {"success": False, "error": str(e)}
810
+ results.append(("validator_absent_penalty", result))
811
+
812
+ if validator_non_consensus_penalty is not None:
813
+ try:
814
+ request = SubnetConfigUpdateRequest(
815
+ subnet_id=subnet_id, value=validator_non_consensus_penalty
816
+ )
817
+ result = self.owner_update_validator_non_consensus_decrease_reputation_factor(
818
+ request, keypair
819
+ )
820
+ except Exception as e:
821
+ logger.error(f"Error updating validator non-consensus penalty: {e}")
822
+ result = {"success": False, "error": str(e)}
823
+ results.append(("validator_non_consensus_penalty", result))
824
+
825
+ if non_consensus_attestor_penalty is not None:
826
+ try:
827
+ request = SubnetConfigUpdateRequest(
828
+ subnet_id=subnet_id, value=non_consensus_attestor_penalty
829
+ )
830
+ result = (
831
+ self.owner_update_non_consensus_attestor_decrease_reputation_factor(
832
+ request, keypair
833
+ )
834
+ )
835
+ except Exception as e:
836
+ logger.error(f"Error updating non-consensus attestor penalty: {e}")
837
+ result = {"success": False, "error": str(e)}
838
+ results.append(("non_consensus_attestor_penalty", result))
839
+
840
+ return results
841
+
842
+ def owner_update_churn_limit(
843
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
844
+ ) -> dict[str, Any]:
845
+ """
846
+ Update subnet churn limit (owner only).
847
+
848
+ Args:
849
+ request: Subnet churn limit update request
850
+ keypair: Keypair for signing the transaction
851
+
852
+ Returns:
853
+ dictionary with operation result
854
+ """
855
+ try:
856
+ logger.info(f"Updating subnet {request.subnet_id} churn limit")
857
+
858
+ call = self.substrate.compose_call(
859
+ call_module="Network",
860
+ call_function="owner_update_churn_limit",
861
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
862
+ )
863
+
864
+ response = self._submit_extrinsic(call, keypair)
865
+
866
+ if response["success"]:
867
+ response["message"] = (
868
+ f"Subnet {request.subnet_id} churn limit updated successfully"
869
+ )
870
+
871
+ return response
872
+
873
+ except Exception as e:
874
+ logger.error(f"Error updating subnet churn limit: {e}")
875
+ return {"success": False, "error": str(e)}
876
+
877
+ def owner_update_registration_queue_epochs(
878
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
879
+ ) -> dict[str, Any]:
880
+ """
881
+ Update subnet registration queue epochs (owner only).
882
+
883
+ Args:
884
+ request: Subnet queue epochs update request
885
+ keypair: Keypair for signing the transaction
886
+
887
+ Returns:
888
+ dictionary with operation result
889
+ """
890
+ try:
891
+ logger.info(
892
+ f"Updating subnet {request.subnet_id} registration queue epochs"
893
+ )
894
+
895
+ call = self.substrate.compose_call(
896
+ call_module="Network",
897
+ call_function="owner_update_registration_queue_epochs",
898
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
899
+ )
900
+
901
+ response = self._submit_extrinsic(call, keypair)
902
+
903
+ if response["success"]:
904
+ response["message"] = (
905
+ f"Subnet {request.subnet_id} registration queue epochs updated successfully"
906
+ )
907
+
908
+ return response
909
+
910
+ except Exception as e:
911
+ logger.error(f"Error updating subnet registration queue epochs: {e}")
912
+ return {"success": False, "error": str(e)}
913
+
914
+ def owner_update_target_node_registrations_per_epoch(
915
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
916
+ ) -> dict[str, Any]:
917
+ """
918
+ Update target node registrations per epoch (owner only).
919
+
920
+ Args:
921
+ request: Target node registrations update request
922
+ keypair: Keypair for signing the transaction
923
+
924
+ Returns:
925
+ dictionary with operation result
926
+ """
927
+ return self._execute_owner_value_update(
928
+ request,
929
+ keypair,
930
+ call_function="owner_update_target_node_registrations_per_epoch",
931
+ success_message=(
932
+ "Subnet {subnet_id} target node registrations per epoch updated successfully"
933
+ ),
934
+ )
935
+
936
+ def owner_update_activation_grace_epochs(
937
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
938
+ ) -> dict[str, Any]:
939
+ """
940
+ Update subnet activation grace epochs (owner only).
941
+
942
+ Args:
943
+ request: Subnet grace epochs update request
944
+ keypair: Keypair for signing the transaction
945
+
946
+ Returns:
947
+ dictionary with operation result
948
+ """
949
+ try:
950
+ logger.info(f"Updating subnet {request.subnet_id} activation grace epochs")
951
+
952
+ call = self.substrate.compose_call(
953
+ call_module="Network",
954
+ call_function="owner_update_activation_grace_epochs",
955
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
956
+ )
957
+
958
+ response = self._submit_extrinsic(call, keypair)
959
+
960
+ if response["success"]:
961
+ response["message"] = (
962
+ f"Subnet {request.subnet_id} activation grace epochs updated successfully"
963
+ )
964
+
965
+ return response
966
+
967
+ except Exception as e:
968
+ logger.error(f"Error updating subnet activation grace epochs: {e}")
969
+ return {"success": False, "error": str(e)}
970
+
971
+ def owner_update_idle_classification_epochs(
972
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
973
+ ) -> dict[str, Any]:
974
+ """
975
+ Update subnet idle classification epochs (owner only).
976
+
977
+ Args:
978
+ request: Subnet idle epochs update request
979
+ keypair: Keypair for signing the transaction
980
+
981
+ Returns:
982
+ dictionary with operation result
983
+ """
984
+ try:
985
+ logger.info(
986
+ f"Updating subnet {request.subnet_id} idle classification epochs"
987
+ )
988
+
989
+ call = self.substrate.compose_call(
990
+ call_module="Network",
991
+ call_function="owner_update_idle_classification_epochs",
992
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
993
+ )
994
+
995
+ response = self._submit_extrinsic(call, keypair)
996
+
997
+ if response["success"]:
998
+ response["message"] = (
999
+ f"Subnet {request.subnet_id} idle classification epochs updated successfully"
1000
+ )
1001
+
1002
+ return response
1003
+
1004
+ except Exception as e:
1005
+ logger.error(f"Error updating subnet idle classification epochs: {e}")
1006
+ return {"success": False, "error": str(e)}
1007
+
1008
+ def owner_update_included_classification_epochs(
1009
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1010
+ ) -> dict[str, Any]:
1011
+ """
1012
+ Update subnet included classification epochs (owner only).
1013
+
1014
+ Args:
1015
+ request: Subnet included epochs update request
1016
+ keypair: Keypair for signing the transaction
1017
+
1018
+ Returns:
1019
+ dictionary with operation result
1020
+ """
1021
+ try:
1022
+ logger.info(
1023
+ f"Updating subnet {request.subnet_id} included classification epochs"
1024
+ )
1025
+
1026
+ call = self.substrate.compose_call(
1027
+ call_module="Network",
1028
+ call_function="owner_update_included_classification_epochs",
1029
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
1030
+ )
1031
+
1032
+ response = self._submit_extrinsic(call, keypair)
1033
+
1034
+ if response["success"]:
1035
+ response["message"] = (
1036
+ f"Subnet {request.subnet_id} included classification epochs updated successfully"
1037
+ )
1038
+
1039
+ return response
1040
+
1041
+ except Exception as e:
1042
+ logger.error(f"Error updating subnet included classification epochs: {e}")
1043
+ return {"success": False, "error": str(e)}
1044
+
1045
+ def owner_update_queue_immunity_epochs(
1046
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1047
+ ) -> dict[str, Any]:
1048
+ """
1049
+ Update queue immunity epochs (owner only).
1050
+
1051
+ Args:
1052
+ request: Queue immunity epochs update request
1053
+ keypair: Keypair for signing the transaction
1054
+
1055
+ Returns:
1056
+ dictionary with operation result
1057
+ """
1058
+ return self._execute_owner_value_update(
1059
+ request,
1060
+ keypair,
1061
+ call_function="owner_update_queue_immunity_epochs",
1062
+ success_message=(
1063
+ "Subnet {subnet_id} queue immunity epochs updated successfully"
1064
+ ),
1065
+ )
1066
+
1067
+ def owner_update_max_node_penalties(
1068
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1069
+ ) -> dict[str, Any]:
1070
+ """
1071
+ Update subnet max node penalties (owner only).
1072
+
1073
+ Args:
1074
+ request: Subnet max penalties update request
1075
+ keypair: Keypair for signing the transaction
1076
+
1077
+ Returns:
1078
+ dictionary with operation result
1079
+ """
1080
+ try:
1081
+ logger.info(f"Updating subnet {request.subnet_id} max node penalties")
1082
+
1083
+ call = self.substrate.compose_call(
1084
+ call_module="Network",
1085
+ call_function="owner_update_max_node_penalties",
1086
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
1087
+ )
1088
+
1089
+ response = self._submit_extrinsic(call, keypair)
1090
+
1091
+ if response["success"]:
1092
+ response["message"] = (
1093
+ f"Subnet {request.subnet_id} max node penalties updated successfully"
1094
+ )
1095
+
1096
+ return response
1097
+
1098
+ except Exception as e:
1099
+ logger.error(f"Error updating subnet max node penalties: {e}")
1100
+ return {"success": False, "error": str(e)}
1101
+
1102
+ # ============================================================================
1103
+ # SUBNET ACCESS CONTROL UPDATES
1104
+ # ============================================================================
1105
+
1106
+ def owner_add_initial_coldkeys(
1107
+ self, subnet_id: int, coldkeys: list[str], keypair: Keypair
1108
+ ) -> dict[str, Any]:
1109
+ """
1110
+ Add initial coldkeys to subnet (owner only).
1111
+
1112
+ Args:
1113
+ subnet_id: The subnet ID
1114
+ coldkeys: list of coldkey account IDs to add
1115
+ keypair: Keypair for signing the transaction
1116
+
1117
+ Returns:
1118
+ dictionary with operation result
1119
+ """
1120
+ try:
1121
+ logger.info(f"Adding initial coldkeys to subnet {subnet_id}")
1122
+
1123
+ call = self.substrate.compose_call(
1124
+ call_module="Network",
1125
+ call_function="owner_add_initial_coldkeys",
1126
+ call_params={"subnet_id": subnet_id, "coldkeys": coldkeys},
1127
+ )
1128
+
1129
+ response = self._submit_extrinsic(call, keypair)
1130
+
1131
+ if response["success"]:
1132
+ response["message"] = (
1133
+ f"Subnet {subnet_id} initial coldkeys added successfully"
1134
+ )
1135
+
1136
+ return response
1137
+
1138
+ except Exception as e:
1139
+ logger.error(f"Error adding initial coldkeys: {e}")
1140
+ return {"success": False, "error": str(e)}
1141
+
1142
+ def owner_remove_initial_coldkeys(
1143
+ self, subnet_id: int, coldkeys: list[str], keypair: Keypair
1144
+ ) -> dict[str, Any]:
1145
+ """
1146
+ Remove initial coldkeys from subnet (owner only).
1147
+
1148
+ Args:
1149
+ subnet_id: The subnet ID
1150
+ coldkeys: list of coldkey account IDs to remove
1151
+ keypair: Keypair for signing the transaction
1152
+
1153
+ Returns:
1154
+ dictionary with operation result
1155
+ """
1156
+ try:
1157
+ logger.info(f"Removing initial coldkeys from subnet {subnet_id}")
1158
+
1159
+ call = self.substrate.compose_call(
1160
+ call_module="Network",
1161
+ call_function="owner_remove_initial_coldkeys",
1162
+ call_params={"subnet_id": subnet_id, "coldkeys": coldkeys},
1163
+ )
1164
+
1165
+ response = self._submit_extrinsic(call, keypair)
1166
+
1167
+ if response["success"]:
1168
+ response["message"] = (
1169
+ f"Subnet {subnet_id} initial coldkeys removed successfully"
1170
+ )
1171
+
1172
+ return response
1173
+
1174
+ except Exception as e:
1175
+ logger.error(f"Error removing initial coldkeys: {e}")
1176
+ return {"success": False, "error": str(e)}
1177
+
1178
+ def owner_update_key_types(
1179
+ self, subnet_id: int, key_types: list[KeyType], keypair: Keypair
1180
+ ) -> dict[str, Any]:
1181
+ """
1182
+ Update subnet key types (owner only).
1183
+
1184
+ Args:
1185
+ subnet_id: The subnet ID
1186
+ key_types: list of key types to update
1187
+ keypair: Keypair for signing the transaction
1188
+
1189
+ Returns:
1190
+ dictionary with operation result
1191
+ """
1192
+ try:
1193
+ logger.info(f"Updating key types for subnet {subnet_id}")
1194
+
1195
+ call = self.substrate.compose_call(
1196
+ call_module="Network",
1197
+ call_function="owner_update_key_types",
1198
+ call_params={
1199
+ "subnet_id": subnet_id,
1200
+ "key_types": [kt.value for kt in key_types],
1201
+ },
1202
+ )
1203
+
1204
+ response = self._submit_extrinsic(call, keypair)
1205
+
1206
+ if response["success"]:
1207
+ response["message"] = (
1208
+ f"Subnet {subnet_id} key types updated successfully"
1209
+ )
1210
+
1211
+ return response
1212
+
1213
+ except Exception as e:
1214
+ logger.error(f"Error updating key types: {e}")
1215
+ return {"success": False, "error": str(e)}
1216
+
1217
+ # ============================================================================
1218
+ # SUBNET NODE MANAGEMENT
1219
+ # ============================================================================
1220
+
1221
+ def owner_remove_subnet_node(
1222
+ self, subnet_id: int, subnet_node_id: int, keypair: Keypair
1223
+ ) -> dict[str, Any]:
1224
+ """
1225
+ Remove a subnet node (owner only).
1226
+
1227
+ Args:
1228
+ subnet_id: The subnet ID
1229
+ subnet_node_id: The subnet node ID to remove
1230
+ keypair: Keypair for signing the transaction
1231
+
1232
+ Returns:
1233
+ dictionary with operation result
1234
+ """
1235
+ try:
1236
+ logger.info(f"Removing subnet {subnet_id} node {subnet_node_id}")
1237
+
1238
+ call = self.substrate.compose_call(
1239
+ call_module="Network",
1240
+ call_function="owner_remove_subnet_node",
1241
+ call_params={"subnet_id": subnet_id, "subnet_node_id": subnet_node_id},
1242
+ )
1243
+
1244
+ response = self._submit_extrinsic(call, keypair)
1245
+
1246
+ if response["success"]:
1247
+ response["message"] = (
1248
+ f"Subnet {subnet_id} node {subnet_node_id} removed successfully"
1249
+ )
1250
+
1251
+ return response
1252
+
1253
+ except Exception as e:
1254
+ logger.error(f"Error removing subnet node: {e}")
1255
+ return {"success": False, "error": str(e)}
1256
+
1257
+ # ============================================================================
1258
+ # SUBNET STAKE UPDATES
1259
+ # ============================================================================
1260
+
1261
+ def owner_update_min_stake(
1262
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1263
+ ) -> dict[str, Any]:
1264
+ """
1265
+ Update subnet minimum stake (owner only).
1266
+
1267
+ Args:
1268
+ request: Subnet min stake update request
1269
+ keypair: Keypair for signing the transaction
1270
+
1271
+ Returns:
1272
+ dictionary with operation result
1273
+ """
1274
+ try:
1275
+ logger.info(f"Updating subnet {request.subnet_id} min stake")
1276
+
1277
+ call = self.substrate.compose_call(
1278
+ call_module="Network",
1279
+ call_function="owner_update_min_stake",
1280
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
1281
+ )
1282
+
1283
+ response = self._submit_extrinsic(call, keypair)
1284
+
1285
+ if response["success"]:
1286
+ response["message"] = (
1287
+ f"Subnet {request.subnet_id} min stake updated successfully"
1288
+ )
1289
+
1290
+ return response
1291
+
1292
+ except Exception as e:
1293
+ logger.error(f"Error updating subnet min stake: {e}")
1294
+ return {"success": False, "error": str(e)}
1295
+
1296
+ def owner_update_max_stake(
1297
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1298
+ ) -> dict[str, Any]:
1299
+ """
1300
+ Update subnet maximum stake (owner only).
1301
+
1302
+ Args:
1303
+ request: Subnet max stake update request
1304
+ keypair: Keypair for signing the transaction
1305
+
1306
+ Returns:
1307
+ dictionary with operation result
1308
+ """
1309
+ try:
1310
+ logger.info(f"Updating subnet {request.subnet_id} max stake")
1311
+
1312
+ call = self.substrate.compose_call(
1313
+ call_module="Network",
1314
+ call_function="owner_update_max_stake",
1315
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
1316
+ )
1317
+
1318
+ response = self._submit_extrinsic(call, keypair)
1319
+
1320
+ if response["success"]:
1321
+ response["message"] = (
1322
+ f"Subnet {request.subnet_id} max stake updated successfully"
1323
+ )
1324
+
1325
+ return response
1326
+
1327
+ except Exception as e:
1328
+ logger.error(f"Error updating subnet max stake: {e}")
1329
+ return {"success": False, "error": str(e)}
1330
+
1331
+ def owner_update_delegate_stake_percentage(
1332
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1333
+ ) -> dict[str, Any]:
1334
+ """
1335
+ Update subnet delegate stake percentage (owner only).
1336
+
1337
+ Args:
1338
+ request: Subnet delegate stake percentage update request
1339
+ keypair: Keypair for signing the transaction
1340
+
1341
+ Returns:
1342
+ dictionary with operation result
1343
+ """
1344
+ try:
1345
+ logger.info(
1346
+ f"Updating subnet {request.subnet_id} delegate stake percentage"
1347
+ )
1348
+
1349
+ call = self.substrate.compose_call(
1350
+ call_module="Network",
1351
+ call_function="owner_update_delegate_stake_percentage",
1352
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
1353
+ )
1354
+
1355
+ response = self._submit_extrinsic(call, keypair)
1356
+
1357
+ if response["success"]:
1358
+ response["message"] = (
1359
+ f"Subnet {request.subnet_id} delegate stake percentage updated successfully"
1360
+ )
1361
+
1362
+ return response
1363
+
1364
+ except Exception as e:
1365
+ logger.error(f"Error updating subnet delegate stake percentage: {e}")
1366
+ return {"success": False, "error": str(e)}
1367
+
1368
+ def owner_update_max_registered_nodes(
1369
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1370
+ ) -> dict[str, Any]:
1371
+ """
1372
+ Update subnet max registered nodes (owner only).
1373
+
1374
+ Args:
1375
+ request: Subnet max registered nodes update request
1376
+ keypair: Keypair for signing the transaction
1377
+
1378
+ Returns:
1379
+ dictionary with operation result
1380
+ """
1381
+ try:
1382
+ logger.info(f"Updating subnet {request.subnet_id} max registered nodes")
1383
+
1384
+ call = self.substrate.compose_call(
1385
+ call_module="Network",
1386
+ call_function="owner_update_max_registered_nodes",
1387
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
1388
+ )
1389
+
1390
+ response = self._submit_extrinsic(call, keypair)
1391
+
1392
+ if response["success"]:
1393
+ response["message"] = (
1394
+ f"Subnet {request.subnet_id} max registered nodes updated successfully"
1395
+ )
1396
+
1397
+ return response
1398
+
1399
+ except Exception as e:
1400
+ logger.error(f"Error updating subnet max registered nodes: {e}")
1401
+ return {"success": False, "error": str(e)}
1402
+
1403
+ def owner_update_node_burn_rate_alpha(
1404
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1405
+ ) -> dict[str, Any]:
1406
+ """
1407
+ Update node burn rate alpha (owner only).
1408
+
1409
+ Args:
1410
+ request: Node burn rate alpha update request
1411
+ keypair: Keypair for signing the transaction
1412
+
1413
+ Returns:
1414
+ dictionary with operation result
1415
+ """
1416
+ return self._execute_owner_value_update(
1417
+ request,
1418
+ keypair,
1419
+ call_function="owner_update_node_burn_rate_alpha",
1420
+ success_message=(
1421
+ "Subnet {subnet_id} node burn rate alpha updated successfully"
1422
+ ),
1423
+ )
1424
+
1425
+ def owner_update_subnet_node_min_weight_decrease_reputation_threshold(
1426
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1427
+ ) -> dict[str, Any]:
1428
+ """
1429
+ Update minimum weight decrease reputation threshold (owner only).
1430
+
1431
+ Args:
1432
+ request: Min weight decrease reputation threshold update request
1433
+ keypair: Keypair for signing the transaction
1434
+
1435
+ Returns:
1436
+ dictionary with operation result
1437
+ """
1438
+ return self._execute_owner_value_update(
1439
+ request,
1440
+ keypair,
1441
+ call_function=(
1442
+ "owner_update_subnet_node_min_weight_decrease_reputation_threshold"
1443
+ ),
1444
+ success_message=(
1445
+ "Subnet {subnet_id} min weight decrease reputation threshold updated successfully"
1446
+ ),
1447
+ )
1448
+
1449
+ def owner_update_min_subnet_node_reputation(
1450
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1451
+ ) -> dict[str, Any]:
1452
+ """
1453
+ Update minimum subnet node reputation (owner only).
1454
+
1455
+ Args:
1456
+ request: Minimum subnet node reputation update request
1457
+ keypair: Keypair for signing the transaction
1458
+
1459
+ Returns:
1460
+ dictionary with operation result
1461
+ """
1462
+ return self._execute_owner_value_update(
1463
+ request,
1464
+ keypair,
1465
+ call_function="owner_update_min_subnet_node_reputation",
1466
+ success_message=(
1467
+ "Subnet {subnet_id} minimum node reputation updated successfully"
1468
+ ),
1469
+ )
1470
+
1471
+ def owner_update_absent_decrease_reputation_factor(
1472
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1473
+ ) -> dict[str, Any]:
1474
+ """
1475
+ Update absent decrease reputation factor (owner only).
1476
+
1477
+ Args:
1478
+ request: Absent decrease reputation factor update request
1479
+ keypair: Keypair for signing the transaction
1480
+
1481
+ Returns:
1482
+ dictionary with operation result
1483
+ """
1484
+ return self._execute_owner_value_update(
1485
+ request,
1486
+ keypair,
1487
+ call_function="owner_update_absent_decrease_reputation_factor",
1488
+ success_message=(
1489
+ "Subnet {subnet_id} absent decrease reputation factor updated successfully"
1490
+ ),
1491
+ )
1492
+
1493
+ def owner_update_included_increase_reputation_factor(
1494
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1495
+ ) -> dict[str, Any]:
1496
+ """
1497
+ Update included increase reputation factor (owner only).
1498
+
1499
+ Args:
1500
+ request: Included increase reputation factor update request
1501
+ keypair: Keypair for signing the transaction
1502
+
1503
+ Returns:
1504
+ dictionary with operation result
1505
+ """
1506
+ return self._execute_owner_value_update(
1507
+ request,
1508
+ keypair,
1509
+ call_function="owner_update_included_increase_reputation_factor",
1510
+ success_message=(
1511
+ "Subnet {subnet_id} included increase reputation factor updated successfully"
1512
+ ),
1513
+ )
1514
+
1515
+ def owner_update_below_min_weight_decrease_reputation_factor(
1516
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1517
+ ) -> dict[str, Any]:
1518
+ """
1519
+ Update below-min weight decrease reputation factor (owner only).
1520
+
1521
+ Args:
1522
+ request: Below-min weight decrease reputation factor update request
1523
+ keypair: Keypair for signing the transaction
1524
+
1525
+ Returns:
1526
+ dictionary with operation result
1527
+ """
1528
+ return self._execute_owner_value_update(
1529
+ request,
1530
+ keypair,
1531
+ call_function="owner_update_below_min_weight_decrease_reputation_factor",
1532
+ success_message=(
1533
+ "Subnet {subnet_id} below-min weight decrease reputation factor updated successfully"
1534
+ ),
1535
+ )
1536
+
1537
+ def owner_update_non_attestor_decrease_reputation_factor(
1538
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1539
+ ) -> dict[str, Any]:
1540
+ """
1541
+ Update non-attestor decrease reputation factor (owner only).
1542
+
1543
+ Args:
1544
+ request: Non-attestor decrease reputation factor update request
1545
+ keypair: Keypair for signing the transaction
1546
+
1547
+ Returns:
1548
+ dictionary with operation result
1549
+ """
1550
+ return self._execute_owner_value_update(
1551
+ request,
1552
+ keypair,
1553
+ call_function="owner_update_non_attestor_decrease_reputation_factor",
1554
+ success_message=(
1555
+ "Subnet {subnet_id} non-attestor decrease reputation factor updated successfully"
1556
+ ),
1557
+ )
1558
+
1559
+ def owner_update_validator_absent_decrease_reputation_factor(
1560
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1561
+ ) -> dict[str, Any]:
1562
+ """
1563
+ Update validator absent decrease reputation factor (owner only).
1564
+
1565
+ Args:
1566
+ request: Validator absent decrease reputation factor update request
1567
+ keypair: Keypair for signing the transaction
1568
+
1569
+ Returns:
1570
+ dictionary with operation result
1571
+ """
1572
+ return self._execute_owner_value_update(
1573
+ request,
1574
+ keypair,
1575
+ call_function="owner_update_validator_absent_decrease_reputation_factor",
1576
+ success_message=(
1577
+ "Subnet {subnet_id} validator absent decrease reputation factor updated successfully"
1578
+ ),
1579
+ )
1580
+
1581
+ def owner_update_validator_non_consensus_decrease_reputation_factor(
1582
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1583
+ ) -> dict[str, Any]:
1584
+ """
1585
+ Update validator non-consensus decrease reputation factor (owner only).
1586
+
1587
+ Args:
1588
+ request: Validator non-consensus decrease reputation factor update request
1589
+ keypair: Keypair for signing the transaction
1590
+
1591
+ Returns:
1592
+ dictionary with operation result
1593
+ """
1594
+ return self._execute_owner_value_update(
1595
+ request,
1596
+ keypair,
1597
+ call_function="owner_update_validator_non_consensus_decrease_reputation_factor",
1598
+ success_message=(
1599
+ "Subnet {subnet_id} validator non-consensus decrease reputation factor updated successfully"
1600
+ ),
1601
+ )
1602
+
1603
+ def owner_update_non_consensus_attestor_decrease_reputation_factor(
1604
+ self, request: SubnetConfigUpdateRequest, keypair: Keypair
1605
+ ) -> dict[str, Any]:
1606
+ """
1607
+ Update non-consensus attestor decrease reputation factor (owner only).
1608
+
1609
+ Args:
1610
+ request: Non-consensus attestor decrease reputation factor update request
1611
+ keypair: Keypair for signing the transaction
1612
+
1613
+ Returns:
1614
+ dictionary with operation result
1615
+ """
1616
+ return self._execute_owner_value_update(
1617
+ request,
1618
+ keypair,
1619
+ call_function="owner_update_non_consensus_attestor_decrease_reputation_factor",
1620
+ success_message=(
1621
+ "Subnet {subnet_id} non-consensus attestor decrease reputation factor updated successfully"
1622
+ ),
1623
+ )
1624
+
1625
+ # ============================================================================
1626
+ # SUBNET OWNERSHIP TRANSFER
1627
+ # ============================================================================
1628
+
1629
+ def transfer_subnet_ownership(
1630
+ self, request: SubnetOwnershipTransferRequest, keypair: Keypair
1631
+ ) -> dict[str, Any]:
1632
+ """
1633
+ Transfer subnet ownership.
1634
+
1635
+ Args:
1636
+ request: Subnet ownership transfer request
1637
+ keypair: Keypair for signing the transaction
1638
+
1639
+ Returns:
1640
+ dictionary with operation result
1641
+ """
1642
+ try:
1643
+ logger.info(
1644
+ f"Transferring subnet {request.subnet_id} ownership to {request.new_owner}"
1645
+ )
1646
+
1647
+ call = self.substrate.compose_call(
1648
+ call_module="Network",
1649
+ call_function="transfer_subnet_ownership",
1650
+ call_params={
1651
+ "subnet_id": request.subnet_id,
1652
+ "new_owner": request.new_owner,
1653
+ },
1654
+ )
1655
+
1656
+ response = self._submit_extrinsic(call, keypair)
1657
+
1658
+ if response["success"]:
1659
+ response["message"] = (
1660
+ f"Subnet {request.subnet_id} ownership transfer initiated successfully"
1661
+ )
1662
+
1663
+ return response
1664
+
1665
+ except Exception as e:
1666
+ logger.error(f"Error transferring subnet ownership: {e}")
1667
+ return {"success": False, "error": str(e)}
1668
+
1669
+ def accept_subnet_ownership(
1670
+ self, request: SubnetOwnershipAcceptRequest, keypair: Keypair
1671
+ ) -> dict[str, Any]:
1672
+ """
1673
+ Accept subnet ownership.
1674
+
1675
+ Args:
1676
+ request: Subnet ownership accept request
1677
+ keypair: Keypair for signing the transaction
1678
+
1679
+ Returns:
1680
+ dictionary with operation result
1681
+ """
1682
+ try:
1683
+ logger.info(f"Accepting subnet {request.subnet_id} ownership")
1684
+
1685
+ call = self.substrate.compose_call(
1686
+ call_module="Network",
1687
+ call_function="accept_subnet_ownership",
1688
+ call_params={"subnet_id": request.subnet_id},
1689
+ )
1690
+
1691
+ response = self._submit_extrinsic(call, keypair)
1692
+
1693
+ if response["success"]:
1694
+ response["message"] = (
1695
+ f"Subnet {request.subnet_id} ownership accepted successfully"
1696
+ )
1697
+
1698
+ return response
1699
+
1700
+ except Exception as e:
1701
+ logger.error(f"Error accepting subnet ownership: {e}")
1702
+ return {"success": False, "error": str(e)}
1703
+
1704
+ # ============================================================================
1705
+ # SUBNET EMERGENCY CONTROLS
1706
+ # ============================================================================
1707
+
1708
+ def owner_set_emergency_validator_set(
1709
+ self, subnet_id: int, subnet_node_ids: list[int], keypair: Keypair
1710
+ ) -> dict[str, Any]:
1711
+ """
1712
+ Override the validator set for a subnet (owner only).
1713
+
1714
+ Args:
1715
+ subnet_id: The subnet ID
1716
+ subnet_node_ids: List of subnet node IDs to be used as validators
1717
+ keypair: Keypair for signing the transaction
1718
+
1719
+ Returns:
1720
+ dictionary with operation result
1721
+ """
1722
+ try:
1723
+ sorted_ids = sorted(subnet_node_ids)
1724
+ logger.info(
1725
+ f"Setting emergency validator set for subnet {subnet_id}: {sorted_ids}"
1726
+ )
1727
+
1728
+ call = self.substrate.compose_call(
1729
+ call_module="Network",
1730
+ call_function="owner_set_emergency_validator_set",
1731
+ call_params={"subnet_id": subnet_id, "subnet_node_ids": sorted_ids},
1732
+ )
1733
+
1734
+ response = self._submit_extrinsic(call, keypair)
1735
+
1736
+ if response["success"]:
1737
+ response["message"] = (
1738
+ f"Subnet {subnet_id} emergency validator set updated successfully"
1739
+ )
1740
+
1741
+ return response
1742
+
1743
+ except Exception as e:
1744
+ logger.error(
1745
+ f"Error setting emergency validator set for subnet {subnet_id}: {e}"
1746
+ )
1747
+ return {"success": False, "error": str(e)}
1748
+
1749
+ def owner_add_bootnode_access(
1750
+ self, subnet_id: int, new_account: str, keypair: Keypair
1751
+ ) -> dict[str, Any]:
1752
+ """
1753
+ Grant bootnode access to an account (owner only).
1754
+
1755
+ Args:
1756
+ subnet_id: The subnet ID
1757
+ new_account: Account ID to add
1758
+ keypair: Keypair for signing the transaction
1759
+
1760
+ Returns:
1761
+ dictionary with operation result
1762
+ """
1763
+ try:
1764
+ validate_address(new_account)
1765
+ logger.info(
1766
+ f"Adding bootnode access for subnet {subnet_id} to {new_account}"
1767
+ )
1768
+
1769
+ call = self.substrate.compose_call(
1770
+ call_module="Network",
1771
+ call_function="owner_add_bootnode_access",
1772
+ call_params={"subnet_id": subnet_id, "new_account": new_account},
1773
+ )
1774
+
1775
+ response = self._submit_extrinsic(call, keypair)
1776
+
1777
+ if response["success"]:
1778
+ response["message"] = (
1779
+ f"Subnet {subnet_id} bootnode access granted to {new_account}"
1780
+ )
1781
+
1782
+ return response
1783
+
1784
+ except Exception as e:
1785
+ logger.error(f"Error adding bootnode access for subnet {subnet_id}: {e}")
1786
+ return {"success": False, "error": str(e)}
1787
+
1788
+ def owner_remove_bootnode_access(
1789
+ self, subnet_id: int, remove_account: str, keypair: Keypair
1790
+ ) -> dict[str, Any]:
1791
+ """
1792
+ Remove bootnode access from an account (owner only).
1793
+
1794
+ Args:
1795
+ subnet_id: The subnet ID
1796
+ remove_account: Account ID to remove
1797
+ keypair: Keypair for signing the transaction
1798
+
1799
+ Returns:
1800
+ dictionary with operation result
1801
+ """
1802
+ try:
1803
+ validate_address(remove_account)
1804
+ logger.info(
1805
+ f"Removing bootnode access for subnet {subnet_id} from {remove_account}"
1806
+ )
1807
+
1808
+ call = self.substrate.compose_call(
1809
+ call_module="Network",
1810
+ call_function="owner_remove_bootnode_access",
1811
+ call_params={"subnet_id": subnet_id, "remove_account": remove_account},
1812
+ )
1813
+
1814
+ response = self._submit_extrinsic(call, keypair)
1815
+
1816
+ if response["success"]:
1817
+ response["message"] = (
1818
+ f"Subnet {subnet_id} bootnode access revoked from {remove_account}"
1819
+ )
1820
+
1821
+ return response
1822
+
1823
+ except Exception as e:
1824
+ logger.error(f"Error removing bootnode access for subnet {subnet_id}: {e}")
1825
+ return {"success": False, "error": str(e)}
1826
+
1827
+ # ============================================================================
1828
+ # BOOTNODE MANAGEMENT
1829
+ # ============================================================================
1830
+
1831
+ def update_bootnodes(
1832
+ self,
1833
+ subnet_id: int,
1834
+ add_bootnodes: list[str],
1835
+ remove_bootnodes: list[str],
1836
+ keypair: Keypair,
1837
+ ) -> dict[str, Any]:
1838
+ """
1839
+ Update subnet bootnodes.
1840
+
1841
+ Args:
1842
+ subnet_id: The subnet ID
1843
+ add_bootnodes: list of bootnode addresses to add
1844
+ remove_bootnodes: list of bootnode addresses to remove
1845
+ keypair: Keypair for signing the transaction
1846
+
1847
+ Returns:
1848
+ dictionary with operation result
1849
+ """
1850
+ try:
1851
+ logger.info(f"Updating subnet {subnet_id} bootnodes")
1852
+
1853
+ # Encode bootnode addresses to bytes
1854
+ add_bootnodes_encoded = [addr.encode("utf-8") for addr in add_bootnodes]
1855
+ remove_bootnodes_encoded = [
1856
+ addr.encode("utf-8") for addr in remove_bootnodes
1857
+ ]
1858
+
1859
+ call = self.substrate.compose_call(
1860
+ call_module="Network",
1861
+ call_function="update_bootnodes",
1862
+ call_params={
1863
+ "subnet_id": subnet_id,
1864
+ "add": add_bootnodes_encoded,
1865
+ "remove": remove_bootnodes_encoded,
1866
+ },
1867
+ )
1868
+
1869
+ response = self._submit_extrinsic(call, keypair)
1870
+
1871
+ if response["success"]:
1872
+ response["message"] = (
1873
+ f"Subnet {subnet_id} bootnodes updated successfully"
1874
+ )
1875
+
1876
+ return response
1877
+
1878
+ except Exception as e:
1879
+ logger.error(f"Error updating bootnodes: {e}")
1880
+ return {"success": False, "error": str(e)}
1881
+
1882
+ # ============================================================================
1883
+ # QUERY METHODS
1884
+ # ============================================================================
1885
+
1886
+ def get_subnet_info(self, subnet_id: int) -> Optional[SubnetInfo]:
1887
+ """Delegate to RPC layer for comprehensive subnet info."""
1888
+ from ..rpc.subnet import SubnetRpcClient
1889
+
1890
+ try:
1891
+ rpc_client = SubnetRpcClient(self.substrate)
1892
+ return rpc_client.get_subnet_info(subnet_id)
1893
+ except Exception as e:
1894
+ logger.error(f"Error getting subnet info: {e}")
1895
+ return None
1896
+
1897
+ def get_current_registration_cost(self) -> int:
1898
+ """Get the current subnet registration cost."""
1899
+ return self._get_current_registration_cost()
1900
+
1901
+ def list_subnets(self) -> list[SubnetInfo]:
1902
+ """
1903
+ list all registered subnets.
1904
+
1905
+ Returns:
1906
+ list of SubnetInfo objects
1907
+ """
1908
+ try:
1909
+ subnets = []
1910
+
1911
+ # Get total subnet count
1912
+ total_subnets = (
1913
+ self.substrate.query(
1914
+ module="Network", storage_function="TotalSubnetUids"
1915
+ ).value
1916
+ or 0
1917
+ )
1918
+
1919
+ # Query each subnet
1920
+ for subnet_id in range(1, total_subnets + 1):
1921
+ subnet_info = self.get_subnet_info(subnet_id)
1922
+ if subnet_info:
1923
+ subnets.append(subnet_info)
1924
+
1925
+ return subnets
1926
+
1927
+ except Exception as e:
1928
+ logger.error(f"Error listing subnets: {e}")
1929
+ return []
1930
+
1931
+ # ============================================================================
1932
+ # PRIVATE HELPER METHODS
1933
+ # ============================================================================
1934
+
1935
+ def _validate_registration_request(self, request: SubnetRegisterRequest):
1936
+ """Validate the registration request."""
1937
+ # Validate initial coldkeys
1938
+ for coldkey in request.initial_coldkeys:
1939
+ if not validate_address(coldkey):
1940
+ raise ValueError(f"Invalid coldkey address format: {coldkey}")
1941
+
1942
+ # Validate bootnodes format
1943
+ # Bootnodes is a set[str] in the model (matches BTreeSet in Rust)
1944
+ # Ensure it's not None and filter out any None values
1945
+ if request.bootnodes is None:
1946
+ request.bootnodes = set() # Default to empty set if None
1947
+ else:
1948
+ # Filter out None values from the set (shouldn't happen, but be safe)
1949
+ request.bootnodes = {
1950
+ bn for bn in request.bootnodes if bn is not None and isinstance(bn, str)
1951
+ }
1952
+
1953
+ for bootnode in request.bootnodes:
1954
+ if not isinstance(bootnode, str):
1955
+ raise ValueError(f"Bootnode must be a string, got {type(bootnode)}")
1956
+ if not bootnode.strip():
1957
+ raise ValueError("Bootnode cannot be empty")
1958
+
1959
+ def _prepare_subnet_data(self, request: SubnetRegisterRequest) -> dict[str, Any]:
1960
+ """
1961
+ Prepare subnet data for the extrinsic call.
1962
+
1963
+ RegistrationSubnetData structure (from Rust):
1964
+ - name: Vec<u8>
1965
+ - repo: Vec<u8>
1966
+ - description: Vec<u8>
1967
+ - misc: Vec<u8>
1968
+ - min_stake: u128
1969
+ - max_stake: u128
1970
+ - delegate_stake_percentage: u128
1971
+ - initial_validators: BTreeMap<u32, u32> (validator_id -> max_registrations)
1972
+ - bootnodes: BTreeMap<PeerId, BoundedVec<u8, ...>>
1973
+ """
1974
+
1975
+ def encode_validator_limits(accounts: dict[int, int]) -> list[tuple[int, int]]:
1976
+ encoded = []
1977
+ for validator_id, max_registrations in accounts.items():
1978
+ final_max_registrations = max(max_registrations, 1)
1979
+ encoded.append((int(validator_id), final_max_registrations))
1980
+
1981
+ encoded.sort(key=lambda item: item[0])
1982
+ return encoded
1983
+
1984
+ def encode_bootnodes(bootnodes: set[str]) -> list[tuple[bytes, bytes]]:
1985
+ encoded = []
1986
+ for bootnode in bootnodes:
1987
+ if (
1988
+ bootnode is None
1989
+ or not isinstance(bootnode, str)
1990
+ or not bootnode.strip()
1991
+ ):
1992
+ continue
1993
+
1994
+ parts = bootnode.rsplit("/p2p/", 1)
1995
+ if len(parts) != 2 or not parts[1].strip():
1996
+ raise ValueError(
1997
+ f"Bootnode must include a peer id using /p2p/<peer_id>: {bootnode}"
1998
+ )
1999
+
2000
+ multiaddr = bootnode.strip()
2001
+ peer_id = parts[1].strip()
2002
+ if not multiaddr:
2003
+ raise ValueError(f"Bootnode multiaddr cannot be empty: {bootnode}")
2004
+
2005
+ encoded.append((encode_peer_id(peer_id), _encode_multiaddr(multiaddr)))
2006
+
2007
+ encoded.sort(key=lambda item: item[0])
2008
+ return encoded
2009
+
2010
+ initial_validators_list = encode_validator_limits(request.initial_validators)
2011
+ bootnodes_list = encode_bootnodes(request.bootnodes or set())
2012
+
2013
+ return {
2014
+ "name": request.name.encode("utf-8") if request.name else b"",
2015
+ "repo": request.repo.encode("utf-8") if request.repo else b"",
2016
+ "description": (
2017
+ request.description.encode("utf-8") if request.description else b""
2018
+ ),
2019
+ "misc": request.misc.encode("utf-8") if request.misc else b"",
2020
+ "min_stake": request.min_stake,
2021
+ "max_stake": request.max_stake,
2022
+ "delegate_stake_percentage": request.delegate_stake_percentage,
2023
+ "initial_validators": initial_validators_list,
2024
+ "bootnodes": bootnodes_list,
2025
+ }
2026
+
2027
+ def _get_current_registration_cost(self) -> int:
2028
+ """
2029
+ Get the current registration cost from the chain.
2030
+
2031
+ Uses ChainRpcClient to query storage and calculate current cost.
2032
+ Falls back to a safe default if query fails.
2033
+ """
2034
+ try:
2035
+ from ..rpc import ChainRpcClient
2036
+
2037
+ chain_rpc = ChainRpcClient(self.substrate)
2038
+ cost = chain_rpc.get_subnet_registration_cost()
2039
+
2040
+ if cost is not None:
2041
+ logger.info(f"Current registration cost: {cost / 1e18:.2f} TENSOR")
2042
+ return cost
2043
+
2044
+ # Fallback: return high value to ensure it passes
2045
+ logger.warning("Could not get registration cost, using default 100 TENSOR")
2046
+ return int(100 * 1e18)
2047
+ except Exception as e:
2048
+ logger.warning(
2049
+ f"Error getting registration cost: {e}, using default 100 TENSOR"
2050
+ )
2051
+ # Return safe default
2052
+ return int(100 * 1e18)
2053
+
2054
+ def _get_current_epoch(self) -> int:
2055
+ """Get the current epoch number."""
2056
+ try:
2057
+ block_header = self.substrate.get_block_header()
2058
+ current_block = block_header.get("number", 0) if block_header else 0
2059
+ epoch_length = (
2060
+ self.substrate.query(
2061
+ module="Network", storage_function="EpochLength"
2062
+ ).value
2063
+ or 300
2064
+ )
2065
+
2066
+ return current_block // epoch_length
2067
+ except Exception as e:
2068
+ logger.warning(f"Could not get current epoch: {e}")
2069
+ return 0
2070
+
2071
+ def _execute_owner_value_update(
2072
+ self,
2073
+ request: SubnetConfigUpdateRequest,
2074
+ keypair: Keypair,
2075
+ call_function: str,
2076
+ success_message: str,
2077
+ ) -> dict[str, Any]:
2078
+ """
2079
+ Helper to execute owner-only configuration updates with a single value.
2080
+
2081
+ Args:
2082
+ request: Configuration update request with subnet_id and value
2083
+ keypair: Keypair for signing the transaction
2084
+ call_function: Runtime call to execute
2085
+ success_message: Message to attach upon success
2086
+
2087
+ Returns:
2088
+ dictionary with operation result
2089
+ """
2090
+ try:
2091
+ logger.info(
2092
+ f"Calling {call_function} for subnet {request.subnet_id} with value {request.value}"
2093
+ )
2094
+
2095
+ call = self.substrate.compose_call(
2096
+ call_module="Network",
2097
+ call_function=call_function,
2098
+ call_params={"subnet_id": request.subnet_id, "value": request.value},
2099
+ )
2100
+
2101
+ response = self._submit_extrinsic(call, keypair)
2102
+
2103
+ if response.get("success"):
2104
+ response["message"] = success_message.format(
2105
+ subnet_id=request.subnet_id
2106
+ )
2107
+
2108
+ return response
2109
+
2110
+ except Exception as e:
2111
+ logger.error(
2112
+ f"Error executing {call_function} for subnet {request.subnet_id}: {e}"
2113
+ )
2114
+ return {"success": False, "error": str(e)}
2115
+
2116
+ # _submit_extrinsic method is now inherited from BaseExtrinsicClient
2117
+
2118
+ def _extract_registration_event(self, response) -> dict[str, Any]:
2119
+ """Extract subnet registration event data from the response."""
2120
+ event_data = {}
2121
+
2122
+ try:
2123
+ for event in response.triggered_events:
2124
+ if (
2125
+ event.value["module_id"] == "Network"
2126
+ and event.value["event_id"] == "SubnetRegistered"
2127
+ ):
2128
+ attributes = event.value.get("attributes", [])
2129
+ # SubnetRegistered event has 3 attributes: [owner, name, subnet_id]
2130
+ if isinstance(attributes, list) and len(attributes) >= 3:
2131
+ # Handle name which might be bytes
2132
+ name_value = attributes[1]
2133
+ if isinstance(name_value, bytes):
2134
+ name_str = name_value.decode("utf-8")
2135
+ elif (
2136
+ isinstance(name_value, (list, tuple))
2137
+ and len(name_value) > 0
2138
+ ):
2139
+ name_str = (
2140
+ name_value[0].decode("utf-8")
2141
+ if isinstance(name_value[0], bytes)
2142
+ else str(name_value[0])
2143
+ )
2144
+ else:
2145
+ name_str = str(name_value)
2146
+
2147
+ event_data = {
2148
+ "subnet_id": attributes[2], # subnet_id
2149
+ "owner": attributes[0], # owner
2150
+ "name": name_str, # name
2151
+ }
2152
+ break
2153
+ except Exception as e:
2154
+ logger.warning(f"Could not extract event data: {e}")
2155
+
2156
+ return event_data
2157
+
2158
+ def _extract_registration_event_from_result(
2159
+ self, result: dict[str, Any]
2160
+ ) -> dict[str, Any]:
2161
+ """Extract subnet registration event data from result dict."""
2162
+ event_data = {}
2163
+
2164
+ try:
2165
+ events = result.get("events", [])
2166
+ logger.debug(f"Extracting from {len(events)} events")
2167
+
2168
+ for idx, event in enumerate(events):
2169
+ logger.debug(f"Event {idx}: {type(event)}")
2170
+
2171
+ # Check if event has value attribute
2172
+ if hasattr(event, "value"):
2173
+ event_value = event.value
2174
+ module_id = event_value.get("module_id", "")
2175
+ event_id = event_value.get("event_id", "")
2176
+
2177
+ if module_id == "Network" and event_id == "SubnetRegistered":
2178
+ attributes = event_value.get("attributes", {})
2179
+
2180
+ # Handle both dict and list formats
2181
+ if isinstance(attributes, dict):
2182
+ event_data = {
2183
+ "subnet_id": attributes.get("subnet_id"),
2184
+ "owner": attributes.get("owner"),
2185
+ "name": attributes.get("name"),
2186
+ }
2187
+ elif isinstance(attributes, list) and len(attributes) >= 3:
2188
+ # Handle name which might be bytes or a tuple
2189
+ name_value = attributes[1]
2190
+ if isinstance(name_value, bytes):
2191
+ name_str = name_value.decode("utf-8")
2192
+ elif (
2193
+ isinstance(name_value, (list, tuple))
2194
+ and len(name_value) > 0
2195
+ ):
2196
+ # If it's a tuple/list, take the first element
2197
+ name_str = (
2198
+ name_value[0].decode("utf-8")
2199
+ if isinstance(name_value[0], bytes)
2200
+ else str(name_value[0])
2201
+ )
2202
+ else:
2203
+ name_str = str(name_value)
2204
+
2205
+ event_data = {
2206
+ "subnet_id": attributes[2],
2207
+ "owner": attributes[0],
2208
+ "name": name_str,
2209
+ }
2210
+
2211
+ break
2212
+
2213
+ except Exception as e:
2214
+ logger.warning(f"Event extraction error: {e}")
2215
+
2216
+ return event_data
2217
+
2218
+ # _extract_error_message method is now inherited from BaseExtrinsicClient