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,203 @@
1
+ """
2
+ Validator management extrinsics for HTCLI.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+
7
+ from substrateinterface import Keypair, SubstrateInterface
8
+
9
+ from ...models.requests.validator import (
10
+ ValidatorColdkeyUpdateRequest,
11
+ ValidatorDelegateAccountUpdateRequest,
12
+ ValidatorDelegateRewardRateUpdateRequest,
13
+ ValidatorHotkeyUpdateRequest,
14
+ ValidatorIdentityUpdateRequest,
15
+ ValidatorRegisterRequest,
16
+ )
17
+ from ...utils.logging import get_logger
18
+ from .base import BaseExtrinsicClient
19
+ from .node import SimpleExtrinsicResponse
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ class ValidatorExtrinsics(BaseExtrinsicClient):
25
+ """Client for validator-related extrinsics."""
26
+
27
+ def __init__(self, substrate: Optional[SubstrateInterface] = None):
28
+ super().__init__(substrate)
29
+
30
+ def register_validator(
31
+ self, request: ValidatorRegisterRequest, keypair: Keypair
32
+ ) -> SimpleExtrinsicResponse:
33
+ """Register a validator using live runtime metadata parameters."""
34
+ try:
35
+ logger.info("Registering validator")
36
+
37
+ delegate_account = None
38
+ if request.delegate_account_id:
39
+ delegate_account = {
40
+ "account_id": request.delegate_account_id,
41
+ "rate": (
42
+ 0
43
+ if request.delegate_account_rate is None
44
+ else request.delegate_account_rate
45
+ ),
46
+ }
47
+
48
+ call_params: dict[str, Any] = {
49
+ "hotkey": request.hotkey,
50
+ "delegate_reward_rate": request.delegate_reward_rate,
51
+ "delegate_account": delegate_account,
52
+ "identity": request.identity,
53
+ }
54
+
55
+ call = self.substrate.compose_call(
56
+ call_module="Network",
57
+ call_function="register_validator",
58
+ call_params=call_params,
59
+ )
60
+ result = self._submit_extrinsic(call, keypair)
61
+
62
+ if result["success"]:
63
+ self._attach_validator_event_fields(result, request.hotkey)
64
+ result["message"] = "Validator registered successfully"
65
+
66
+ return SimpleExtrinsicResponse(result)
67
+ except Exception as e:
68
+ logger.error(f"Error registering validator: {e}")
69
+ return SimpleExtrinsicResponse({"success": False, "error": str(e)})
70
+
71
+ def update_validator_delegate_reward_rate(
72
+ self,
73
+ request: ValidatorDelegateRewardRateUpdateRequest,
74
+ keypair: Keypair,
75
+ ) -> SimpleExtrinsicResponse:
76
+ """Update a validator delegate reward rate."""
77
+ return self._submit_validator_update(
78
+ call_function="update_validator_delegate_reward_rate",
79
+ call_params={
80
+ "validator_id": request.validator_id,
81
+ "new_delegate_reward_rate": request.new_delegate_reward_rate,
82
+ },
83
+ keypair=keypair,
84
+ success_message=(
85
+ f"Validator {request.validator_id} delegate reward rate updated"
86
+ ),
87
+ )
88
+
89
+ def update_validator_delegate_account(
90
+ self,
91
+ request: ValidatorDelegateAccountUpdateRequest,
92
+ keypair: Keypair,
93
+ ) -> SimpleExtrinsicResponse:
94
+ """Update or clear a validator delegate account."""
95
+ return self._submit_validator_update(
96
+ call_function="update_validator_delegate_account",
97
+ call_params={
98
+ "validator_id": request.validator_id,
99
+ "delegate_account_id": request.delegate_account_id,
100
+ "delegate_rate": request.delegate_rate,
101
+ },
102
+ keypair=keypair,
103
+ success_message=f"Validator {request.validator_id} delegate account updated",
104
+ )
105
+
106
+ def update_validator_hotkey(
107
+ self,
108
+ request: ValidatorHotkeyUpdateRequest,
109
+ keypair: Keypair,
110
+ ) -> SimpleExtrinsicResponse:
111
+ """Update a validator hotkey."""
112
+ return self._submit_validator_update(
113
+ call_function="update_validator_hotkey",
114
+ call_params={
115
+ "validator_id": request.validator_id,
116
+ "new_hotkey": request.new_hotkey,
117
+ },
118
+ keypair=keypair,
119
+ success_message=f"Validator {request.validator_id} hotkey updated",
120
+ )
121
+
122
+ def update_validator_coldkey(
123
+ self,
124
+ request: ValidatorColdkeyUpdateRequest,
125
+ keypair: Keypair,
126
+ ) -> SimpleExtrinsicResponse:
127
+ """Update a validator coldkey."""
128
+ return self._submit_validator_update(
129
+ call_function="update_validator_coldkey",
130
+ call_params={
131
+ "validator_id": request.validator_id,
132
+ "new_coldkey": request.new_coldkey,
133
+ },
134
+ keypair=keypair,
135
+ success_message=f"Validator {request.validator_id} coldkey updated",
136
+ )
137
+
138
+ def update_validator_identity(
139
+ self,
140
+ request: ValidatorIdentityUpdateRequest,
141
+ keypair: Keypair,
142
+ ) -> SimpleExtrinsicResponse:
143
+ """Update or clear a validator identity."""
144
+ return self._submit_validator_update(
145
+ call_function="update_validator_identity",
146
+ call_params={
147
+ "validator_id": request.validator_id,
148
+ "identity": request.identity,
149
+ },
150
+ keypair=keypair,
151
+ success_message=f"Validator {request.validator_id} identity updated",
152
+ )
153
+
154
+ def _submit_validator_update(
155
+ self,
156
+ call_function: str,
157
+ call_params: dict[str, Any],
158
+ keypair: Keypair,
159
+ success_message: str,
160
+ ) -> SimpleExtrinsicResponse:
161
+ try:
162
+ call = self.substrate.compose_call(
163
+ call_module="Network",
164
+ call_function=call_function,
165
+ call_params=call_params,
166
+ )
167
+ result = self._submit_extrinsic(call, keypair)
168
+ if result["success"]:
169
+ result["message"] = success_message
170
+ result["validator_id"] = call_params.get("validator_id")
171
+ return SimpleExtrinsicResponse(result)
172
+ except Exception as e:
173
+ logger.error(f"Error submitting validator update {call_function}: {e}")
174
+ return SimpleExtrinsicResponse({"success": False, "error": str(e)})
175
+
176
+ def _attach_validator_event_fields(
177
+ self, result: dict[str, Any], fallback_hotkey: str
178
+ ) -> None:
179
+ for event in result.get("events", []):
180
+ if not hasattr(event, "value"):
181
+ continue
182
+ event_data = event.value
183
+ if event_data.get("module_id") != "Network":
184
+ continue
185
+ if event_data.get("event_id") not in {
186
+ "ValidatorRegistered",
187
+ "ValidatorCreated",
188
+ }:
189
+ continue
190
+
191
+ attributes = event_data.get("attributes", {})
192
+ if isinstance(attributes, dict):
193
+ result["validator_id"] = attributes.get("validator_id")
194
+ result["hotkey"] = attributes.get("hotkey", fallback_hotkey)
195
+ elif isinstance(attributes, (list, tuple)):
196
+ if attributes:
197
+ result["validator_id"] = attributes[0]
198
+ if len(attributes) > 1:
199
+ result["hotkey"] = attributes[1]
200
+ break
201
+
202
+ if "hotkey" not in result or not result["hotkey"]:
203
+ result["hotkey"] = fallback_hotkey
@@ -0,0 +1,323 @@
1
+ """
2
+ Wallet extrinsics operations for blockchain state changes.
3
+ Handles staking, unstaking, transfers, and other wallet-related transactions.
4
+ """
5
+
6
+ from substrateinterface import SubstrateInterface
7
+
8
+ from ...utils.logging import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class WalletExtrinsics:
14
+ """Extrinsics client for wallet state-changing operations."""
15
+
16
+ def __init__(self, substrate: SubstrateInterface):
17
+ self.substrate = substrate
18
+
19
+ def transfer_funds(
20
+ self, from_address: str, to_address: str, amount: str, keypair=None
21
+ ):
22
+ """Transfer funds using Balances.transfer with real transaction submission."""
23
+ try:
24
+ if not self.substrate:
25
+ raise Exception("Not connected to blockchain")
26
+
27
+ # Convert amount to the smallest unit (18 decimals for TENSOR)
28
+ amount_in_smallest_unit = int(float(amount) * 1e18)
29
+
30
+ # Convert address to proper format for substrate
31
+ if to_address.startswith("0x"):
32
+ # EVM address format
33
+ dest_bytes = bytes.fromhex(to_address[2:])
34
+ else:
35
+ # SS58 address format
36
+ from substrateinterface.utils.ss58 import ss58_decode
37
+
38
+ try:
39
+ decoded_hex = ss58_decode(to_address)
40
+ dest_bytes = bytes.fromhex(decoded_hex)
41
+ except Exception as e:
42
+ logger.error(
43
+ f"Failed to decode SS58 address {to_address}: {str(e)}"
44
+ )
45
+ raise Exception(f"Invalid SS58 address format: {str(e)}") from e
46
+
47
+ if len(dest_bytes) not in [20, 32]:
48
+ raise Exception("Address must be 20 or 32 bytes.")
49
+
50
+ # If keypair provided, check balance before submitting transaction
51
+ if keypair:
52
+ # Check sender's balance before attempting transfer
53
+ try:
54
+ account_info = self.substrate.query("System", "Account", [from_address])
55
+ available_balance = (
56
+ account_info.value["data"]["free"]
57
+ if account_info and account_info.value
58
+ else 0
59
+ )
60
+
61
+ # Check if balance is sufficient (including potential fees)
62
+ # Reserve some buffer for transaction fees (estimate ~0.01 TENSOR)
63
+ fee_buffer = int(0.01 * 1e18) # 0.01 TENSOR in smallest units
64
+ required_balance = amount_in_smallest_unit + fee_buffer
65
+
66
+ if available_balance < amount_in_smallest_unit:
67
+ error_msg = (
68
+ f"Insufficient balance. Available: {available_balance / 1e18:.4f} TENSOR, "
69
+ f"Required: {float(amount):.4f} TENSOR"
70
+ )
71
+ logger.error(error_msg)
72
+ raise Exception(error_msg)
73
+
74
+ if available_balance < required_balance:
75
+ logger.warning(
76
+ f"Balance may be insufficient after fees. Available: {available_balance / 1e18:.4f} TENSOR, "
77
+ f"Transfer amount: {float(amount):.4f} TENSOR"
78
+ )
79
+ except Exception as e:
80
+ # If balance check fails, log but don't block transfer
81
+ # (in case of network issues, let blockchain validate)
82
+ logger.warning(f"Could not verify balance before transfer: {str(e)}")
83
+ # Re-raise if it's our insufficient balance error
84
+ if "Insufficient balance" in str(e):
85
+ raise
86
+
87
+ # Compose the call using Balances pallet
88
+ call_data = self.substrate.compose_call(
89
+ call_module="Balances",
90
+ call_function="transfer_keep_alive",
91
+ call_params={
92
+ "dest": dest_bytes,
93
+ "value": amount_in_smallest_unit,
94
+ },
95
+ )
96
+
97
+ # If keypair provided, submit real transaction
98
+ if keypair:
99
+ # Create and submit transaction
100
+ extrinsic = self.substrate.create_signed_extrinsic(
101
+ call=call_data, keypair=keypair
102
+ )
103
+
104
+ # Submit and wait for confirmation with timeout
105
+ try:
106
+ receipt = self.substrate.submit_extrinsic(
107
+ extrinsic=extrinsic, wait_for_inclusion=True
108
+ )
109
+ except Exception as e:
110
+ error_str = str(e)
111
+ # Improve error message for fee-related errors
112
+ if "1010" in error_str or "Inability to pay" in error_str or "balance too low" in error_str.lower():
113
+ error_msg = (
114
+ f"Transaction failed: Insufficient balance to pay transaction fees.\n"
115
+ f" • The account needs sufficient balance to cover both the transfer amount and transaction fees.\n"
116
+ f" • Please ensure the account has enough TENSOR to cover fees (typically ~0.01-0.1 TENSOR).\n"
117
+ f" • Original error: {error_str}"
118
+ )
119
+ logger.error(error_msg)
120
+ raise Exception(error_msg) from e
121
+ logger.error(f"Failed to submit extrinsic: {error_str}")
122
+ raise Exception(f"Transaction submission failed: {error_str}") from e
123
+
124
+ # CRITICAL: Check if transaction actually succeeded
125
+ # Even if no exception was raised, the transaction might have failed
126
+ if not hasattr(receipt, "is_success") or not receipt.is_success:
127
+ # Extract error message from receipt
128
+ error_msg = "Transaction failed"
129
+ if hasattr(receipt, "error_message") and receipt.error_message:
130
+ error_msg = str(receipt.error_message)
131
+ elif hasattr(receipt, "triggered_events"):
132
+ # Check for ExtrinsicFailed events
133
+ for event in receipt.triggered_events:
134
+ try:
135
+ event_data = event.value if hasattr(event, "value") else event
136
+ if (
137
+ event_data.get("module_id") == "System"
138
+ and event_data.get("event_id") == "ExtrinsicFailed"
139
+ ):
140
+ error_msg = "Transaction failed: ExtrinsicFailed event detected"
141
+ break
142
+ except Exception:
143
+ continue
144
+
145
+ logger.error(f"Transfer transaction failed: {error_msg}")
146
+ raise Exception(f"Insufficient balance or transaction failed: {error_msg}")
147
+
148
+ # Check for ExtrinsicFailed events even if is_success is True
149
+ # Sometimes transactions are included but fail at runtime
150
+ extrinsic_failed = False
151
+ if hasattr(receipt, "triggered_events"):
152
+ for event in receipt.triggered_events:
153
+ try:
154
+ event_data = event.value if hasattr(event, "value") else event
155
+ if (
156
+ event_data.get("module_id") == "System"
157
+ and event_data.get("event_id") == "ExtrinsicFailed"
158
+ ):
159
+ extrinsic_failed = True
160
+ break
161
+ except Exception:
162
+ # Skip malformed events
163
+ continue
164
+
165
+ if extrinsic_failed:
166
+ error_msg = "Transaction failed: ExtrinsicFailed event detected"
167
+ logger.error(f"Transfer transaction failed: {error_msg}")
168
+ raise Exception(f"Insufficient balance or transaction failed: {error_msg}")
169
+
170
+ # Return real transaction details
171
+ return {
172
+ "success": True,
173
+ "message": "Transfer completed successfully",
174
+ "transaction_hash": receipt.extrinsic_hash,
175
+ "block_hash": receipt.block_hash,
176
+ "block_number": receipt.block_number,
177
+ "data": {
178
+ "from_address": from_address,
179
+ "to_address": to_address,
180
+ "amount": amount,
181
+ "amount_in_smallest_unit": amount_in_smallest_unit,
182
+ "receipt": receipt,
183
+ "fee": receipt.get("total_fee_amount"),
184
+ },
185
+ }
186
+ else:
187
+ # Return composed call data for manual submission
188
+ return {
189
+ "success": True,
190
+ "message": "Transfer call composed successfully",
191
+ "transaction_hash": None,
192
+ "block_number": None,
193
+ "data": {
194
+ "from_address": from_address,
195
+ "to_address": to_address,
196
+ "amount": amount,
197
+ "amount_in_smallest_unit": amount_in_smallest_unit,
198
+ "call_data": call_data,
199
+ },
200
+ }
201
+ except Exception as e:
202
+ # Use the centralized error handling system
203
+ from src.htcli.errors import handle_blockchain_error
204
+
205
+ blockchain_error = handle_blockchain_error(str(e))
206
+ raise blockchain_error from e
207
+
208
+ def update_coldkey(
209
+ self, hotkey: str, new_coldkey: str, keypair=None
210
+ ) -> dict:
211
+ """
212
+ Update the coldkey owner of a hotkey on-chain.
213
+
214
+ This changes the blockchain state, moving ownership from the current coldkey
215
+ (signer) to the new coldkey. This is the authoritative way to change hotkey ownership.
216
+
217
+ Args:
218
+ hotkey: The hotkey address whose owner is being changed
219
+ new_coldkey: The new coldkey address that will own the hotkey
220
+ keypair: Keypair of the current coldkey owner (must sign the transaction)
221
+
222
+ Returns:
223
+ Dictionary with transaction result including hash, block number, etc.
224
+ """
225
+ try:
226
+ if not self.substrate:
227
+ raise Exception("Not connected to blockchain")
228
+
229
+ if not keypair:
230
+ raise Exception("Keypair required to sign update_coldkey transaction")
231
+
232
+ # Validate addresses
233
+ from ...utils.blockchain import validate_address
234
+ validate_address(hotkey)
235
+ validate_address(new_coldkey)
236
+
237
+ # Ensure hotkey != new_coldkey
238
+ if hotkey.lower() == new_coldkey.lower():
239
+ raise Exception("Hotkey cannot be the same as the new coldkey")
240
+
241
+ # Compose the call using Network pallet
242
+ call_data = self.substrate.compose_call(
243
+ call_module="Network",
244
+ call_function="update_coldkey",
245
+ call_params={
246
+ "hotkey": hotkey,
247
+ "new_coldkey": new_coldkey,
248
+ },
249
+ )
250
+
251
+ # Create and submit transaction
252
+ extrinsic = self.substrate.create_signed_extrinsic(
253
+ call=call_data, keypair=keypair
254
+ )
255
+
256
+ # Submit and wait for finalization
257
+ try:
258
+ receipt = self.substrate.submit_extrinsic(
259
+ extrinsic=extrinsic,
260
+ wait_for_inclusion=True,
261
+ wait_for_finalization=True,
262
+ )
263
+ except Exception as e:
264
+ logger.error(f"Failed to submit update_coldkey extrinsic: {str(e)}")
265
+ raise Exception(f"Transaction submission failed: {str(e)}") from e
266
+
267
+ # Check if transaction actually succeeded
268
+ if not hasattr(receipt, "is_success") or not receipt.is_success:
269
+ error_msg = "Transaction failed"
270
+ if hasattr(receipt, "error_message") and receipt.error_message:
271
+ error_msg = str(receipt.error_message)
272
+ elif hasattr(receipt, "triggered_events"):
273
+ for event in receipt.triggered_events:
274
+ try:
275
+ event_data = event.value if hasattr(event, "value") else event
276
+ if (
277
+ event_data.get("module_id") == "System"
278
+ and event_data.get("event_id") == "ExtrinsicFailed"
279
+ ):
280
+ error_msg = "Transaction failed: ExtrinsicFailed event detected"
281
+ break
282
+ except Exception:
283
+ continue
284
+
285
+ logger.error(f"Update coldkey transaction failed: {error_msg}")
286
+ raise Exception(f"Update coldkey failed: {error_msg}")
287
+
288
+ # Check for ExtrinsicFailed events
289
+ extrinsic_failed = False
290
+ if hasattr(receipt, "triggered_events"):
291
+ for event in receipt.triggered_events:
292
+ try:
293
+ event_data = event.value if hasattr(event, "value") else event
294
+ if (
295
+ event_data.get("module_id") == "System"
296
+ and event_data.get("event_id") == "ExtrinsicFailed"
297
+ ):
298
+ extrinsic_failed = True
299
+ break
300
+ except Exception:
301
+ continue
302
+
303
+ if extrinsic_failed:
304
+ error_msg = "Transaction failed: ExtrinsicFailed event detected"
305
+ logger.error(f"Update coldkey transaction failed: {error_msg}")
306
+ raise Exception(f"Update coldkey failed: {error_msg}")
307
+
308
+ # Return transaction details
309
+ return {
310
+ "success": True,
311
+ "message": "Coldkey updated successfully on-chain",
312
+ "transaction_hash": receipt.extrinsic_hash,
313
+ "block_hash": receipt.block_hash,
314
+ "block_number": receipt.block_number,
315
+ "data": {
316
+ "hotkey": hotkey,
317
+ "old_coldkey": keypair.ss58_address,
318
+ "new_coldkey": new_coldkey,
319
+ },
320
+ }
321
+ except Exception as e:
322
+ logger.error(f"Error updating coldkey: {e}")
323
+ raise
@@ -0,0 +1,10 @@
1
+ """
2
+ Off-chain module for local operations that don't interact with the blockchain.
3
+ Contains utilities for wallet management, configuration, and local state management.
4
+ """
5
+
6
+ from .backup import BackupManager
7
+ from .config import ConfigManager
8
+ from .wallet import WalletManager
9
+
10
+ __all__ = ["WalletManager", "ConfigManager", "BackupManager"]