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,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Temporary patches for blockchain SCALE encoding quirks.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from scalecodec.types import CompactU32, Map
|
|
10
|
+
|
|
11
|
+
# Import logger for debugging
|
|
12
|
+
try:
|
|
13
|
+
from ...utils.logging import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
except ImportError:
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_ORIGINAL_MAP_PROCESS_ENCODE = Map.process_encode
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _should_patch_btreemap_accountid_u32(value: Any, map_instance: Map) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Determine whether we need to patch the Map encoder for BTreeMap<AccountId, u32>.
|
|
27
|
+
|
|
28
|
+
The issue arises when map_instance.map_key/map_value are None because the
|
|
29
|
+
runtime metadata does not correctly expose the key/value types, which happens
|
|
30
|
+
for BTreeMap<AccountId, u32> in the Hypertensor runtime.
|
|
31
|
+
"""
|
|
32
|
+
if map_instance.map_key and map_instance.map_value:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
# Value must be a list/tuple of 2-tuples
|
|
36
|
+
if not isinstance(value, Iterable):
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
first_item = next(iter(value))
|
|
41
|
+
except StopIteration:
|
|
42
|
+
# Empty map - no need to patch
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
if not isinstance(first_item, (tuple, list)) or len(first_item) != 2:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
key_candidate, value_candidate = first_item
|
|
49
|
+
|
|
50
|
+
# Keys should be 20-byte addresses (bytes)
|
|
51
|
+
if not isinstance(key_candidate, (bytes, bytearray)) or len(key_candidate) != 20:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# Values should be ints (u32)
|
|
55
|
+
if not isinstance(value_candidate, int):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _should_patch_btreemap_u32_u32(value: Any, map_instance: Map) -> bool:
|
|
62
|
+
"""Detect BTreeMap<u32, u32> values when metadata lacks key/value types."""
|
|
63
|
+
if map_instance.map_key and map_instance.map_value:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
if not isinstance(value, Iterable):
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
first_item = next(iter(value))
|
|
71
|
+
except StopIteration:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
if not isinstance(first_item, (tuple, list)) or len(first_item) != 2:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
key_candidate, value_candidate = first_item
|
|
78
|
+
return isinstance(key_candidate, int) and isinstance(value_candidate, int)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _should_patch_btreemap_bytes_bytes(value: Any, map_instance: Map) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Detect BTreeMap<PeerId, BoundedVec<u8, ...>> values when metadata lacks types.
|
|
84
|
+
|
|
85
|
+
Both PeerId and BoundedVec<u8, ...> are encoded like Vec<u8> in the deployed
|
|
86
|
+
Hypertensor runtime metadata.
|
|
87
|
+
"""
|
|
88
|
+
if map_instance.map_key and map_instance.map_value:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
if not isinstance(value, Iterable):
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
first_item = next(iter(value))
|
|
96
|
+
except StopIteration:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
if not isinstance(first_item, (tuple, list)) or len(first_item) != 2:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
key_candidate, value_candidate = first_item
|
|
103
|
+
return isinstance(key_candidate, (bytes, bytearray)) and isinstance(
|
|
104
|
+
value_candidate, (bytes, bytearray)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _encode_compact_bytes(value: bytes | bytearray) -> bytes:
|
|
109
|
+
element_count_compact = CompactU32()
|
|
110
|
+
element_count_compact.encode(len(value))
|
|
111
|
+
return element_count_compact.data + bytes(value)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _patched_process_encode(self: Map, value: Any) -> bytes:
|
|
115
|
+
"""
|
|
116
|
+
Patched Map.process_encode that handles BTreeMap<AccountId, u32>.
|
|
117
|
+
|
|
118
|
+
This patch is needed because substrate-interface/scalecodec may not correctly
|
|
119
|
+
identify BTreeMap<AccountId, u32> from runtime metadata, especially when
|
|
120
|
+
AccountId is defined as [u8; 20].
|
|
121
|
+
|
|
122
|
+
Encoding format:
|
|
123
|
+
- Compact<u32> length
|
|
124
|
+
- For each entry (sorted by key):
|
|
125
|
+
- [u8; 20] AccountId (20 raw bytes)
|
|
126
|
+
- u32 value (4 bytes, little-endian)
|
|
127
|
+
"""
|
|
128
|
+
if _should_patch_btreemap_u32_u32(value, self):
|
|
129
|
+
value_list = list(value)
|
|
130
|
+
value_list.sort(key=lambda x: x[0])
|
|
131
|
+
|
|
132
|
+
element_count_compact = CompactU32()
|
|
133
|
+
element_count_compact.encode(len(value_list))
|
|
134
|
+
data = element_count_compact.data
|
|
135
|
+
|
|
136
|
+
self.map_key = "u32"
|
|
137
|
+
self.map_value = "u32"
|
|
138
|
+
|
|
139
|
+
for idx, item in enumerate(value_list):
|
|
140
|
+
if not isinstance(item, (tuple, list)) or len(item) != 2:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"BTreeMap entry {idx} must be (key, value) tuple, got: {type(item)}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
item_key, item_value = item
|
|
146
|
+
if not isinstance(item_key, int) or not isinstance(item_value, int):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"BTreeMap<u32, u32> entry {idx} must contain ints, got: {item}"
|
|
149
|
+
)
|
|
150
|
+
if not 0 <= item_key <= 4294967295:
|
|
151
|
+
raise ValueError(f"u32 key at index {idx} out of range: {item_key}")
|
|
152
|
+
if not 0 <= item_value <= 4294967295:
|
|
153
|
+
raise ValueError(f"u32 value at index {idx} out of range: {item_value}")
|
|
154
|
+
|
|
155
|
+
data += item_key.to_bytes(4, byteorder="little", signed=False)
|
|
156
|
+
data += item_value.to_bytes(4, byteorder="little", signed=False)
|
|
157
|
+
|
|
158
|
+
return data
|
|
159
|
+
|
|
160
|
+
if _should_patch_btreemap_bytes_bytes(value, self):
|
|
161
|
+
value_list = list(value)
|
|
162
|
+
value_list.sort(key=lambda x: bytes(x[0]))
|
|
163
|
+
|
|
164
|
+
element_count_compact = CompactU32()
|
|
165
|
+
element_count_compact.encode(len(value_list))
|
|
166
|
+
data = element_count_compact.data
|
|
167
|
+
|
|
168
|
+
self.map_key = "Vec<u8>"
|
|
169
|
+
self.map_value = "Vec<u8>"
|
|
170
|
+
|
|
171
|
+
for idx, item in enumerate(value_list):
|
|
172
|
+
if not isinstance(item, (tuple, list)) or len(item) != 2:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"BTreeMap entry {idx} must be (key, value) tuple, got: {type(item)}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
item_key, item_value = item
|
|
178
|
+
if not isinstance(item_key, (bytes, bytearray)) or not isinstance(
|
|
179
|
+
item_value, (bytes, bytearray)
|
|
180
|
+
):
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"BTreeMap<bytes, bytes> entry {idx} must contain bytes, got: {item}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
data += _encode_compact_bytes(item_key)
|
|
186
|
+
data += _encode_compact_bytes(item_value)
|
|
187
|
+
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
if not _should_patch_btreemap_accountid_u32(value, self):
|
|
191
|
+
return _ORIGINAL_MAP_PROCESS_ENCODE(self, value)
|
|
192
|
+
|
|
193
|
+
logger.debug("Patching BTreeMap<AccountId, u32> encoding")
|
|
194
|
+
|
|
195
|
+
# Ensure we have a concrete list (value might be set, generator, etc.)
|
|
196
|
+
# BTreeMap entries must be sorted by key for deterministic encoding
|
|
197
|
+
value_list = list(value)
|
|
198
|
+
|
|
199
|
+
# Sort by key (address bytes) to match BTreeMap ordering
|
|
200
|
+
# This is critical - BTreeMap is always sorted!
|
|
201
|
+
value_list.sort(
|
|
202
|
+
key=lambda x: x[0] if isinstance(x, (tuple, list)) and len(x) >= 1 else b""
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
logger.debug(f"Encoding BTreeMap with {len(value_list)} entries")
|
|
206
|
+
|
|
207
|
+
# Encode length as Compact<u32>
|
|
208
|
+
element_count_compact = CompactU32()
|
|
209
|
+
element_count_compact.encode(len(value_list))
|
|
210
|
+
data = element_count_compact.data
|
|
211
|
+
|
|
212
|
+
# Set type information for scalecodec
|
|
213
|
+
self.map_key = "[u8; 20]"
|
|
214
|
+
self.map_value = "u32"
|
|
215
|
+
|
|
216
|
+
# Encode each entry
|
|
217
|
+
for idx, item in enumerate(value_list):
|
|
218
|
+
if not isinstance(item, (tuple, list)) or len(item) != 2:
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f"BTreeMap entry {idx} must be (key, value) tuple, got: {type(item)}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
item_key, item_value = item
|
|
224
|
+
|
|
225
|
+
# Encode key: [u8; 20] AccountId
|
|
226
|
+
if isinstance(item_key, bytes):
|
|
227
|
+
if len(item_key) != 20:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"AccountId at index {idx} must be exactly 20 bytes, got {len(item_key)} bytes"
|
|
230
|
+
)
|
|
231
|
+
# [u8; 20] is encoded as raw 20 bytes (no length prefix)
|
|
232
|
+
data += item_key
|
|
233
|
+
elif isinstance(item_key, bytearray):
|
|
234
|
+
if len(item_key) != 20:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"AccountId at index {idx} must be exactly 20 bytes, got {len(item_key)} bytes"
|
|
237
|
+
)
|
|
238
|
+
data += bytes(item_key)
|
|
239
|
+
else:
|
|
240
|
+
# Try to use scalecodec to encode (fallback)
|
|
241
|
+
try:
|
|
242
|
+
key_obj = self.runtime_config.create_scale_object(
|
|
243
|
+
type_string="[u8; 20]", metadata=self.metadata
|
|
244
|
+
)
|
|
245
|
+
data += key_obj.encode(item_key)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"Failed to encode AccountId key at index {idx}: {item_key}, error: {e}"
|
|
249
|
+
) from e
|
|
250
|
+
|
|
251
|
+
# Encode value: u32 (4 bytes, little-endian)
|
|
252
|
+
if isinstance(item_value, int):
|
|
253
|
+
# Ensure it fits in u32 range
|
|
254
|
+
if item_value < 0 or item_value > 4294967295:
|
|
255
|
+
raise ValueError(
|
|
256
|
+
f"u32 value at index {idx} must be 0-4294967295, got {item_value}"
|
|
257
|
+
)
|
|
258
|
+
# u32 is 4 bytes, little-endian
|
|
259
|
+
data += item_value.to_bytes(4, byteorder="little", signed=False)
|
|
260
|
+
else:
|
|
261
|
+
# Try to use scalecodec to encode (fallback)
|
|
262
|
+
try:
|
|
263
|
+
value_obj = self.runtime_config.create_scale_object(
|
|
264
|
+
type_string="u32", metadata=self.metadata
|
|
265
|
+
)
|
|
266
|
+
data += value_obj.encode(item_value)
|
|
267
|
+
except Exception as e:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"Failed to encode u32 value at index {idx}: {item_value}, error: {e}"
|
|
270
|
+
) from e
|
|
271
|
+
|
|
272
|
+
logger.debug(f"Successfully encoded BTreeMap<AccountId, u32> ({len(data)} bytes)")
|
|
273
|
+
return data
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@contextmanager
|
|
277
|
+
def patch_btreemap_accountid_u32_encoder():
|
|
278
|
+
"""
|
|
279
|
+
Context manager that patches scalecodec Map.process_encode to handle
|
|
280
|
+
BTreeMap<AccountId, u32> encoding when metadata lacks type information.
|
|
281
|
+
"""
|
|
282
|
+
Map.process_encode = _patched_process_encode
|
|
283
|
+
try:
|
|
284
|
+
yield
|
|
285
|
+
finally:
|
|
286
|
+
Map.process_encode = _ORIGINAL_MAP_PROCESS_ENCODE
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Peer ID encoding utilities for Hypertensor blockchain.
|
|
3
|
+
|
|
4
|
+
Handles proper encoding of libp2p peer IDs which are base58-encoded multihashes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import base58
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def encode_peer_id(peer_id: Union[str, bytes]) -> bytes:
|
|
13
|
+
"""
|
|
14
|
+
Encode a peer ID for blockchain submission.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
peer_id: Peer ID as string (base58 multihash) or bytes
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Encoded peer ID as bytes for blockchain submission
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: If peer ID format is invalid
|
|
24
|
+
"""
|
|
25
|
+
if isinstance(peer_id, bytes):
|
|
26
|
+
# Already bytes, validate it's base58 decodable
|
|
27
|
+
try:
|
|
28
|
+
# Try to decode and re-encode to validate
|
|
29
|
+
decoded = base58.b58decode(peer_id)
|
|
30
|
+
return peer_id
|
|
31
|
+
except Exception:
|
|
32
|
+
raise ValueError(f"Invalid peer ID bytes: {peer_id}")
|
|
33
|
+
|
|
34
|
+
if isinstance(peer_id, str):
|
|
35
|
+
# String peer ID - should be base58 multihash
|
|
36
|
+
if not peer_id:
|
|
37
|
+
raise ValueError("Peer ID cannot be empty")
|
|
38
|
+
|
|
39
|
+
# Validate it's a valid base58 string
|
|
40
|
+
try:
|
|
41
|
+
decoded = base58.b58decode(peer_id)
|
|
42
|
+
# Re-encode to get proper bytes representation
|
|
43
|
+
return peer_id.encode("utf-8")
|
|
44
|
+
except Exception as e:
|
|
45
|
+
raise ValueError(f"Invalid peer ID format '{peer_id}': {e}")
|
|
46
|
+
|
|
47
|
+
raise ValueError(f"Peer ID must be string or bytes, got {type(peer_id)}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def decode_peer_id(encoded_peer_id: bytes) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Decode a peer ID from blockchain response.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
encoded_peer_id: Peer ID bytes from blockchain
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Peer ID as base58 string
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If decoding fails
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(encoded_peer_id, bytes):
|
|
64
|
+
raise ValueError(f"Encoded peer ID must be bytes, got {type(encoded_peer_id)}")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# Try to decode as UTF-8 first (most common case)
|
|
68
|
+
peer_id_str = encoded_peer_id.decode("utf-8")
|
|
69
|
+
|
|
70
|
+
# Validate it's a valid base58 string
|
|
71
|
+
base58.b58decode(peer_id_str)
|
|
72
|
+
return peer_id_str
|
|
73
|
+
|
|
74
|
+
except UnicodeDecodeError:
|
|
75
|
+
# If not UTF-8, treat as raw bytes and encode to base58
|
|
76
|
+
return base58.b58encode(encoded_peer_id).decode("ascii")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
raise ValueError(f"Failed to decode peer ID: {e}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_peer_id_format(peer_id: str) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Validate that a peer ID is in correct format.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
peer_id: Peer ID string to validate
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if valid, False otherwise
|
|
90
|
+
"""
|
|
91
|
+
if not isinstance(peer_id, str) or not peer_id:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Check if it's a valid base58 string
|
|
96
|
+
decoded = base58.b58decode(peer_id)
|
|
97
|
+
|
|
98
|
+
# Basic validation - peer IDs are typically 34-46 characters
|
|
99
|
+
# and start with 'Qm' for SHA256 multihash
|
|
100
|
+
if len(peer_id) < 30 or len(peer_id) > 60:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
# Common peer ID prefixes for libp2p
|
|
104
|
+
valid_prefixes = ["Qm", "12D3", "16Uiu2HA"]
|
|
105
|
+
if not any(peer_id.startswith(prefix) for prefix in valid_prefixes):
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
except Exception:
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def generate_test_peer_id(seed: int = 0) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Generate a test peer ID for testing purposes.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
seed: Seed for deterministic generation
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Valid test peer ID string
|
|
123
|
+
"""
|
|
124
|
+
import hashlib
|
|
125
|
+
|
|
126
|
+
# Generate deterministic test data
|
|
127
|
+
test_data = f"test_peer_id_{seed}".encode()
|
|
128
|
+
hash_bytes = hashlib.sha256(test_data).digest()
|
|
129
|
+
|
|
130
|
+
# Create a valid multihash (Qm prefix indicates SHA256)
|
|
131
|
+
# Format: [0x12, 0x20, <32-byte-hash>]
|
|
132
|
+
multihash = b"\x12\x20" + hash_bytes
|
|
133
|
+
|
|
134
|
+
# Encode as base58
|
|
135
|
+
peer_id = base58.b58encode(multihash).decode("ascii")
|
|
136
|
+
|
|
137
|
+
return peer_id
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Common peer ID patterns for testing
|
|
141
|
+
TEST_PEER_IDS = {
|
|
142
|
+
"alice": "QmAliceTest1234567890abcdefghijklmnopqrstuvwxyz",
|
|
143
|
+
"bob": "QmBobTest1234567890abcdefghijklmnopqrstuvwxyz",
|
|
144
|
+
"charlie": "QmCharlieTest1234567890abcdefghijklmnopqrstuvwxyz",
|
|
145
|
+
"bootnode": "QmBootnodeTest1234567890abcdefghijklmnopqrstuvwxyz",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_test_peer_id(name: str = "alice") -> str:
|
|
150
|
+
"""
|
|
151
|
+
Get a predefined test peer ID.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Name of test peer (alice, bob, charlie, bootnode)
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Test peer ID string
|
|
158
|
+
"""
|
|
159
|
+
return TEST_PEER_IDS.get(name, generate_test_peer_id(hash(name) % 1000))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Example usage and validation
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
# Test the utilities
|
|
165
|
+
test_peer = "QmTestPeerID1234567890abcdefghijklmnopqrstuvwxyz"
|
|
166
|
+
|
|
167
|
+
print("Testing peer ID utilities...")
|
|
168
|
+
|
|
169
|
+
# Test encoding
|
|
170
|
+
encoded = encode_peer_id(test_peer)
|
|
171
|
+
print(f"Original: {test_peer}")
|
|
172
|
+
print(f"Encoded: {encoded}")
|
|
173
|
+
|
|
174
|
+
# Test decoding
|
|
175
|
+
decoded = decode_peer_id(encoded)
|
|
176
|
+
print(f"Decoded: {decoded}")
|
|
177
|
+
|
|
178
|
+
# Test validation
|
|
179
|
+
is_valid = validate_peer_id_format(test_peer)
|
|
180
|
+
print(f"Valid: {is_valid}")
|
|
181
|
+
|
|
182
|
+
# Test generation
|
|
183
|
+
generated = generate_test_peer_id(42)
|
|
184
|
+
print(f"Generated: {generated}")
|
|
185
|
+
|
|
186
|
+
print("✅ All tests passed!")
|