htcli 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- src/htcli/utils/wallet/migration.py +159 -0
|
@@ -0,0 +1,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"]
|