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,856 @@
1
+ """
2
+ Subnet-related response models.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+
7
+ from pydantic import BaseModel, Field, field_validator
8
+ from scalecodec.base import RuntimeConfiguration, ScaleBytes
9
+ from substrateinterface.base import load_type_registry_preset
10
+
11
+ from src.htcli.models.enums.enum_types import KeyType, SubnetState
12
+
13
+ from .base import BaseResponse
14
+
15
+
16
+ class SubnetRegisterResponse(BaseResponse):
17
+ """Response model for subnet registration."""
18
+
19
+ subnet_id: Optional[int] = Field(None, description="Assigned subnet ID")
20
+ cost_paid: Optional[int] = Field(
21
+ None, description="Actual cost paid for registration"
22
+ )
23
+ owner: Optional[str] = Field(None, description="Subnet owner account")
24
+ name: Optional[str] = Field(None, description="Registered subnet name")
25
+
26
+
27
+ class SubnetData(BaseModel):
28
+ """Subnet data structure matching Rust SubnetData with validation."""
29
+
30
+ id: int = Field(..., description="Subnet ID", ge=0)
31
+ friendly_id: Optional[int] = Field(None, description="Subnet friendly ID", ge=0)
32
+ name: bytes = Field(..., description="Subnet name")
33
+ repo: bytes = Field(..., description="Repository URL")
34
+ description: bytes = Field(default=b"", description="Subnet description")
35
+ misc: bytes = Field(default=b"", description="Miscellaneous information")
36
+ state: SubnetState = Field(..., description="Subnet state")
37
+ start_epoch: int = Field(..., description="Start epoch", ge=0)
38
+
39
+ @field_validator("name", "repo", "description", "misc")
40
+ def validate_bytes_not_empty(cls, v):
41
+ """Ensure byte fields are not None."""
42
+ if v is None:
43
+ return b""
44
+ return v
45
+
46
+ @classmethod
47
+ def from_scale_bytes(cls, data: bytes) -> "SubnetData":
48
+ """Decode from SCALE bytes using scalecodec."""
49
+ if not ScaleBytes:
50
+ raise ImportError("scalecodec is required for decoding")
51
+
52
+ # Load type registry
53
+ type_registry = load_type_registry_preset("substrate-node-template")
54
+
55
+ # Create runtime configuration
56
+ runtime_config = RuntimeConfiguration()
57
+ runtime_config.update_type_registry(type_registry)
58
+
59
+ # Decode the data
60
+ obj = runtime_config.create_scale_object("SubnetData", ScaleBytes(data))
61
+ obj.decode()
62
+
63
+ _missing = object()
64
+ friendly_id = obj.get("friendly_id", _missing)
65
+ if friendly_id is _missing:
66
+ friendly_id = obj["id"]
67
+
68
+ return cls(
69
+ id=obj["id"],
70
+ friendly_id=friendly_id,
71
+ name=obj["name"],
72
+ repo=obj["repo"],
73
+ description=obj["description"],
74
+ misc=obj["misc"],
75
+ state=SubnetState(obj["state"].name),
76
+ start_epoch=obj["start_epoch"],
77
+ )
78
+
79
+
80
+ class SubnetInfo(BaseModel):
81
+ """Complete subnet information structure matching Rust SubnetInfo with validation."""
82
+
83
+ id: int = Field(..., description="Subnet ID", ge=0)
84
+ friendly_id: Optional[int] = Field(None, description="Subnet friendly ID", ge=0)
85
+ name: bytes = Field(..., description="Subnet name")
86
+ repo: bytes = Field(..., description="Repository URL")
87
+ description: bytes = Field(default=b"", description="Subnet description")
88
+ misc: bytes = Field(default=b"", description="Miscellaneous information")
89
+ state: SubnetState = Field(..., description="Subnet state")
90
+ start_epoch: int = Field(..., description="Start epoch", ge=0)
91
+ churn_limit: int = Field(default=0, description="Churn limit", ge=0)
92
+ churn_limit_multiplier: int = Field(
93
+ default=1, description="Churn limit multiplier", ge=0
94
+ )
95
+ min_stake: int = Field(default=0, description="Minimum stake balance", ge=0)
96
+ max_stake: int = Field(default=0, description="Maximum stake balance", ge=0)
97
+ queue_immunity_epochs: int = Field(
98
+ default=0, description="Queue immunity epochs", ge=0
99
+ )
100
+ target_node_registrations_per_epoch: int = Field(
101
+ default=0, description="Target node registrations per epoch", ge=0
102
+ )
103
+ node_registrations_this_epoch: int = Field(
104
+ default=0, description="Node registrations this epoch", ge=0
105
+ )
106
+ subnet_node_queue_epochs: int = Field(
107
+ default=0, description="Subnet node queue epochs", ge=0
108
+ )
109
+ idle_classification_epochs: int = Field(
110
+ default=0, description="Idle classification epochs", ge=0
111
+ )
112
+ included_classification_epochs: int = Field(
113
+ default=0, description="Included classification epochs", ge=0
114
+ )
115
+ delegate_stake_percentage: int = Field(
116
+ default=0, description="Delegate stake percentage (wei)", ge=0
117
+ )
118
+ last_delegate_stake_rewards_update: int = Field(
119
+ default=0, description="Last delegate stake rewards update epoch", ge=0
120
+ )
121
+ node_burn_rate_alpha: int = Field(
122
+ default=0, description="Node burn rate alpha (wei)", ge=0
123
+ )
124
+ current_node_burn_rate: int = Field(
125
+ default=0, description="Current node burn rate (wei)", ge=0
126
+ )
127
+ initial_coldkeys: Optional[dict[str, int]] = Field(
128
+ None,
129
+ description="Initial coldkeys and how many nodes each may register",
130
+ )
131
+ initial_coldkey_data: Optional[dict[str, int]] = Field(
132
+ None,
133
+ description="Initial coldkey data",
134
+ )
135
+ max_registered_nodes: int = Field(
136
+ default=0, description="Maximum registered nodes", ge=0
137
+ )
138
+ owner: Optional[str] = Field(None, description="Subnet owner")
139
+ pending_owner: Optional[str] = Field(None, description="Pending subnet owner")
140
+ registration_epoch: Optional[int] = Field(
141
+ None, description="Registration epoch", ge=0
142
+ )
143
+ prev_pause_epoch: int = Field(default=0, description="Previous pause epoch", ge=0)
144
+ key_types: set[KeyType] = Field(
145
+ default_factory=set, description="Supported key types"
146
+ )
147
+ slot_index: Optional[int] = Field(None, description="Slot index", ge=0)
148
+ slot_assignment: Optional[int] = Field(None, description="Slot assignment", ge=0)
149
+ subnet_node_min_weight_decrease_reputation_threshold: int = Field(
150
+ default=0,
151
+ description="Subnet node min weight decrease reputation threshold",
152
+ ge=0,
153
+ )
154
+ reputation: int = Field(default=0, description="Subnet reputation score", ge=0)
155
+ min_subnet_node_reputation: int = Field(
156
+ default=0, description="Minimum subnet node reputation", ge=0
157
+ )
158
+ absent_decrease_reputation_factor: int = Field(
159
+ default=0, description="Absent decrease reputation factor", ge=0
160
+ )
161
+ included_increase_reputation_factor: int = Field(
162
+ default=0, description="Included increase reputation factor", ge=0
163
+ )
164
+ below_min_weight_decrease_reputation_factor: int = Field(
165
+ default=0, description="Below minimum weight decrease factor", ge=0
166
+ )
167
+ non_attestor_decrease_reputation_factor: int = Field(
168
+ default=0, description="Non-attestor decrease reputation factor", ge=0
169
+ )
170
+ non_consensus_attestor_decrease_reputation_factor: int = Field(
171
+ default=0, description="Non-consensus attestor decrease factor", ge=0
172
+ )
173
+ validator_absent_subnet_node_reputation_factor: int = Field(
174
+ default=0, description="Validator absent reputation factor", ge=0
175
+ )
176
+ validator_non_consensus_subnet_node_reputation_factor: int = Field(
177
+ default=0, description="Validator non-consensus reputation factor", ge=0
178
+ )
179
+ bootnode_access: set[str] = Field(
180
+ default_factory=set, description="Bootnode access accounts"
181
+ )
182
+ bootnodes: set[bytes] = Field(default_factory=set, description="Bootnode addresses")
183
+ total_nodes: int = Field(default=0, description="Total number of nodes", ge=0)
184
+ total_active_nodes: int = Field(default=0, description="Total active nodes", ge=0)
185
+ total_electable_nodes: int = Field(
186
+ default=0, description="Total electable nodes", ge=0
187
+ )
188
+ current_min_delegate_stake: int = Field(
189
+ default=0, description="Current minimum delegate stake balance", ge=0
190
+ )
191
+ total_subnet_stake: int = Field(
192
+ default=0, description="Total subnet stake balance", ge=0
193
+ )
194
+ total_subnet_delegate_stake_shares: int = Field(
195
+ default=0, description="Total subnet delegate stake shares", ge=0
196
+ )
197
+ total_subnet_delegate_stake_balance: int = Field(
198
+ default=0, description="Total subnet delegate stake balance", ge=0
199
+ )
200
+
201
+ @field_validator("name", "repo", "description", "misc")
202
+ def validate_bytes_not_empty(cls, v):
203
+ """Ensure byte fields are not None."""
204
+ if v is None:
205
+ return b""
206
+ return v
207
+
208
+ @field_validator("max_stake")
209
+ def validate_max_stake(cls, v, info):
210
+ """Ensure max_stake >= min_stake."""
211
+ values = info.data if hasattr(info, "data") else {}
212
+ if "min_stake" in values and v < values["min_stake"]:
213
+ raise ValueError("max_stake must be greater than or equal to min_stake")
214
+ return v
215
+
216
+ @field_validator("total_active_nodes")
217
+ def validate_active_nodes(cls, v, info):
218
+ """Ensure total_active_nodes <= total_nodes."""
219
+ values = info.data if hasattr(info, "data") else {}
220
+ if "total_nodes" in values and v > values["total_nodes"]:
221
+ raise ValueError("total_active_nodes cannot be greater than total_nodes")
222
+ return v
223
+
224
+ @field_validator("total_electable_nodes")
225
+ def validate_electable_nodes(cls, v, info):
226
+ """Ensure total_electable_nodes <= total_active_nodes."""
227
+ values = info.data if hasattr(info, "data") else {}
228
+ if "total_active_nodes" in values and v > values["total_active_nodes"]:
229
+ raise ValueError(
230
+ "total_electable_nodes cannot be greater than total_active_nodes"
231
+ )
232
+ return v
233
+
234
+ @classmethod
235
+ def from_scale_bytes(cls, data: bytes) -> "SubnetInfo":
236
+ """Decode from SCALE bytes using scalecodec."""
237
+ if not ScaleBytes:
238
+ raise ImportError("scalecodec is required for decoding")
239
+
240
+ type_registry = load_type_registry_preset("substrate-node-template")
241
+ runtime_config = RuntimeConfiguration()
242
+ runtime_config.update_type_registry(type_registry)
243
+
244
+ obj = runtime_config.create_scale_object("SubnetInfo", ScaleBytes(data))
245
+ obj.decode()
246
+
247
+ def _format_address(value):
248
+ from src.htcli.utils.blockchain.formatting import to_checksum_address
249
+
250
+ if value is None:
251
+ return None
252
+ if isinstance(value, str):
253
+ raw_addr = value if value.startswith("0x") else f"0x{value}"
254
+ return to_checksum_address(raw_addr)
255
+ if isinstance(value, (bytes, bytearray)):
256
+ raw_addr = "0x" + value.hex()
257
+ return to_checksum_address(raw_addr)
258
+ if isinstance(value, list):
259
+ raw_addr = "0x" + bytes(value).hex()
260
+ return to_checksum_address(raw_addr)
261
+ return str(value)
262
+
263
+ def _parse_initial_coldkeys(raw):
264
+ if not raw:
265
+ return None
266
+ mapping: dict[str, int] = {}
267
+ for entry in raw:
268
+ if not isinstance(entry, (list, tuple)) or len(entry) != 2:
269
+ continue
270
+ addr = _format_address(entry[0])
271
+ max_regs = entry[1]
272
+ if addr is not None:
273
+ mapping[addr] = max_regs
274
+ return mapping if mapping else None
275
+
276
+ return cls(
277
+ id=obj["id"],
278
+ friendly_id=obj.get("friendly_id"),
279
+ name=obj["name"],
280
+ repo=obj["repo"],
281
+ description=obj["description"],
282
+ misc=obj["misc"],
283
+ state=SubnetState(obj["state"].name),
284
+ start_epoch=obj["start_epoch"],
285
+ churn_limit=obj["churn_limit"],
286
+ min_stake=obj["min_stake"],
287
+ max_stake=obj["max_stake"],
288
+ queue_immunity_epochs=obj["queue_immunity_epochs"],
289
+ target_node_registrations_per_epoch=obj[
290
+ "target_node_registrations_per_epoch"
291
+ ],
292
+ subnet_node_queue_epochs=obj["subnet_node_queue_epochs"],
293
+ idle_classification_epochs=obj["idle_classification_epochs"],
294
+ included_classification_epochs=obj["included_classification_epochs"],
295
+ delegate_stake_percentage=obj["delegate_stake_percentage"],
296
+ node_burn_rate_alpha=obj["node_burn_rate_alpha"],
297
+ initial_coldkeys=_parse_initial_coldkeys(obj.get("initial_coldkeys")),
298
+ max_registered_nodes=obj["max_registered_nodes"],
299
+ owner=_format_address(obj["owner"]),
300
+ pending_owner=_format_address(obj["pending_owner"]),
301
+ registration_epoch=obj["registration_epoch"],
302
+ key_types={KeyType(kt.name) for kt in obj["key_types"]},
303
+ slot_index=obj["slot_index"],
304
+ reputation=obj["reputation"],
305
+ min_subnet_node_reputation=obj["min_subnet_node_reputation"],
306
+ absent_decrease_reputation_factor=obj["absent_decrease_reputation_factor"],
307
+ included_increase_reputation_factor=obj[
308
+ "included_increase_reputation_factor"
309
+ ],
310
+ below_min_weight_decrease_reputation_factor=obj[
311
+ "below_min_weight_decrease_reputation_factor"
312
+ ],
313
+ non_attestor_decrease_reputation_factor=obj[
314
+ "non_attestor_decrease_reputation_factor"
315
+ ],
316
+ non_consensus_attestor_decrease_reputation_factor=obj[
317
+ "non_consensus_attestor_decrease_reputation_factor"
318
+ ],
319
+ validator_absent_subnet_node_reputation_factor=obj[
320
+ "validator_absent_subnet_node_reputation_factor"
321
+ ],
322
+ validator_non_consensus_subnet_node_reputation_factor=obj[
323
+ "validator_non_consensus_subnet_node_reputation_factor"
324
+ ],
325
+ bootnode_access={_format_address(addr) for addr in obj["bootnode_access"]},
326
+ bootnodes={bytes(bn) for bn in obj["bootnodes"]},
327
+ total_nodes=obj["total_nodes"],
328
+ total_active_nodes=obj["total_active_nodes"],
329
+ total_electable_nodes=obj["total_electable_nodes"],
330
+ current_min_delegate_stake=obj["current_min_delegate_stake"],
331
+ )
332
+
333
+
334
+ class SubnetNodeInfo(BaseModel):
335
+ """Subnet node information structure matching Rust SubnetNodeInfo with validation."""
336
+
337
+ subnet_id: int = Field(..., description="Subnet ID", ge=0)
338
+ subnet_node_id: int = Field(..., description="Subnet node ID", ge=0)
339
+ coldkey: str = Field(..., description="Coldkey account ID")
340
+ hotkey: str = Field(..., description="Hotkey account ID")
341
+ peer_id: bytes = Field(..., description="Peer ID")
342
+ bootnode_peer_id: bytes = Field(..., description="Bootnode peer ID")
343
+ client_peer_id: bytes = Field(..., description="Client peer ID")
344
+ bootnode: Optional[bytes] = Field(None, description="Bootnode address")
345
+ identity: Optional[dict[str, Any]] = Field(None, description="Identity data")
346
+ classification: dict[str, Any] = Field(
347
+ ..., description="Node classification (dict with node_class and start_epoch)"
348
+ )
349
+ delegate_reward_rate: int = Field(
350
+ default=0, description="Delegate reward rate", ge=0, le=1000000000000000000
351
+ )
352
+ last_delegate_reward_rate_update: int = Field(
353
+ default=0, description="Last delegate reward rate update", ge=0
354
+ )
355
+ unique: Optional[bytes] = Field(None, description="Unique parameter")
356
+ non_unique: Optional[bytes] = Field(None, description="Non-unique parameter")
357
+ stake_balance: int = Field(default=0, description="Stake balance", ge=0)
358
+ total_node_delegate_stake_shares: int = Field(
359
+ default=0, description="Total node delegate stake shares", ge=0
360
+ )
361
+ node_delegate_stake_balance: int = Field(
362
+ default=0, description="Node delegate stake balance", ge=0
363
+ )
364
+ coldkey_reputation: Optional[dict[str, Any]] = Field(
365
+ None, description="Reputation data for the coldkey"
366
+ )
367
+ subnet_node_reputation: int = Field(
368
+ default=0, description="Subnet node reputation score", ge=0
369
+ )
370
+ node_slot_index: Optional[int] = Field(None, description="Node slot index", ge=0)
371
+ consecutive_idle_epochs: int = Field(
372
+ default=0, description="Consecutive idle epochs", ge=0
373
+ )
374
+ consecutive_included_epochs: int = Field(
375
+ default=0, description="Consecutive included epochs", ge=0
376
+ )
377
+
378
+ @field_validator("peer_id", "bootnode_peer_id", "client_peer_id")
379
+ def validate_peer_ids_not_empty(cls, v):
380
+ """Allow empty peer IDs (blockchain can have empty peer IDs for some nodes)."""
381
+ return v if v is not None else b""
382
+
383
+ @field_validator("delegate_reward_rate")
384
+ def validate_delegate_reward_rate(cls, v):
385
+ """Ensure delegate reward rate is within valid range."""
386
+ if v < 0 or v > 1000000000000000000: # 100%
387
+ raise ValueError("Delegate reward rate must be between 0 and 100%")
388
+ return v
389
+
390
+ @classmethod
391
+ def from_scale_bytes(cls, data: bytes) -> "SubnetNodeInfo":
392
+ """Decode from SCALE bytes using scalecodec."""
393
+ if not ScaleBytes:
394
+ raise ImportError("scalecodec is required for decoding")
395
+
396
+ type_registry = load_type_registry_preset("substrate-node-template")
397
+ runtime_config = RuntimeConfiguration()
398
+ runtime_config.update_type_registry(type_registry)
399
+
400
+ obj = runtime_config.create_scale_object("SubnetNodeInfo", ScaleBytes(data))
401
+ obj.decode()
402
+
403
+ def _format_address(value: bytes) -> str:
404
+ from src.htcli.utils.blockchain.formatting import to_checksum_address
405
+
406
+ if isinstance(value, str):
407
+ raw_addr = value if value.startswith("0x") else f"0x{value}"
408
+ return to_checksum_address(raw_addr)
409
+ if isinstance(value, (bytes, bytearray)):
410
+ raw_addr = "0x" + value.hex()
411
+ return to_checksum_address(raw_addr)
412
+ raw_addr = "0x" + bytes(value).hex()
413
+ return to_checksum_address(raw_addr)
414
+
415
+ return cls(
416
+ subnet_id=obj["subnet_id"],
417
+ subnet_node_id=obj["subnet_node_id"],
418
+ coldkey=_format_address(obj["coldkey"]),
419
+ hotkey=_format_address(obj["hotkey"]),
420
+ peer_id=obj["peer_id"],
421
+ bootnode_peer_id=obj["bootnode_peer_id"],
422
+ client_peer_id=obj["client_peer_id"],
423
+ bootnode=bytes(obj["bootnode"]) if obj["bootnode"] else None,
424
+ identity=obj.get("identity"),
425
+ classification=obj["classification"],
426
+ delegate_reward_rate=obj["delegate_reward_rate"],
427
+ last_delegate_reward_rate_update=obj["last_delegate_reward_rate_update"],
428
+ unique=bytes(obj["unique"]) if obj["unique"] else None,
429
+ non_unique=bytes(obj["non_unique"]) if obj["non_unique"] else None,
430
+ stake_balance=obj["stake_balance"],
431
+ node_delegate_stake_balance=obj["node_delegate_stake_balance"],
432
+ coldkey_reputation=obj.get("coldkey_reputation"),
433
+ subnet_node_reputation=obj["subnet_node_reputation"],
434
+ )
435
+
436
+
437
+ # ============================================================================
438
+ # ============================================================================
439
+ # OVERWATCH NODE RESPONSE MODELS
440
+ # ============================================================================
441
+
442
+
443
+ class OverwatchNodeInfo(BaseModel):
444
+ """Overwatch node information structure matching Rust OverwatchNodeInfo with validation."""
445
+
446
+ overwatch_node_id: int = Field(..., description="Overwatch node ID", ge=0)
447
+ coldkey: str = Field(..., description="Coldkey account ID")
448
+ hotkey: str = Field(..., description="Hotkey account ID")
449
+ peer_ids: list[bytes] = Field(default_factory=list, description="Peer IDs")
450
+ penalties: int = Field(default=0, description="Penalties", ge=0)
451
+ reputation: Optional[dict[str, Any]] = Field(None, description="Reputation data")
452
+
453
+ @field_validator("peer_ids")
454
+ def validate_peer_ids(cls, v):
455
+ """Ensure peer IDs are valid."""
456
+ if v is None:
457
+ return []
458
+ return v
459
+
460
+ @classmethod
461
+ def from_scale_bytes(cls, data: bytes) -> "OverwatchNodeInfo":
462
+ """Decode from SCALE bytes using scalecodec."""
463
+ if not ScaleBytes:
464
+ raise ImportError("scalecodec is required for decoding")
465
+
466
+ # Load type registry
467
+ type_registry = load_type_registry_preset("substrate-node-template")
468
+
469
+ # Create runtime configuration
470
+ runtime_config = RuntimeConfiguration()
471
+ runtime_config.update_type_registry(type_registry)
472
+
473
+ # Decode the data
474
+ obj = runtime_config.create_scale_object("OverwatchNodeInfo", ScaleBytes(data))
475
+ obj.decode()
476
+
477
+ return cls(
478
+ overwatch_node_id=obj["overwatch_node_id"],
479
+ coldkey=obj["coldkey"],
480
+ hotkey=obj["hotkey"],
481
+ peer_ids=list(obj["peer_ids"]),
482
+ penalties=obj["penalties"],
483
+ reputation=obj["reputation"],
484
+ )
485
+
486
+
487
+ # ============================================================================
488
+ # IDENTITY RESPONSE MODELS
489
+ # ============================================================================
490
+
491
+
492
+ class IdentityInfo(BaseModel):
493
+ """Identity information structure with validation."""
494
+
495
+ coldkey: str = Field(..., description="Coldkey account ID")
496
+ hotkey: str = Field(..., description="Hotkey account ID")
497
+ name: bytes = Field(..., description="Identity name")
498
+ url: bytes = Field(default=b"", description="Identity URL")
499
+ image: bytes = Field(default=b"", description="Identity image URL")
500
+ description: bytes = Field(default=b"", description="Identity description")
501
+
502
+ @field_validator("name", "url", "image", "description")
503
+ def validate_bytes_not_empty(cls, v):
504
+ """Ensure byte fields are not None."""
505
+ if v is None:
506
+ return b""
507
+ return v
508
+
509
+ @classmethod
510
+ def from_scale_bytes(cls, data: bytes) -> "IdentityInfo":
511
+ """Decode from SCALE bytes using scalecodec."""
512
+ if not ScaleBytes:
513
+ raise ImportError("scalecodec is required for decoding")
514
+
515
+ # Load type registry
516
+ type_registry = load_type_registry_preset("substrate-node-template")
517
+
518
+ # Create runtime configuration
519
+ runtime_config = RuntimeConfiguration()
520
+ runtime_config.update_type_registry(type_registry)
521
+
522
+ # Decode the data
523
+ obj = runtime_config.create_scale_object("IdentityInfo", ScaleBytes(data))
524
+ obj.decode()
525
+
526
+ return cls(
527
+ coldkey=obj["coldkey"],
528
+ hotkey=obj["hotkey"],
529
+ name=obj["name"],
530
+ url=obj["url"],
531
+ image=obj["image"],
532
+ description=obj["description"],
533
+ )
534
+
535
+
536
+ # ============================================================================
537
+ # STAKE RESPONSE MODELS
538
+ # ============================================================================
539
+
540
+
541
+ class StakeInfo(BaseModel):
542
+ """Stake information structure with validation."""
543
+
544
+ account_id: str = Field(..., description="Account ID")
545
+ subnet_id: int = Field(..., description="Subnet ID", ge=0)
546
+ stake_amount: int = Field(..., description="Stake amount", ge=0)
547
+ delegate_stake: int = Field(default=0, description="Delegate stake amount", ge=0)
548
+ unbonding: int = Field(default=0, description="Unbonding amount", ge=0)
549
+
550
+ @classmethod
551
+ def from_scale_bytes(cls, data: bytes) -> "StakeInfo":
552
+ """Decode from SCALE bytes using scalecodec."""
553
+ if not ScaleBytes:
554
+ raise ImportError("scalecodec is required for decoding")
555
+
556
+ # Load type registry
557
+ type_registry = load_type_registry_preset("substrate-node-template")
558
+
559
+ # Create runtime configuration
560
+ runtime_config = RuntimeConfiguration()
561
+ runtime_config.update_type_registry(type_registry)
562
+
563
+ # Decode the data
564
+ obj = runtime_config.create_scale_object("StakeInfo", ScaleBytes(data))
565
+ obj.decode()
566
+
567
+ return cls(
568
+ account_id=obj["account_id"],
569
+ subnet_id=obj["subnet_id"],
570
+ stake_amount=obj["stake_amount"],
571
+ delegate_stake=obj["delegate_stake"],
572
+ unbonding=obj["unbonding"],
573
+ )
574
+
575
+
576
+ # ============================================================================
577
+ # NETWORK RESPONSE MODELS
578
+ # ============================================================================
579
+
580
+
581
+ class NetworkInfo(BaseModel):
582
+ """Network information structure with validation."""
583
+
584
+ total_subnets: int = Field(..., description="Total number of subnets", ge=0)
585
+ total_nodes: int = Field(..., description="Total number of nodes", ge=0)
586
+ total_overwatch_nodes: int = Field(
587
+ ..., description="Total number of overwatch nodes", ge=0
588
+ )
589
+ current_epoch: int = Field(..., description="Current epoch", ge=0)
590
+ registration_cost: int = Field(..., description="Current registration cost", ge=0)
591
+ network_state: str = Field(..., description="Network state")
592
+
593
+ @field_validator("network_state")
594
+ def validate_network_state(cls, v):
595
+ """Ensure network state is valid."""
596
+ valid_states = ["Active", "Paused"]
597
+ if v not in valid_states:
598
+ raise ValueError(f"Network state must be one of {valid_states}")
599
+ return v
600
+
601
+ @classmethod
602
+ def from_scale_bytes(cls, data: bytes) -> "NetworkInfo":
603
+ """Decode from SCALE bytes using scalecodec."""
604
+ if not ScaleBytes:
605
+ raise ImportError("scalecodec is required for decoding")
606
+
607
+ # Load type registry
608
+ type_registry = load_type_registry_preset("substrate-node-template")
609
+
610
+ # Create runtime configuration
611
+ runtime_config = RuntimeConfiguration()
612
+ runtime_config.update_type_registry(type_registry)
613
+
614
+ # Decode the data
615
+ obj = runtime_config.create_scale_object("NetworkInfo", ScaleBytes(data))
616
+ obj.decode()
617
+
618
+ return cls(
619
+ total_subnets=obj["total_subnets"],
620
+ total_nodes=obj["total_nodes"],
621
+ total_overwatch_nodes=obj["total_overwatch_nodes"],
622
+ current_epoch=obj["current_epoch"],
623
+ registration_cost=obj["registration_cost"],
624
+ network_state=obj["network_state"],
625
+ )
626
+
627
+
628
+ class SubnetNodeStakeInfo(BaseModel):
629
+ """Response model for subnet node stake information."""
630
+
631
+ subnet_id: Optional[int] = Field(None, description="Subnet ID")
632
+ subnet_node_id: Optional[int] = Field(None, description="Subnet node ID")
633
+ hotkey: str = Field(..., description="Hotkey address")
634
+ balance: int = Field(..., description="Stake balance")
635
+
636
+
637
+ class NodeStakeInfo(BaseModel):
638
+ """Response model for validator node stake information."""
639
+
640
+ subnet_id: Optional[int] = Field(None, description="Subnet ID")
641
+ subnet_node_id: Optional[int] = Field(None, description="Subnet node ID")
642
+ balance: int = Field(..., description="Stake balance")
643
+
644
+
645
+ class DelegateStakeInfo(BaseModel):
646
+ """Response model for delegate stake information."""
647
+
648
+ subnet_id: int = Field(..., description="Subnet ID")
649
+ shares: int = Field(..., description="Delegate stake shares")
650
+ balance: int = Field(..., description="Delegate stake balance")
651
+
652
+
653
+ class NodeDelegateStakeInfo(BaseModel):
654
+ """Response model for node delegate stake information."""
655
+
656
+ subnet_id: int = Field(..., description="Subnet ID")
657
+ subnet_node_id: int = Field(..., description="Subnet node ID")
658
+ shares: int = Field(..., description="Node delegate stake shares")
659
+ balance: int = Field(..., description="Node delegate stake balance")
660
+
661
+
662
+ class AllSubnetBootnodes(BaseModel):
663
+ """Response model for all subnet bootnodes."""
664
+
665
+ bootnodes: list[str] = Field(..., description="Official bootnodes")
666
+ node_bootnodes: list[str] = Field(..., description="Node bootnodes")
667
+
668
+
669
+ class OverwatchCommitInfo(BaseModel):
670
+ """Response model for overwatch commit information."""
671
+
672
+ subnet_id: int = Field(..., description="Subnet ID")
673
+ commit_hash: str = Field(..., description="Commit hash")
674
+
675
+
676
+ class OverwatchRevealInfo(BaseModel):
677
+ """Response model for overwatch reveal information."""
678
+
679
+ subnet_id: int = Field(..., description="Subnet ID")
680
+ reveal_value: int = Field(..., description="Reveal value")
681
+
682
+
683
+ class SubnetInfoResponse(BaseModel):
684
+ """Response model for subnet information."""
685
+
686
+ success: bool = Field(..., description="Operation success status")
687
+ message: str = Field(..., description="Response message")
688
+ data: Optional[Any] = Field(None, description="Subnet information")
689
+
690
+
691
+ class SubnetNodeInfoResponse(BaseModel):
692
+ """Response wrapper for subnet node info RPC calls."""
693
+
694
+ success: bool = Field(..., description="Operation success status")
695
+ message: str = Field(..., description="Response message")
696
+ data: Optional[Any] = Field(None, description="Subnet node information")
697
+
698
+
699
+ class ColdkeySubnetNodesResponse(BaseModel):
700
+ """Response wrapper for coldkey subnet nodes RPC calls."""
701
+
702
+ success: bool = Field(..., description="Operation success status")
703
+ message: str = Field(..., description="Response message")
704
+ data: Optional[Any] = Field(None, description="Coldkey subnet nodes information")
705
+
706
+
707
+ class ValidatorSubnetNodesResponse(BaseModel):
708
+ """Response wrapper for validator subnet nodes RPC calls."""
709
+
710
+ success: bool = Field(..., description="Operation success status")
711
+ message: str = Field(..., description="Response message")
712
+ data: Optional[Any] = Field(None, description="Validator subnet nodes information")
713
+
714
+
715
+ class ColdkeyStakesResponse(BaseModel):
716
+ """Response wrapper for coldkey stakes RPC calls."""
717
+
718
+ success: bool = Field(..., description="Operation success status")
719
+ message: str = Field(..., description="Response message")
720
+ data: Optional[Any] = Field(None, description="Coldkey stakes information")
721
+
722
+
723
+ class ValidatorStakesResponse(BaseModel):
724
+ """Response wrapper for validator stakes RPC calls."""
725
+
726
+ success: bool = Field(..., description="Operation success status")
727
+ message: str = Field(..., description="Response message")
728
+ data: Optional[Any] = Field(None, description="Validator stakes information")
729
+
730
+
731
+ class DelegateStakesResponse(BaseModel):
732
+ """Response wrapper for delegate stakes RPC calls."""
733
+
734
+ success: bool = Field(..., description="Operation success status")
735
+ message: str = Field(..., description="Response message")
736
+ data: Optional[Any] = Field(None, description="Delegate stakes information")
737
+
738
+
739
+ class NodeDelegateStakesResponse(BaseModel):
740
+ """Response wrapper for node delegate stakes RPC calls."""
741
+
742
+ success: bool = Field(..., description="Operation success status")
743
+ message: str = Field(..., description="Response message")
744
+ data: Optional[Any] = Field(None, description="Node delegate stakes information")
745
+
746
+
747
+ class BootnodesResponse(BaseModel):
748
+ """Response wrapper for bootnodes RPC calls."""
749
+
750
+ success: bool = Field(..., description="Operation success status")
751
+ message: str = Field(..., description="Response message")
752
+ data: Optional[Any] = Field(None, description="Bootnodes information")
753
+
754
+
755
+ class ProofOfStakeResponse(BaseModel):
756
+ """Response wrapper for proof of stake RPC calls."""
757
+
758
+ success: bool = Field(..., description="Operation success status")
759
+ message: str = Field(..., description="Response message")
760
+ data: Optional[Any] = Field(None, description="Proof of stake information")
761
+
762
+
763
+ class OverwatchCommitsResponse(BaseModel):
764
+ """Response wrapper for overwatch commits RPC calls."""
765
+
766
+ success: bool = Field(..., description="Operation success status")
767
+ message: str = Field(..., description="Response message")
768
+ data: Optional[Any] = Field(None, description="Overwatch commits information")
769
+
770
+
771
+ class OverwatchRevealsResponse(BaseModel):
772
+ """Response wrapper for overwatch reveals RPC calls."""
773
+
774
+ success: bool = Field(..., description="Operation success status")
775
+ message: str = Field(..., description="Response message")
776
+ data: Optional[Any] = Field(None, description="Overwatch reveals information")
777
+
778
+
779
+ class SubnetPauseResponse(BaseModel):
780
+ """Response model for subnet pause operations."""
781
+
782
+ subnet_id: int = Field(..., description="Subnet ID")
783
+ success: bool = Field(..., description="Pause operation success")
784
+ message: str = Field(..., description="Operation message")
785
+ transaction_hash: Optional[str] = Field(None, description="Transaction hash")
786
+ block_hash: Optional[str] = Field(
787
+ None, description="Block hash where transaction was included"
788
+ )
789
+ block_number: Optional[int] = Field(
790
+ None, description="Block number where transaction was included"
791
+ )
792
+
793
+
794
+ class SubnetUnpauseResponse(BaseModel):
795
+ """Response model for subnet unpause operations."""
796
+
797
+ subnet_id: int = Field(..., description="Subnet ID")
798
+ success: bool = Field(..., description="Unpause operation success")
799
+ message: str = Field(..., description="Operation message")
800
+ transaction_hash: Optional[str] = Field(None, description="Transaction hash")
801
+ block_hash: Optional[str] = Field(
802
+ None, description="Block hash where transaction was included"
803
+ )
804
+ block_number: Optional[int] = Field(
805
+ None, description="Block number where transaction was included"
806
+ )
807
+
808
+
809
+ class SubnetOwnershipTransferResponse(BaseModel):
810
+ """Response model for subnet ownership transfer."""
811
+
812
+ subnet_id: int = Field(..., description="Subnet ID")
813
+ new_owner: str = Field(..., description="New owner address")
814
+ success: bool = Field(..., description="Transfer success status")
815
+ message: str = Field(..., description="Transfer message")
816
+ transaction_hash: Optional[str] = Field(None, description="Transaction hash")
817
+ block_hash: Optional[str] = Field(
818
+ None, description="Block hash where transaction was included"
819
+ )
820
+ block_number: Optional[int] = Field(
821
+ None, description="Block number where transaction was included"
822
+ )
823
+
824
+
825
+ class SubnetOwnerUpdateResponse(BaseModel):
826
+ """Response model for subnet owner updates."""
827
+
828
+ subnet_id: int = Field(..., description="Subnet ID")
829
+ old_owner: str = Field(..., description="Previous owner address")
830
+ new_owner: str = Field(..., description="New owner address")
831
+ success: bool = Field(..., description="Update success status")
832
+ message: str = Field(..., description="Update message")
833
+ transaction_hash: Optional[str] = Field(None, description="Transaction hash")
834
+ block_hash: Optional[str] = Field(
835
+ None, description="Block hash where transaction was included"
836
+ )
837
+ block_number: Optional[int] = Field(
838
+ None, description="Block number where transaction was included"
839
+ )
840
+
841
+
842
+ class DelegateStakeIncreaseResponse(BaseModel):
843
+ """Response model for delegate stake increase."""
844
+
845
+ subnet_id: int = Field(..., description="Subnet ID")
846
+ node_hotkey: str = Field(..., description="Node hotkey")
847
+ stake_amount: str = Field(..., description="Stake amount")
848
+ success: bool = Field(..., description="Stake increase success")
849
+ message: str = Field(..., description="Operation message")
850
+ transaction_hash: Optional[str] = Field(None, description="Transaction hash")
851
+ block_hash: Optional[str] = Field(
852
+ None, description="Block hash where transaction was included"
853
+ )
854
+ block_number: Optional[int] = Field(
855
+ None, description="Block number where transaction was included"
856
+ )