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,1080 @@
1
+ """
2
+ Stake command prompting logic.
3
+
4
+ Handles user interaction and input validation for stake operations.
5
+ Uses HTCLI UI components and Pydantic models for proper validation.
6
+ """
7
+
8
+ import json
9
+ from typing import Optional
10
+
11
+ from ...dependencies import get_client
12
+ from ...models.requests import (
13
+ ClaimUnbondingsRequest,
14
+ DelegateStakeAddRequest,
15
+ DelegateStakeDonateRequest,
16
+ DelegateStakeRemoveRequest,
17
+ DelegateStakeSwapRequest,
18
+ DelegateStakeTransferRequest,
19
+ NodeDelegateStakeAddRequest,
20
+ NodeDelegateStakeDonateRequest,
21
+ NodeDelegateStakeRemoveRequest,
22
+ NodeDelegateStakeSwapRequest,
23
+ NodeDelegateStakeTransferRequest,
24
+ StakeAddRequest,
25
+ StakeRemoveRequest,
26
+ StakeSwapFromNodeToSubnetRequest,
27
+ StakeSwapFromSubnetToNodeRequest,
28
+ StakeSwapFromSubnetToValidatorRequest,
29
+ StakeSwapFromValidatorToSubnetRequest,
30
+ StakeSwapQueueUpdateRequest,
31
+ ValidatorDelegateStakeAddRequest,
32
+ ValidatorDelegateStakeDonateRequest,
33
+ ValidatorDelegateStakeRemoveRequest,
34
+ ValidatorDelegateStakeSwapRequest,
35
+ ValidatorDelegateStakeTransferRequest,
36
+ )
37
+ from ...ui.colors import info, warning
38
+ from ...ui.display import HTCLIConsole, print_error, print_info
39
+ from ...ui.prompts import HTCLIPrompt, amount_prompt, confirm_prompt
40
+ from ...utils.blockchain.validation import validate_address
41
+ from ...utils.validation import (
42
+ validate_hotkey_address,
43
+ validate_node_id_prompt,
44
+ validate_stake_amount_prompt,
45
+ validate_subnet_id_prompt,
46
+ )
47
+
48
+ console = HTCLIConsole()
49
+ prompt = HTCLIPrompt()
50
+
51
+
52
+ def prompt_stake_add(
53
+ subnet_id: Optional[int] = None,
54
+ node_id: Optional[int] = None,
55
+ hotkey: Optional[str] = None,
56
+ amount: Optional[int] = None,
57
+ ) -> StakeAddRequest:
58
+ """Prompt for stake addition parameters."""
59
+ # Validate subnet_id if provided
60
+ if subnet_id is not None:
61
+ is_valid, error_msg = validate_subnet_id_prompt(subnet_id)
62
+ if not is_valid:
63
+ print_error(f"Invalid subnet ID: {error_msg}")
64
+ subnet_id = None
65
+
66
+ if subnet_id is None:
67
+ subnet_id = prompt.integer_prompt(
68
+ "Enter the Subnet ID to stake to",
69
+ min_value=0,
70
+ )
71
+
72
+ # Validate node_id if provided
73
+ if node_id is not None:
74
+ is_valid, error_msg = validate_node_id_prompt(node_id)
75
+ if not is_valid:
76
+ print_error(f"Invalid node ID: {error_msg}")
77
+ node_id = None
78
+
79
+ # If node_id is provided, fetch hotkey from node_id automatically
80
+ if node_id is not None:
81
+ client = get_client()
82
+ node_info = client.rpc.node.get_subnet_node_info(subnet_id, node_id)
83
+
84
+ if node_info is None:
85
+ raise ValueError(f"Node {node_id} not found in subnet {subnet_id}")
86
+
87
+ hotkey = node_info.hotkey
88
+ print_info(f"Found hotkey {hotkey} for node {node_id} in subnet {subnet_id}")
89
+ else:
90
+ # No node_id provided - user must provide hotkey manually
91
+ # Validate hotkey if provided
92
+ if hotkey is not None:
93
+ is_valid, error_msg = validate_hotkey_address(hotkey)
94
+ if not is_valid:
95
+ print_error(f"Invalid hotkey address: {error_msg}")
96
+ hotkey = None
97
+
98
+ if hotkey is None:
99
+ hotkey = prompt.text_prompt(
100
+ "Enter the hotkey address to stake with",
101
+ )
102
+ # Validate address format
103
+ if not validate_address(hotkey):
104
+ raise ValueError(f"Invalid hotkey address format: {hotkey}")
105
+
106
+ # Try to get node_id from hotkey (optional - stake works even if hotkey not registered as node yet)
107
+ # Note: subnet_node_id is required by extrinsic but not used in staking logic
108
+ # We use 0 as default if hotkey is not registered
109
+ node_id = 0
110
+ client = get_client()
111
+ try:
112
+ result = client.substrate.query(
113
+ module="Network",
114
+ storage_function="HotkeySubnetNodeId",
115
+ params=[subnet_id, hotkey],
116
+ )
117
+ if result and result.value is not None:
118
+ node_id = result.value
119
+ except Exception:
120
+ # Hotkey might not be registered yet - that's OK, stake still works
121
+ # The blockchain will validate that coldkey owns the hotkey
122
+ pass
123
+
124
+ # Validate amount if provided (amount is in TENSOR)
125
+ if amount is not None:
126
+ is_valid, error_msg = validate_stake_amount_prompt(amount)
127
+ if not is_valid:
128
+ print_error(f"Invalid stake amount: {error_msg}")
129
+ amount = None
130
+
131
+ if amount is None:
132
+ amount = prompt.integer_prompt(
133
+ "Enter the stake amount to add",
134
+ min_value=1,
135
+ )
136
+
137
+ # Convert TENSOR to WEI for the request model (which expects WEI)
138
+ # amount is in TENSOR (integer), convert to WEI
139
+ amount_wei = int(amount * 1e18)
140
+
141
+ # subnet_node_id is required by extrinsic signature but not used in staking logic
142
+ # Staking is stored per (hotkey, subnet_id), not per node_id
143
+ # If node_id is 0, that's fine - the blockchain only validates coldkey owns hotkey
144
+ return StakeAddRequest(
145
+ subnet_id=subnet_id,
146
+ subnet_node_id=node_id or 0, # Use 0 as default if hotkey not registered yet
147
+ hotkey=hotkey,
148
+ stake_amount=amount_wei,
149
+ )
150
+
151
+
152
+ def prompt_stake_remove(
153
+ subnet_id: Optional[int] = None,
154
+ node_id: Optional[int] = None,
155
+ hotkey: Optional[str] = None,
156
+ amount: Optional[int] = None,
157
+ coldkey_name: Optional[str] = None,
158
+ ) -> StakeRemoveRequest:
159
+ """Prompt for stake removal parameters."""
160
+ client = get_client()
161
+
162
+ # Get coldkey address if provided (for hotkey disambiguation)
163
+ coldkey_address = None
164
+ if coldkey_name:
165
+ try:
166
+ from ...utils.wallet.crypto import get_wallet_info_by_name
167
+
168
+ coldkey_info = get_wallet_info_by_name(coldkey_name, is_hotkey=False)
169
+ coldkey_address = (
170
+ coldkey_info.get("evm_address")
171
+ or coldkey_info.get("ss58_address")
172
+ or coldkey_info.get("address")
173
+ )
174
+ except Exception:
175
+ # Coldkey not found yet, will be resolved later - that's okay
176
+ pass
177
+ # Validate subnet_id if provided
178
+ if subnet_id is not None:
179
+ is_valid, error_msg = validate_subnet_id_prompt(subnet_id)
180
+ if not is_valid:
181
+ print_error(f"Invalid subnet ID: {error_msg}")
182
+ subnet_id = None
183
+
184
+ if subnet_id is None:
185
+ subnet_id = prompt.integer_prompt(
186
+ "Enter the Subnet ID to remove stake from",
187
+ min_value=0,
188
+ )
189
+
190
+ # Validate node_id if provided
191
+ if node_id is not None:
192
+ is_valid, error_msg = validate_node_id_prompt(node_id)
193
+ if not is_valid:
194
+ print_error(f"Invalid node ID: {error_msg}")
195
+ node_id = None
196
+
197
+ if node_id is None:
198
+ # Ask if user wants to remove stake from subnet or specific node
199
+ console.print(info("Remove stake from:"))
200
+ console.print(" 1. Subnet (general subnet staking)")
201
+ console.print(" 2. Specific node in subnet")
202
+
203
+ choice = prompt.integer_prompt(
204
+ "Select option (1-2)",
205
+ min_value=1,
206
+ max_value=2,
207
+ )
208
+
209
+ if choice == 2:
210
+ node_id = prompt.integer_prompt(
211
+ "Enter the Node ID within the subnet",
212
+ min_value=0,
213
+ )
214
+
215
+ # Resolve hotkey if provided (handles both wallet names and addresses)
216
+ if hotkey is not None:
217
+ hotkey = hotkey.strip()
218
+ # Check if it's already a valid address
219
+ if not client.offchain.wallet.validate_ethereum_address(hotkey):
220
+ # Try to resolve as wallet name with coldkey context
221
+ try:
222
+ resolved_hotkey = client.offchain.wallet.resolve_hotkey_address(
223
+ hotkey,
224
+ owner_coldkey_name=coldkey_name,
225
+ owner_address=coldkey_address,
226
+ )
227
+ # Verify the resolved address is valid
228
+ if not client.offchain.wallet.validate_ethereum_address(
229
+ resolved_hotkey
230
+ ):
231
+ raise ValueError(
232
+ f"Resolved hotkey address is invalid: {resolved_hotkey}"
233
+ )
234
+ hotkey = resolved_hotkey
235
+ except ValueError as e:
236
+ # Resolution failed - provide clear error message
237
+ print_error(f"Failed to resolve hotkey '{hotkey}': {str(e)}")
238
+ print_error(
239
+ "Please provide a valid hotkey wallet name or Ethereum address (0x...)"
240
+ )
241
+ hotkey = None
242
+ except Exception as e:
243
+ # Unexpected error - re-raise with context
244
+ raise RuntimeError(
245
+ f"Unexpected error resolving hotkey '{hotkey}': {str(e)}"
246
+ ) from e
247
+
248
+ if hotkey is None:
249
+ hotkey = prompt.text_prompt(
250
+ "Enter the hotkey address or wallet name to remove stake from",
251
+ )
252
+ hotkey = hotkey.strip()
253
+ # Check if it's already a valid address
254
+ if not client.offchain.wallet.validate_ethereum_address(hotkey):
255
+ # Try to resolve as wallet name with coldkey context
256
+ try:
257
+ resolved_hotkey = client.offchain.wallet.resolve_hotkey_address(
258
+ hotkey,
259
+ owner_coldkey_name=coldkey_name,
260
+ owner_address=coldkey_address,
261
+ )
262
+ # Verify the resolved address is valid
263
+ if not client.offchain.wallet.validate_ethereum_address(
264
+ resolved_hotkey
265
+ ):
266
+ raise ValueError(
267
+ f"Resolved hotkey address is invalid: {resolved_hotkey}"
268
+ )
269
+ hotkey = resolved_hotkey
270
+ except ValueError as e:
271
+ # Resolution failed - validate as address format
272
+ if not validate_address(hotkey):
273
+ raise ValueError(
274
+ f"Invalid hotkey: '{hotkey}' is neither a valid wallet name nor a valid Ethereum address. "
275
+ f"Error: {str(e)}"
276
+ ) from e
277
+ # If it passes validate_address but not validate_ethereum_address, something is wrong
278
+ raise ValueError(f"Invalid hotkey address format: {hotkey}") from e
279
+ except Exception as e:
280
+ # Unexpected error - re-raise with context
281
+ raise RuntimeError(
282
+ f"Unexpected error resolving hotkey '{hotkey}': {str(e)}"
283
+ ) from e
284
+
285
+ # Final validation - ensure we have a valid Ethereum address
286
+ if not client.offchain.wallet.validate_ethereum_address(hotkey):
287
+ raise ValueError(f"Invalid hotkey address format: {hotkey}")
288
+
289
+ # Validate amount if provided (amount is in TENSOR)
290
+ if amount is not None:
291
+ is_valid, error_msg = validate_stake_amount_prompt(amount)
292
+ if not is_valid:
293
+ print_error(f"Invalid stake amount: {error_msg}")
294
+ amount = None
295
+
296
+ if amount is None:
297
+ amount = prompt.integer_prompt(
298
+ "Enter the stake amount to remove",
299
+ min_value=1,
300
+ )
301
+
302
+ # Convert TENSOR to WEI for the request model (which expects WEI)
303
+ amount_wei = int(amount * 1e18)
304
+
305
+ return StakeRemoveRequest(
306
+ subnet_id=subnet_id,
307
+ hotkey=hotkey,
308
+ stake_amount=amount_wei,
309
+ )
310
+
311
+
312
+ def prompt_stake_claim() -> ClaimUnbondingsRequest:
313
+ """Prompt for stake claiming parameters."""
314
+ console.print(info("Claim Unbonded Tokens"))
315
+ console.print(
316
+ "This will claim all tokens that have completed the unbonding period.\n"
317
+ )
318
+
319
+ # Confirm action
320
+ confirmed = confirm_prompt("Proceed with claiming unbonded tokens?", default=True)
321
+ if not confirmed:
322
+ raise KeyboardInterrupt("Operation cancelled")
323
+
324
+ return ClaimUnbondingsRequest()
325
+
326
+
327
+ def prompt_delegate_stake_add(
328
+ subnet_id: Optional[int] = None,
329
+ stake_amount: Optional[float] = None,
330
+ coldkey: Optional[str] = None,
331
+ ) -> DelegateStakeAddRequest:
332
+ """Prompt for adding delegate stake to a subnet."""
333
+ # Validate subnet_id if provided
334
+ if subnet_id is not None:
335
+ is_valid, error_msg = validate_subnet_id_prompt(subnet_id)
336
+ if not is_valid:
337
+ print_error(f"Invalid subnet ID: {error_msg}")
338
+ subnet_id = None
339
+
340
+ if subnet_id is None:
341
+ subnet_id = prompt.integer_prompt(
342
+ "Enter the Subnet ID to delegate stake to",
343
+ min_value=0,
344
+ )
345
+
346
+ # Validate stake_amount if provided (stake_amount is in TENSOR)
347
+ if stake_amount is not None:
348
+ # Convert to float if it's an integer (treat integers as TENSOR)
349
+ if isinstance(stake_amount, int):
350
+ stake_amount = float(stake_amount)
351
+ is_valid, error_msg = validate_stake_amount_prompt(stake_amount)
352
+ if not is_valid:
353
+ print_error(f"Invalid stake amount: {error_msg}")
354
+ stake_amount = None
355
+
356
+ if stake_amount is None:
357
+ from ...ui.prompts import amount_prompt
358
+
359
+ stake_amount = amount_prompt(
360
+ "Enter the amount to delegate",
361
+ currency="TENSOR",
362
+ min_amount=0.000001,
363
+ )
364
+
365
+ # Convert TENSOR to WEI for the request model (which expects WEI)
366
+ stake_amount_wei = int(stake_amount * 1e18)
367
+
368
+ return DelegateStakeAddRequest(
369
+ subnet_id=subnet_id,
370
+ stake_amount=stake_amount_wei,
371
+ )
372
+
373
+
374
+ def prompt_delegate_stake_remove(
375
+ subnet_id: Optional[int] = None,
376
+ shares_to_remove: Optional[int] = None,
377
+ ) -> DelegateStakeRemoveRequest:
378
+ """Prompt for removing delegate stake from a subnet."""
379
+ if subnet_id is None:
380
+ subnet_id = prompt.integer_prompt(
381
+ "Enter the Subnet ID to remove delegate stake from",
382
+ min_value=0,
383
+ )
384
+
385
+ if shares_to_remove is None:
386
+ console.print(warning("Note: Enter shares as displayed in 'htcli stake list'."))
387
+ console.print(
388
+ info("Check your current shares with: htcli stake list --coldkey <wallet>")
389
+ )
390
+ shares_to_remove = prompt.integer_prompt(
391
+ "Enter the number of shares to remove",
392
+ min_value=1,
393
+ )
394
+
395
+ # Convert human-readable shares to wei format (multiply by 10^18)
396
+ # Blockchain stores shares with 18 decimals
397
+ shares_in_wei = int(shares_to_remove * 10**18)
398
+
399
+ return DelegateStakeRemoveRequest(
400
+ subnet_id=subnet_id,
401
+ shares_to_be_removed=shares_in_wei,
402
+ )
403
+
404
+
405
+ def prompt_validator_delegate_stake_add(
406
+ validator_id: Optional[int] = None,
407
+ stake_amount: Optional[float] = None,
408
+ coldkey: Optional[str] = None,
409
+ ) -> ValidatorDelegateStakeAddRequest:
410
+ """Prompt for adding delegate stake to a validator."""
411
+ if validator_id is not None and validator_id < 0:
412
+ print_error("Invalid validator ID: must be non-negative")
413
+ validator_id = None
414
+
415
+ if validator_id is None:
416
+ validator_id = prompt.integer_prompt(
417
+ "Enter the Validator ID to delegate to",
418
+ min_value=0,
419
+ )
420
+
421
+ if stake_amount is not None:
422
+ if isinstance(stake_amount, int):
423
+ stake_amount = float(stake_amount)
424
+ is_valid, error_msg = validate_stake_amount_prompt(stake_amount)
425
+ if not is_valid:
426
+ print_error(f"Invalid stake amount: {error_msg}")
427
+ stake_amount = None
428
+
429
+ if stake_amount is None:
430
+ stake_amount = amount_prompt(
431
+ "Enter the amount to delegate",
432
+ currency="TENSOR",
433
+ min_amount=0.000001,
434
+ )
435
+
436
+ return ValidatorDelegateStakeAddRequest(
437
+ validator_id=validator_id,
438
+ delegate_stake_to_be_added=int(stake_amount * 1e18),
439
+ )
440
+
441
+
442
+ def prompt_validator_delegate_stake_remove(
443
+ validator_id: Optional[int] = None,
444
+ shares_to_remove: Optional[int] = None,
445
+ ) -> ValidatorDelegateStakeRemoveRequest:
446
+ """Prompt for removing delegate stake from a validator."""
447
+ if validator_id is not None and validator_id < 0:
448
+ print_error("Invalid validator ID: must be non-negative")
449
+ validator_id = None
450
+
451
+ if validator_id is None:
452
+ validator_id = prompt.integer_prompt(
453
+ "Enter the Validator ID to remove delegate stake from",
454
+ min_value=0,
455
+ )
456
+
457
+ if shares_to_remove is None:
458
+ console.print(warning("Note: You must specify shares to remove, not amount."))
459
+ shares_to_remove = prompt.integer_prompt(
460
+ "Enter the number of shares to remove",
461
+ min_value=1,
462
+ )
463
+
464
+ return ValidatorDelegateStakeRemoveRequest(
465
+ validator_id=validator_id,
466
+ validator_delegate_stake_shares_to_be_removed=shares_to_remove,
467
+ )
468
+
469
+
470
+ def prompt_validator_delegate_swap(
471
+ from_validator_id: Optional[int] = None,
472
+ to_validator_id: Optional[int] = None,
473
+ shares_to_swap: Optional[int] = None,
474
+ ) -> ValidatorDelegateStakeSwapRequest:
475
+ """Prompt for swapping validator delegate shares between validators."""
476
+ if from_validator_id is not None and from_validator_id < 0:
477
+ print_error("Invalid source validator ID: must be non-negative")
478
+ from_validator_id = None
479
+ if to_validator_id is not None and to_validator_id < 0:
480
+ print_error("Invalid destination validator ID: must be non-negative")
481
+ to_validator_id = None
482
+
483
+ if from_validator_id is None:
484
+ from_validator_id = prompt.integer_prompt(
485
+ "Enter source validator ID",
486
+ min_value=0,
487
+ )
488
+ if to_validator_id is None:
489
+ to_validator_id = prompt.integer_prompt(
490
+ "Enter destination validator ID",
491
+ min_value=0,
492
+ )
493
+ if shares_to_swap is None:
494
+ shares_to_swap = prompt.integer_prompt(
495
+ "Enter number of shares to swap",
496
+ min_value=1,
497
+ )
498
+
499
+ return ValidatorDelegateStakeSwapRequest(
500
+ from_validator_id=from_validator_id,
501
+ to_validator_id=to_validator_id,
502
+ stake_to_be_removed=shares_to_swap,
503
+ )
504
+
505
+
506
+ def prompt_validator_delegate_transfer(
507
+ validator_id: Optional[int] = None,
508
+ to_account: Optional[str] = None,
509
+ shares_to_transfer: Optional[int] = None,
510
+ ) -> ValidatorDelegateStakeTransferRequest:
511
+ """Prompt for transferring validator delegate shares."""
512
+ if validator_id is not None and validator_id < 0:
513
+ print_error("Invalid validator ID: must be non-negative")
514
+ validator_id = None
515
+
516
+ if validator_id is None:
517
+ validator_id = prompt.integer_prompt(
518
+ "Enter validator ID",
519
+ min_value=0,
520
+ )
521
+ if to_account is None:
522
+ to_account = prompt.text_prompt("Enter destination account (0x...)")
523
+ if not validate_address(to_account):
524
+ raise ValueError("Destination account must be a valid Ethereum address")
525
+
526
+ if shares_to_transfer is None:
527
+ shares_to_transfer = prompt.integer_prompt(
528
+ "Enter number of shares to transfer",
529
+ min_value=1,
530
+ )
531
+
532
+ return ValidatorDelegateStakeTransferRequest(
533
+ validator_id=validator_id,
534
+ to_account_id=to_account,
535
+ validator_delegate_stake_shares_to_transfer=shares_to_transfer,
536
+ )
537
+
538
+
539
+ def prompt_validator_delegate_donate(
540
+ validator_id: Optional[int] = None,
541
+ amount: Optional[float] = None,
542
+ ) -> ValidatorDelegateStakeDonateRequest:
543
+ """Prompt for donating validator delegate stake."""
544
+ if validator_id is not None and validator_id < 0:
545
+ print_error("Invalid validator ID: must be non-negative")
546
+ validator_id = None
547
+
548
+ if validator_id is None:
549
+ validator_id = prompt.integer_prompt(
550
+ "Enter validator ID",
551
+ min_value=0,
552
+ )
553
+
554
+ if amount is not None:
555
+ if isinstance(amount, int):
556
+ amount = float(amount)
557
+ is_valid, error_msg = validate_stake_amount_prompt(amount)
558
+ if not is_valid:
559
+ print_error(f"Invalid amount: {error_msg}")
560
+ amount = None
561
+
562
+ if amount is None:
563
+ amount = amount_prompt(
564
+ "Enter donation amount",
565
+ currency="TENSOR",
566
+ min_amount=0.000001,
567
+ )
568
+
569
+ return ValidatorDelegateStakeDonateRequest(
570
+ validator_id=validator_id,
571
+ amount=int(amount * 1e18),
572
+ )
573
+
574
+
575
+ def prompt_node_delegate_stake_add(
576
+ subnet_id: Optional[int] = None,
577
+ subnet_node_id: Optional[int] = None,
578
+ stake_amount: Optional[float] = None,
579
+ coldkey: Optional[str] = None,
580
+ ) -> NodeDelegateStakeAddRequest:
581
+ """Prompt for adding delegate stake to a specific node."""
582
+ # Validate subnet_id if provided
583
+ if subnet_id is not None:
584
+ is_valid, error_msg = validate_subnet_id_prompt(subnet_id)
585
+ if not is_valid:
586
+ print_error(f"Invalid subnet ID: {error_msg}")
587
+ subnet_id = None
588
+
589
+ if subnet_id is None:
590
+ subnet_id = prompt.integer_prompt(
591
+ "Enter the Subnet ID",
592
+ min_value=0,
593
+ )
594
+
595
+ # Validate subnet_node_id if provided
596
+ if subnet_node_id is not None:
597
+ is_valid, error_msg = validate_node_id_prompt(subnet_node_id)
598
+ if not is_valid:
599
+ print_error(f"Invalid node ID: {error_msg}")
600
+ subnet_node_id = None
601
+
602
+ if subnet_node_id is None:
603
+ subnet_node_id = prompt.integer_prompt(
604
+ "Enter the Node ID to delegate to",
605
+ min_value=0,
606
+ )
607
+
608
+ # Validate stake_amount if provided (stake_amount is in TENSOR)
609
+ if stake_amount is not None:
610
+ # Convert to float if it's an integer (treat integers as TENSOR)
611
+ if isinstance(stake_amount, int):
612
+ stake_amount = float(stake_amount)
613
+ is_valid, error_msg = validate_stake_amount_prompt(stake_amount)
614
+ if not is_valid:
615
+ print_error(f"Invalid stake amount: {error_msg}")
616
+ stake_amount = None
617
+
618
+ if stake_amount is None:
619
+ from ...ui.prompts import amount_prompt
620
+
621
+ stake_amount = amount_prompt(
622
+ "Enter the amount to delegate",
623
+ currency="TENSOR",
624
+ min_amount=0.000001,
625
+ )
626
+
627
+ # Convert TENSOR to WEI for the request model (which expects WEI)
628
+ stake_amount_wei = int(stake_amount * 1e18)
629
+
630
+ return NodeDelegateStakeAddRequest(
631
+ subnet_id=subnet_id,
632
+ subnet_node_id=subnet_node_id,
633
+ node_delegate_stake_to_be_added=stake_amount_wei,
634
+ )
635
+
636
+
637
+ def prompt_node_delegate_stake_remove(
638
+ subnet_id: Optional[int] = None,
639
+ subnet_node_id: Optional[int] = None,
640
+ shares_to_remove: Optional[int] = None,
641
+ ) -> NodeDelegateStakeRemoveRequest:
642
+ """Prompt for removing delegate stake from a specific node."""
643
+ if subnet_id is None:
644
+ subnet_id = prompt.integer_prompt(
645
+ "Enter the Subnet ID",
646
+ min_value=0,
647
+ )
648
+
649
+ if subnet_node_id is None:
650
+ subnet_node_id = prompt.integer_prompt(
651
+ "Enter the Node ID to remove delegate stake from",
652
+ min_value=0,
653
+ )
654
+
655
+ if shares_to_remove is None:
656
+ console.print(warning("Note: You must specify shares to remove, not amount."))
657
+ console.print(
658
+ info(
659
+ "Check your current shares with: htcli stake list --coldkey <your coldkey address>"
660
+ )
661
+ )
662
+ shares_to_remove = prompt.integer_prompt(
663
+ "Enter the number of shares to remove",
664
+ min_value=1,
665
+ )
666
+
667
+ return NodeDelegateStakeRemoveRequest(
668
+ subnet_id=subnet_id,
669
+ subnet_node_id=subnet_node_id,
670
+ node_delegate_stake_shares_to_be_removed=shares_to_remove,
671
+ )
672
+
673
+
674
+ def prompt_node_delegate_swap(
675
+ from_subnet_id: Optional[int] = None,
676
+ from_subnet_node_id: Optional[int] = None,
677
+ to_subnet_id: Optional[int] = None,
678
+ to_subnet_node_id: Optional[int] = None,
679
+ shares_to_swap: Optional[int] = None,
680
+ ) -> NodeDelegateStakeSwapRequest:
681
+ """Prompt for swapping node delegate shares between nodes."""
682
+ if from_subnet_id is None:
683
+ from_subnet_id = prompt.integer_prompt(
684
+ "Enter source subnet ID",
685
+ min_value=0,
686
+ )
687
+ if from_subnet_node_id is None:
688
+ from_subnet_node_id = prompt.integer_prompt(
689
+ "Enter source node ID",
690
+ min_value=0,
691
+ )
692
+ if to_subnet_id is None:
693
+ to_subnet_id = prompt.integer_prompt(
694
+ "Enter destination subnet ID",
695
+ min_value=0,
696
+ )
697
+ if to_subnet_node_id is None:
698
+ to_subnet_node_id = prompt.integer_prompt(
699
+ "Enter destination node ID",
700
+ min_value=0,
701
+ )
702
+ if shares_to_swap is None:
703
+ shares_to_swap = prompt.integer_prompt(
704
+ "Enter number of shares to swap",
705
+ min_value=1,
706
+ )
707
+
708
+ return NodeDelegateStakeSwapRequest(
709
+ from_subnet_id=from_subnet_id,
710
+ from_subnet_node_id=from_subnet_node_id,
711
+ to_subnet_id=to_subnet_id,
712
+ to_subnet_node_id=to_subnet_node_id,
713
+ node_delegate_stake_shares_to_swap=shares_to_swap,
714
+ )
715
+
716
+
717
+ def prompt_node_delegate_transfer(
718
+ subnet_id: Optional[int] = None,
719
+ subnet_node_id: Optional[int] = None,
720
+ to_account: Optional[str] = None,
721
+ shares_to_transfer: Optional[int] = None,
722
+ ) -> NodeDelegateStakeTransferRequest:
723
+ """Prompt for transferring node delegate shares."""
724
+ if subnet_id is None:
725
+ subnet_id = prompt.integer_prompt(
726
+ "Enter subnet ID",
727
+ min_value=0,
728
+ )
729
+ if subnet_node_id is None:
730
+ subnet_node_id = prompt.integer_prompt(
731
+ "Enter node ID",
732
+ min_value=0,
733
+ )
734
+ if to_account is None:
735
+ to_account = prompt.text_prompt("Enter destination account (0x...)")
736
+ if not validate_address(to_account):
737
+ raise ValueError("Destination account must be a valid Ethereum address")
738
+
739
+ if shares_to_transfer is None:
740
+ shares_to_transfer = prompt.integer_prompt(
741
+ "Enter number of shares to transfer",
742
+ min_value=1,
743
+ )
744
+
745
+ return NodeDelegateStakeTransferRequest(
746
+ subnet_id=subnet_id,
747
+ subnet_node_id=subnet_node_id,
748
+ to_account_id=to_account,
749
+ node_delegate_stake_shares_to_transfer=shares_to_transfer,
750
+ )
751
+
752
+
753
+ def prompt_node_delegate_donate(
754
+ subnet_id: Optional[int] = None,
755
+ subnet_node_id: Optional[int] = None,
756
+ amount: Optional[float] = None,
757
+ ) -> NodeDelegateStakeDonateRequest:
758
+ """Prompt for donating node delegate stake."""
759
+ if subnet_id is None:
760
+ subnet_id = prompt.integer_prompt(
761
+ "Enter subnet ID",
762
+ min_value=0,
763
+ )
764
+ if subnet_node_id is None:
765
+ subnet_node_id = prompt.integer_prompt(
766
+ "Enter node ID",
767
+ min_value=0,
768
+ )
769
+ # Validate amount if provided (amount is in TENSOR)
770
+ if amount is not None:
771
+ # Convert to float if it's an integer (treat integers as TENSOR)
772
+ if isinstance(amount, int):
773
+ amount = float(amount)
774
+ is_valid, error_msg = validate_stake_amount_prompt(amount)
775
+ if not is_valid:
776
+ print_error(f"Invalid amount: {error_msg}")
777
+ amount = None
778
+
779
+ if amount is None:
780
+ amount = amount_prompt(
781
+ "Enter donation amount",
782
+ currency="TENSOR",
783
+ min_amount=0.000001,
784
+ )
785
+
786
+ # Convert TENSOR to WEI for the request model (which expects WEI)
787
+ amount_wei = int(amount * 1e18)
788
+
789
+ return NodeDelegateStakeDonateRequest(
790
+ subnet_id=subnet_id,
791
+ subnet_node_id=subnet_node_id,
792
+ amount=amount_wei,
793
+ )
794
+
795
+
796
+ def prompt_swap_node_to_subnet(
797
+ from_subnet_id: Optional[int] = None,
798
+ from_subnet_node_id: Optional[int] = None,
799
+ to_subnet_id: Optional[int] = None,
800
+ shares_to_swap: Optional[int] = None,
801
+ ) -> StakeSwapFromNodeToSubnetRequest:
802
+ """Prompt for swapping node delegate shares into subnet shares."""
803
+ if from_subnet_id is None:
804
+ from_subnet_id = prompt.integer_prompt(
805
+ "Enter source subnet ID",
806
+ min_value=0,
807
+ )
808
+ if from_subnet_node_id is None:
809
+ from_subnet_node_id = prompt.integer_prompt(
810
+ "Enter source node ID",
811
+ min_value=0,
812
+ )
813
+ if to_subnet_id is None:
814
+ to_subnet_id = prompt.integer_prompt(
815
+ "Enter destination subnet ID",
816
+ min_value=0,
817
+ )
818
+ if shares_to_swap is None:
819
+ shares_to_swap = prompt.integer_prompt(
820
+ "Enter number of shares to swap",
821
+ min_value=1,
822
+ )
823
+
824
+ return StakeSwapFromNodeToSubnetRequest(
825
+ from_subnet_id=from_subnet_id,
826
+ from_subnet_node_id=from_subnet_node_id,
827
+ to_subnet_id=to_subnet_id,
828
+ node_delegate_stake_shares_to_swap=shares_to_swap,
829
+ )
830
+
831
+
832
+ def prompt_swap_subnet_to_node(
833
+ from_subnet_id: Optional[int] = None,
834
+ to_subnet_id: Optional[int] = None,
835
+ to_subnet_node_id: Optional[int] = None,
836
+ shares_to_swap: Optional[int] = None,
837
+ ) -> StakeSwapFromSubnetToNodeRequest:
838
+ """Prompt for swapping subnet delegate shares into node delegate shares."""
839
+ if from_subnet_id is None:
840
+ from_subnet_id = prompt.integer_prompt(
841
+ "Enter source subnet ID",
842
+ min_value=0,
843
+ )
844
+ if to_subnet_id is None:
845
+ to_subnet_id = prompt.integer_prompt(
846
+ "Enter destination subnet ID",
847
+ min_value=0,
848
+ )
849
+ if to_subnet_node_id is None:
850
+ to_subnet_node_id = prompt.integer_prompt(
851
+ "Enter destination node ID",
852
+ min_value=0,
853
+ )
854
+ if shares_to_swap is None:
855
+ shares_to_swap = prompt.integer_prompt(
856
+ "Enter number of shares to swap",
857
+ min_value=1,
858
+ )
859
+
860
+ return StakeSwapFromSubnetToNodeRequest(
861
+ from_subnet_id=from_subnet_id,
862
+ to_subnet_id=to_subnet_id,
863
+ to_subnet_node_id=to_subnet_node_id,
864
+ delegate_stake_shares_to_swap=shares_to_swap,
865
+ )
866
+
867
+
868
+ def prompt_swap_validator_to_subnet(
869
+ from_validator_id: Optional[int] = None,
870
+ to_subnet_id: Optional[int] = None,
871
+ shares_to_swap: Optional[int] = None,
872
+ ) -> StakeSwapFromValidatorToSubnetRequest:
873
+ """Prompt for swapping validator delegate shares into subnet shares."""
874
+ if from_validator_id is not None and from_validator_id < 0:
875
+ print_error("Invalid source validator ID: must be non-negative")
876
+ from_validator_id = None
877
+ if from_validator_id is None:
878
+ from_validator_id = prompt.integer_prompt(
879
+ "Enter source validator ID",
880
+ min_value=0,
881
+ )
882
+ if to_subnet_id is None:
883
+ to_subnet_id = prompt.integer_prompt(
884
+ "Enter destination subnet ID",
885
+ min_value=0,
886
+ )
887
+ if shares_to_swap is None:
888
+ shares_to_swap = prompt.integer_prompt(
889
+ "Enter number of shares to swap",
890
+ min_value=1,
891
+ )
892
+
893
+ return StakeSwapFromValidatorToSubnetRequest(
894
+ from_validator_id=from_validator_id,
895
+ to_subnet_id=to_subnet_id,
896
+ node_delegate_stake_shares_to_swap=shares_to_swap,
897
+ )
898
+
899
+
900
+ def prompt_swap_subnet_to_validator(
901
+ from_subnet_id: Optional[int] = None,
902
+ to_validator_id: Optional[int] = None,
903
+ shares_to_swap: Optional[int] = None,
904
+ ) -> StakeSwapFromSubnetToValidatorRequest:
905
+ """Prompt for swapping subnet delegate shares into validator shares."""
906
+ if from_subnet_id is None:
907
+ from_subnet_id = prompt.integer_prompt(
908
+ "Enter source subnet ID",
909
+ min_value=0,
910
+ )
911
+ if to_validator_id is not None and to_validator_id < 0:
912
+ print_error("Invalid destination validator ID: must be non-negative")
913
+ to_validator_id = None
914
+ if to_validator_id is None:
915
+ to_validator_id = prompt.integer_prompt(
916
+ "Enter destination validator ID",
917
+ min_value=0,
918
+ )
919
+ if shares_to_swap is None:
920
+ shares_to_swap = prompt.integer_prompt(
921
+ "Enter number of shares to swap",
922
+ min_value=1,
923
+ )
924
+
925
+ return StakeSwapFromSubnetToValidatorRequest(
926
+ from_subnet_id=from_subnet_id,
927
+ to_validator_id=to_validator_id,
928
+ subnet_delegate_stake_shares_to_swap=shares_to_swap,
929
+ )
930
+
931
+
932
+ def prompt_swap_queue_update(
933
+ queue_id: Optional[int] = None,
934
+ new_call_json: Optional[str] = None,
935
+ ) -> StakeSwapQueueUpdateRequest:
936
+ """Prompt for updating a swap queue entry."""
937
+ if queue_id is None:
938
+ queue_id = prompt.integer_prompt(
939
+ "Enter queue entry ID",
940
+ min_value=0,
941
+ )
942
+
943
+ if not new_call_json:
944
+ console.print(
945
+ info(
946
+ "Provide the new call payload as JSON. Example:\n"
947
+ '{"type": "SwapToNodeDelegateStake", "to_subnet_id": 1, "to_subnet_node_id": 3}'
948
+ )
949
+ )
950
+ new_call_json = prompt.text_prompt("Enter new call JSON")
951
+
952
+ try:
953
+ parsed = json.loads(new_call_json)
954
+ except json.JSONDecodeError as exc:
955
+ raise ValueError(f"Invalid JSON: {exc.msg}") from exc
956
+
957
+ return StakeSwapQueueUpdateRequest(
958
+ queue_id=queue_id,
959
+ new_call=parsed,
960
+ )
961
+
962
+
963
+ def prompt_delegate_swap(
964
+ from_subnet_id: Optional[int] = None,
965
+ to_subnet_id: Optional[int] = None,
966
+ shares: Optional[int] = None,
967
+ ) -> DelegateStakeSwapRequest:
968
+ """Prompt for swapping delegate stake between subnets."""
969
+ console.print(info("Swap Delegate Stake Between Subnets"))
970
+ console.print(
971
+ "Move your delegate stake from one subnet to another. Note: Swapping uses SHARES, not TENSOR amount.\\n"
972
+ )
973
+
974
+ if from_subnet_id is None:
975
+ from_subnet_id = prompt.integer_prompt(
976
+ "Enter source subnet ID",
977
+ min_value=0,
978
+ )
979
+ if to_subnet_id is None:
980
+ to_subnet_id = prompt.integer_prompt(
981
+ "Enter destination subnet ID",
982
+ min_value=0,
983
+ )
984
+
985
+ if shares is None:
986
+ shares = prompt.integer_prompt(
987
+ "Enter shares to swap",
988
+ min_value=1,
989
+ )
990
+
991
+ return DelegateStakeSwapRequest(
992
+ from_subnet_id=from_subnet_id,
993
+ to_subnet_id=to_subnet_id,
994
+ delegate_stake_shares_to_swap=shares,
995
+ )
996
+
997
+
998
+ def prompt_delegate_transfer(
999
+ subnet_id: Optional[int] = None,
1000
+ to_account: Optional[str] = None,
1001
+ shares: Optional[int] = None,
1002
+ ) -> DelegateStakeTransferRequest:
1003
+ """Prompt for transferring delegate stake to another account."""
1004
+ console.print(info("Transfer Delegate Stake"))
1005
+ console.print("Transfer your delegate stake to another account.\n")
1006
+
1007
+ if subnet_id is None:
1008
+ subnet_id = prompt.integer_prompt(
1009
+ "Enter subnet ID",
1010
+ min_value=0,
1011
+ )
1012
+
1013
+ if to_account is None:
1014
+ to_account = prompt.text_prompt("Enter destination account (0x...)")
1015
+ if not validate_address(to_account):
1016
+ raise ValueError("Destination account must be a valid Ethereum address")
1017
+
1018
+ if shares is None:
1019
+ shares = prompt.integer_prompt(
1020
+ "Enter shares to transfer",
1021
+ min_value=1,
1022
+ )
1023
+
1024
+ # The from_account will be the signer's address
1025
+ # We set a placeholder here that will be replaced during submission
1026
+ return DelegateStakeTransferRequest(
1027
+ subnet_id=subnet_id,
1028
+ from_account="0x0000000000000000000000000000000000000000", # Placeholder
1029
+ to_account=to_account,
1030
+ delegate_stake_shares_to_transfer=shares,
1031
+ )
1032
+
1033
+
1034
+ def prompt_delegate_donate(
1035
+ subnet_id: Optional[int] = None,
1036
+ amount: Optional[float] = None,
1037
+ ) -> DelegateStakeDonateRequest:
1038
+ """Prompt for donating delegate stake to subnet treasury."""
1039
+ console.print(info("Donate Delegate Stake"))
1040
+ console.print("Donate tokens to the subnet treasury.\n")
1041
+ console.print(
1042
+ warning("⚠️ This is a one-way transfer. Donated funds cannot be recovered.\n")
1043
+ )
1044
+
1045
+ if subnet_id is None:
1046
+ subnet_id = prompt.integer_prompt(
1047
+ "Enter subnet ID",
1048
+ min_value=0,
1049
+ )
1050
+
1051
+ # Validate amount if provided (amount is in TENSOR)
1052
+ if amount is not None:
1053
+ if isinstance(amount, int):
1054
+ amount = float(amount)
1055
+ is_valid, error_msg = validate_stake_amount_prompt(amount)
1056
+ if not is_valid:
1057
+ print_error(f"Invalid amount: {error_msg}")
1058
+ amount = None
1059
+
1060
+ if amount is None:
1061
+ amount = amount_prompt(
1062
+ "Enter donation amount",
1063
+ currency="TENSOR",
1064
+ min_amount=0.000001,
1065
+ )
1066
+
1067
+ # Confirm donation
1068
+ confirmed = confirm_prompt(
1069
+ f"Confirm donation of {amount:.4f} TENSOR to subnet {subnet_id}?", default=False
1070
+ )
1071
+ if not confirmed:
1072
+ raise KeyboardInterrupt("Operation cancelled")
1073
+
1074
+ # Convert TENSOR to WEI
1075
+ amount_wei = int(amount * 1e18)
1076
+
1077
+ return DelegateStakeDonateRequest(
1078
+ subnet_id=subnet_id,
1079
+ stake_amount=amount_wei,
1080
+ )