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,20 @@
1
+ """
2
+ RPC module for read-only blockchain queries.
3
+ Contains clients for querying blockchain state without making changes.
4
+ """
5
+
6
+ from .chain import ChainRpcClient
7
+ from .node import SubnetNodeRpcClient
8
+ from .overwatch import OverwatchRpcClient
9
+ from .staking import StakingRpcClient
10
+ from .subnet import SubnetRpcClient
11
+ from .wallet import WalletRpcClient
12
+
13
+ __all__ = [
14
+ "ChainRpcClient",
15
+ "StakingRpcClient",
16
+ "SubnetRpcClient",
17
+ "WalletRpcClient",
18
+ "SubnetNodeRpcClient",
19
+ "OverwatchRpcClient",
20
+ ]
@@ -0,0 +1,568 @@
1
+ """
2
+ RPC client for general chain queries.
3
+ Handles network peers, block info, chain head, and runtime version queries.
4
+ """
5
+
6
+ import math
7
+ import time
8
+ from typing import Any, Optional
9
+
10
+ from substrateinterface import SubstrateInterface
11
+
12
+ from ...utils.logging import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class ChainRpcClient:
18
+ """RPC client for general chain read-only operations."""
19
+
20
+ def __init__(self, substrate: Optional[SubstrateInterface] = None):
21
+ """Initialize the chain RPC client."""
22
+ self.substrate = substrate
23
+
24
+ def get_block_number(self, block_hash: Optional[str] = None) -> Optional[int]:
25
+ """
26
+ Get the block number for the supplied hash (defaults to latest).
27
+
28
+ Args:
29
+ block_hash: Optional block hash to resolve. When omitted, the latest block
30
+ hash is fetched first (matching mesh-template chain_functions).
31
+
32
+ Returns:
33
+ Current block number or None if error
34
+ """
35
+ try:
36
+ if not self.substrate:
37
+ raise Exception("Not connected to blockchain")
38
+
39
+ # If we already have a block hash, resolve it directly
40
+ if block_hash:
41
+ return self.substrate.get_block_number(block_hash=block_hash)
42
+
43
+ # Otherwise, fetch the latest block hash and resolve that
44
+ latest_hash = self.substrate.get_block_hash()
45
+ if latest_hash:
46
+ return self.substrate.get_block_number(block_hash=latest_hash)
47
+
48
+ # Fallback to querying the latest header (should rarely be needed)
49
+ header = self.substrate.rpc_request("chain_getHeader", [None])
50
+ if header and "result" in header and header["result"].get("number"):
51
+ return int(header["result"]["number"], 16)
52
+
53
+ return None
54
+ except Exception as e:
55
+ logger.error(f"Failed to get block number: {str(e)}")
56
+ return None
57
+
58
+ def get_current_epoch(self) -> Optional[int]:
59
+ """
60
+ Get current epoch number.
61
+
62
+ Returns:
63
+ Current epoch or None if error
64
+ """
65
+ try:
66
+ if not self.substrate:
67
+ raise Exception("Not connected to blockchain")
68
+
69
+ # Get current block
70
+ block_num = self.get_block_number()
71
+ if block_num is None:
72
+ return None
73
+
74
+ # Get epoch length from runtime constant
75
+ epoch_length = self.substrate.get_constant("Network", "EpochLength")
76
+ if epoch_length:
77
+ return block_num // epoch_length.value
78
+ return None
79
+ except Exception as e:
80
+ logger.error(f"Failed to get current epoch: {str(e)}")
81
+ return None
82
+
83
+ def get_account_balance(self, address: str) -> Optional[int]:
84
+ """
85
+ Get account balance in wei.
86
+
87
+ Args:
88
+ address: Account address
89
+
90
+ Returns:
91
+ Balance in wei or None if error
92
+ """
93
+ try:
94
+ if not self.substrate:
95
+ raise Exception("Not connected to blockchain")
96
+
97
+ # Query account info
98
+ result = self.substrate.query("System", "Account", [address])
99
+ if result and hasattr(result, "value") and result.value:
100
+ account_info = result.value
101
+ # Get free balance
102
+ if isinstance(account_info, dict) and "data" in account_info:
103
+ return account_info["data"].get("free", 0)
104
+ return 0
105
+ except Exception as e:
106
+ logger.error(f"Failed to get account balance: {str(e)}")
107
+ return None
108
+
109
+ def get_peers(self) -> dict[str, Any]:
110
+ """
111
+ Get network peers using RPC call.
112
+
113
+ Returns:
114
+ dictionary with peers information
115
+ """
116
+ try:
117
+ if not self.substrate:
118
+ raise Exception("Not connected to blockchain")
119
+
120
+ peers = self.substrate.rpc_request("system_peers", [])
121
+
122
+ return {
123
+ "success": True,
124
+ "message": "Peers retrieved successfully",
125
+ "data": peers.get("result", []) if peers else [],
126
+ }
127
+ except Exception as e:
128
+ logger.error(f"Failed to get peers: {str(e)}")
129
+ return {
130
+ "success": False,
131
+ "message": f"Failed to get peers: {str(e)}",
132
+ "data": [],
133
+ }
134
+
135
+ def get_block_info(self, block_number: Optional[int] = None) -> dict[str, Any]:
136
+ """
137
+ Get block information using RPC calls.
138
+
139
+ Args:
140
+ block_number: Optional block number to query (defaults to latest)
141
+
142
+ Returns:
143
+ dictionary with block information
144
+ """
145
+ try:
146
+ if not self.substrate:
147
+ raise Exception("Not connected to blockchain")
148
+
149
+ # Get block hash
150
+ if block_number is not None:
151
+ block_hash = self.substrate.get_block_hash(block_number)
152
+ else:
153
+ block_hash = self.substrate.get_chain_head()
154
+
155
+ # Get block header and data
156
+ block_header = self.substrate.get_block_header(block_hash)
157
+ block = self.substrate.get_block(block_hash)
158
+
159
+ return {
160
+ "success": True,
161
+ "message": "Block info retrieved successfully",
162
+ "data": {
163
+ "hash": str(block_hash),
164
+ "number": block_header.get("number", 0) if block_header else 0,
165
+ "parent_hash": (
166
+ str(block_header.get("parentHash", "")) if block_header else ""
167
+ ),
168
+ "state_root": (
169
+ str(block_header.get("stateRoot", "")) if block_header else ""
170
+ ),
171
+ "extrinsics_root": (
172
+ str(block_header.get("extrinsicsRoot", ""))
173
+ if block_header
174
+ else ""
175
+ ),
176
+ "digest": block_header.get("digest", {}) if block_header else {},
177
+ "extrinsics_count": (
178
+ len(block.get("extrinsics", [])) if block else 0
179
+ ),
180
+ },
181
+ }
182
+ except Exception as e:
183
+ logger.error(f"Failed to get block info: {str(e)}")
184
+ return {
185
+ "success": False,
186
+ "message": f"Failed to get block info: {str(e)}",
187
+ "data": {},
188
+ }
189
+
190
+ def get_chain_head(self) -> dict[str, Any]:
191
+ """
192
+ Get the current chain head.
193
+
194
+ Returns:
195
+ dictionary with chain head information
196
+ """
197
+ try:
198
+ if not self.substrate:
199
+ raise Exception("Not connected to blockchain")
200
+
201
+ head = self.substrate.get_chain_head()
202
+ return {
203
+ "success": True,
204
+ "message": "Chain head retrieved successfully",
205
+ "data": {"head": str(head)},
206
+ }
207
+ except Exception as e:
208
+ logger.error(f"Failed to get chain head: {str(e)}")
209
+ return {
210
+ "success": False,
211
+ "message": f"Failed to get chain head: {str(e)}",
212
+ "data": {"head": None},
213
+ }
214
+
215
+ def get_runtime_version(self) -> dict[str, Any]:
216
+ """
217
+ Get the runtime version.
218
+
219
+ Returns:
220
+ dictionary with runtime version information
221
+ """
222
+ try:
223
+ if not self.substrate:
224
+ raise Exception("Not connected to blockchain")
225
+
226
+ runtime_version = self.substrate.get_runtime_version()
227
+ return {
228
+ "success": True,
229
+ "message": "Runtime version retrieved successfully",
230
+ "data": runtime_version,
231
+ }
232
+ except Exception as e:
233
+ logger.error(f"Failed to get runtime version: {str(e)}")
234
+ return {
235
+ "success": False,
236
+ "message": f"Failed to get runtime version: {str(e)}",
237
+ "data": {},
238
+ }
239
+
240
+ def get_network_properties(self) -> dict[str, Any]:
241
+ """
242
+ Get network properties using RPC call.
243
+
244
+ Returns:
245
+ dictionary with network properties
246
+ """
247
+ try:
248
+ if not self.substrate:
249
+ raise Exception("Not connected to blockchain")
250
+
251
+ properties = self.substrate.rpc_request("system_properties", [])
252
+
253
+ return {
254
+ "success": True,
255
+ "message": "Network properties retrieved successfully",
256
+ "data": properties.get("result", {}) if properties else {},
257
+ }
258
+ except Exception as e:
259
+ logger.error(f"Failed to get network properties: {str(e)}")
260
+ return {
261
+ "success": False,
262
+ "message": f"Failed to get network properties: {str(e)}",
263
+ "data": {},
264
+ }
265
+
266
+ def get_chain_spec(self) -> dict[str, Any]:
267
+ """
268
+ Get chain specification using RPC call.
269
+
270
+ Returns:
271
+ dictionary with chain specification
272
+ """
273
+ try:
274
+ if not self.substrate:
275
+ raise Exception("Not connected to blockchain")
276
+
277
+ chain_spec = self.substrate.rpc_request("system_chain", [])
278
+
279
+ return {
280
+ "success": True,
281
+ "message": "Chain spec retrieved successfully",
282
+ "data": {
283
+ "chain": chain_spec.get("result", "") if chain_spec else "",
284
+ },
285
+ }
286
+ except Exception as e:
287
+ logger.error(f"Failed to get chain spec: {str(e)}")
288
+ return {
289
+ "success": False,
290
+ "message": f"Failed to get chain spec: {str(e)}",
291
+ "data": {"chain": ""},
292
+ }
293
+
294
+ def get_health(self) -> dict[str, Any]:
295
+ """
296
+ Get network health status using RPC call.
297
+
298
+ Returns:
299
+ dictionary with health information
300
+ """
301
+ try:
302
+ if not self.substrate:
303
+ raise Exception("Not connected to blockchain")
304
+
305
+ health = self.substrate.rpc_request("system_health", [])
306
+
307
+ return {
308
+ "success": True,
309
+ "message": "Health status retrieved successfully",
310
+ "data": health.get("result", {}) if health else {},
311
+ }
312
+ except Exception as e:
313
+ logger.error(f"Failed to get health status: {str(e)}")
314
+ return {
315
+ "success": False,
316
+ "message": f"Failed to get health status: {str(e)}",
317
+ "data": {},
318
+ }
319
+
320
+ def ping(self) -> dict[str, Any]:
321
+ """
322
+ Ping the connected node with a lightweight health RPC.
323
+
324
+ Returns:
325
+ dictionary with latency and health details
326
+ """
327
+ start_time = time.perf_counter()
328
+ try:
329
+ if not self.substrate:
330
+ raise Exception("Not connected to blockchain")
331
+
332
+ health_response = self.substrate.rpc_request("system_health", [])
333
+ latency_ms = (time.perf_counter() - start_time) * 1000
334
+ health = health_response.get("result", {}) if health_response else {}
335
+
336
+ return {
337
+ "success": True,
338
+ "message": "Node responded successfully",
339
+ "data": {
340
+ "latency_ms": latency_ms,
341
+ "peers": health.get("peers"),
342
+ "is_syncing": health.get("isSyncing"),
343
+ "should_have_peers": health.get("shouldHavePeers"),
344
+ },
345
+ }
346
+ except Exception as e:
347
+ latency_ms = (time.perf_counter() - start_time) * 1000
348
+ logger.error(f"Failed to ping node: {str(e)}")
349
+ return {
350
+ "success": False,
351
+ "message": f"Failed to ping node: {str(e)}",
352
+ "data": {"latency_ms": latency_ms},
353
+ }
354
+
355
+ def get_sync_state(self) -> dict[str, Any]:
356
+ """
357
+ Get synchronization state using RPC call.
358
+
359
+ Returns:
360
+ dictionary with sync state information
361
+ """
362
+ try:
363
+ if not self.substrate:
364
+ raise Exception("Not connected to blockchain")
365
+
366
+ sync_state = self.substrate.rpc_request("system_syncState", [])
367
+
368
+ return {
369
+ "success": True,
370
+ "message": "Sync state retrieved successfully",
371
+ "data": sync_state.get("result", {}) if sync_state else {},
372
+ }
373
+ except Exception as e:
374
+ logger.error(f"Failed to get sync state: {str(e)}")
375
+ return {
376
+ "success": False,
377
+ "message": f"Failed to get sync state: {str(e)}",
378
+ "data": {},
379
+ }
380
+
381
+ def _percent_mul(self, x: int, y: int) -> int:
382
+ """
383
+ Multiply x by percentage y (using 1e18 precision).
384
+
385
+ Equivalent to Rust: x * y / PERCENTAGE_FACTOR
386
+ """
387
+ PERCENTAGE_FACTOR = 1_000_000_000_000_000_000 # 1e18
388
+
389
+ if x == 0 or y == 0:
390
+ return 0
391
+
392
+ # Use integer arithmetic to avoid precision loss
393
+ result = (x * y) // PERCENTAGE_FACTOR
394
+ return min(result, 2**127 - 1) # Clamp to safe u128 range
395
+
396
+ def _percent_div(self, x: int, y: int) -> int:
397
+ """
398
+ Divide x by y, returning result as percentage (using 1e18 precision).
399
+
400
+ Equivalent to Rust: x * PERCENTAGE_FACTOR / y
401
+ """
402
+ PERCENTAGE_FACTOR = 1_000_000_000_000_000_000 # 1e18
403
+
404
+ if x == 0 or y == 0:
405
+ return 0
406
+
407
+ # Use integer arithmetic to avoid precision loss
408
+ result = (x * PERCENTAGE_FACTOR) // y
409
+ return min(result, 2**127 - 1) # Clamp to safe u128 range
410
+
411
+ def _get_percent_as_f64(self, v: int) -> float:
412
+ """
413
+ Convert percentage value (1e18 = 100%) to f64.
414
+
415
+ Equivalent to Rust: v as f64 / PERCENTAGE_FACTOR
416
+ """
417
+ PERCENTAGE_FACTOR = 1_000_000_000_000_000_000 # 1e18
418
+ return v / PERCENTAGE_FACTOR
419
+
420
+ def get_subnet_registration_cost(self) -> Optional[int]:
421
+ """
422
+ Get the current subnet registration cost in wei.
423
+
424
+ The cost dynamically decreases over time using exponential decay
425
+ from the last registration until it reaches the minimum cost.
426
+
427
+ This queries storage values and calculates the current cost based on:
428
+ - Last registration cost
429
+ - Minimum cost
430
+ - Decay period and rate
431
+ - Blocks since last registration
432
+ - Alpha parameter for exponential decay
433
+
434
+ Returns:
435
+ Current registration cost in wei (1e18 = 1 TENSOR) or None if error
436
+
437
+ Example:
438
+ >>> chain_rpc = ChainRpcClient(substrate)
439
+ >>> cost = chain_rpc.get_subnet_registration_cost()
440
+ >>> if cost:
441
+ >>> print(f"Current cost: {cost / 1e18:.2f} TENSOR")
442
+ """
443
+ try:
444
+ if not self.substrate:
445
+ raise Exception("Not connected to blockchain")
446
+
447
+ # Query storage values with detailed error tracking
448
+ logger.debug("Querying LastRegistrationCost...")
449
+ last_cost_result = self.substrate.query(
450
+ "Network", "LastRegistrationCost", []
451
+ )
452
+ logger.debug(f"LastRegistrationCost result: {last_cost_result}")
453
+
454
+ logger.debug("Querying MinRegistrationCost...")
455
+ min_cost_result = self.substrate.query("Network", "MinRegistrationCost", [])
456
+ logger.debug(f"MinRegistrationCost result: {min_cost_result}")
457
+
458
+ logger.debug("Querying LastSubnetRegistrationBlock...")
459
+ last_block_result = self.substrate.query(
460
+ "Network", "LastSubnetRegistrationBlock", []
461
+ )
462
+ logger.debug(f"LastSubnetRegistrationBlock result: {last_block_result}")
463
+
464
+ logger.debug("Querying RegistrationCostDecayBlocks...")
465
+ decay_blocks_result = self.substrate.query(
466
+ "Network", "RegistrationCostDecayBlocks", []
467
+ )
468
+ logger.debug(f"RegistrationCostDecayBlocks result: {decay_blocks_result}")
469
+
470
+ logger.debug("Querying RegistrationCostAlpha...")
471
+ alpha_result = self.substrate.query("Network", "RegistrationCostAlpha", [])
472
+ logger.debug(f"RegistrationCostAlpha result: {alpha_result}")
473
+
474
+ # Check which values are missing
475
+ missing = []
476
+ if not last_cost_result:
477
+ missing.append("LastRegistrationCost")
478
+ if not min_cost_result:
479
+ missing.append("MinRegistrationCost")
480
+ if not last_block_result:
481
+ missing.append("LastSubnetRegistrationBlock")
482
+ if not decay_blocks_result:
483
+ missing.append("RegistrationCostDecayBlocks")
484
+ if not alpha_result:
485
+ missing.append("RegistrationCostAlpha")
486
+
487
+ if missing:
488
+ logger.error(f"Missing storage values: {', '.join(missing)}")
489
+ logger.error("These storage items may not exist in the Network pallet")
490
+ return None
491
+
492
+ last_cost = (
493
+ last_cost_result.value
494
+ if hasattr(last_cost_result, "value")
495
+ else last_cost_result
496
+ )
497
+ min_cost = (
498
+ min_cost_result.value
499
+ if hasattr(min_cost_result, "value")
500
+ else min_cost_result
501
+ )
502
+ last_block = (
503
+ last_block_result.value
504
+ if hasattr(last_block_result, "value")
505
+ else last_block_result
506
+ )
507
+ decay_blocks = (
508
+ decay_blocks_result.value
509
+ if hasattr(decay_blocks_result, "value")
510
+ else decay_blocks_result
511
+ )
512
+ alpha = (
513
+ alpha_result.value if hasattr(alpha_result, "value") else alpha_result
514
+ )
515
+
516
+ # Get current block
517
+ current_block = self.get_block_number()
518
+ if current_block is None:
519
+ return None
520
+
521
+ # Calculate decayed cost using exponential decay (matching blockchain)
522
+ delta_blocks = current_block - last_block
523
+
524
+ # If already at min or no decay period
525
+ if decay_blocks == 0 or last_cost <= min_cost:
526
+ cost = max(last_cost, min_cost)
527
+ elif delta_blocks >= decay_blocks:
528
+ # Fully decayed: exactly min price
529
+ cost = min_cost
530
+ else:
531
+ # Exponential decay calculation (matching blockchain implementation)
532
+ diff = last_cost - min_cost
533
+
534
+ # Calculate remaining fraction as percentage: (decay_blocks - delta_blocks) / decay_blocks
535
+ remaining_blocks = decay_blocks - delta_blocks
536
+ remaining_frac = self._percent_div(remaining_blocks, decay_blocks)
537
+
538
+ # Apply concave exponential: remaining_frac ^ alpha
539
+ # Convert to f64 for pow calculation
540
+ remaining_frac_f64 = self._get_percent_as_f64(remaining_frac)
541
+ alpha_f64 = self._get_percent_as_f64(alpha)
542
+
543
+ # Calculate concave factor using pow
544
+ concave_factor = math.pow(remaining_frac_f64, alpha_f64)
545
+
546
+ # Calculate addend: diff * concave_factor
547
+ # Prevent overflow by clamping diff
548
+ safe_diff_f64 = min(diff, (2**127 - 1) / max(concave_factor, 1e-10))
549
+ addend = int(safe_diff_f64 * concave_factor)
550
+ addend = max(0, min(addend, 2**127 - 1)) # Clamp to safe range
551
+
552
+ # Final cost: min_price + addend
553
+ cost = min_cost + addend
554
+ cost = max(cost, min_cost) # Ensure cost >= min_cost
555
+ cost = min(cost, 2**127 - 1) # Clamp to safe u128 range
556
+
557
+ logger.info(
558
+ f"Subnet registration cost at block {current_block}: {cost / 1e18:.2f} TENSOR"
559
+ )
560
+ logger.debug(
561
+ f" Last cost: {last_cost / 1e18:.2f}, Min: {min_cost / 1e18:.2f}, "
562
+ f"Blocks since last: {delta_blocks}/{decay_blocks}, Alpha: {alpha / 1e18:.6f}"
563
+ )
564
+
565
+ return cost
566
+ except Exception as e:
567
+ logger.error(f"Failed to get subnet registration cost: {str(e)}")
568
+ return None