htcli 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- src/htcli/utils/wallet/migration.py +159 -0
src/htcli/ui/prompts.py
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTCLI interactive prompts and user input handling.
|
|
3
|
+
Provides consistent prompt styles and validation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import getpass
|
|
7
|
+
from typing import Any, Callable, Optional, Union
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.prompt import Confirm, FloatPrompt, IntPrompt, Prompt
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from .display import get_console, print_error
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HTCLIPrompt:
|
|
19
|
+
"""Enhanced prompt system for HTCLI."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, console: Console = None):
|
|
22
|
+
self.console = console or get_console()
|
|
23
|
+
|
|
24
|
+
def text(
|
|
25
|
+
self,
|
|
26
|
+
message: str,
|
|
27
|
+
default: str = None,
|
|
28
|
+
placeholder: str = None,
|
|
29
|
+
validator: Callable[[str], bool] = None,
|
|
30
|
+
error_message: str = "Invalid input. Please try again.",
|
|
31
|
+
style: str = "htcli.prompt",
|
|
32
|
+
required: bool = True,
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Prompt for text input with validation."""
|
|
35
|
+
if placeholder and not default:
|
|
36
|
+
message = f"{message} [{placeholder}]"
|
|
37
|
+
|
|
38
|
+
while True:
|
|
39
|
+
try:
|
|
40
|
+
result = Prompt.ask(
|
|
41
|
+
Text(message, style=style),
|
|
42
|
+
default=default,
|
|
43
|
+
console=self.console.console,
|
|
44
|
+
)
|
|
45
|
+
# Normalize None -> empty string for easier handling
|
|
46
|
+
value = "" if result is None else str(result)
|
|
47
|
+
|
|
48
|
+
# If required, enforce non-empty input
|
|
49
|
+
if required and not value.strip():
|
|
50
|
+
print_error("Input is required.")
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# If optional and empty, return empty string to signal 'keep current'
|
|
54
|
+
if not required and not value.strip():
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
# Only run validator when user entered a non-empty value
|
|
58
|
+
if validator and value.strip() and not validator(value):
|
|
59
|
+
print_error(error_message)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
return value.strip()
|
|
63
|
+
|
|
64
|
+
except (KeyboardInterrupt, EOFError):
|
|
65
|
+
self.console.print("\n[htcli.warning]Operation cancelled.[/]")
|
|
66
|
+
raise typer.Abort()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
# Don't continue on EOF errors - they should be handled above
|
|
69
|
+
error_msg = str(e).lower()
|
|
70
|
+
if "eof" in error_msg or "end of file" in error_msg:
|
|
71
|
+
self.console.print("\n[htcli.warning]Operation cancelled.[/]")
|
|
72
|
+
raise typer.Abort()
|
|
73
|
+
# Don't loop indefinitely on errors - abort
|
|
74
|
+
print_error(f"Input error: {str(e)}" if str(e) else "Unknown error")
|
|
75
|
+
raise typer.Abort()
|
|
76
|
+
|
|
77
|
+
def password(
|
|
78
|
+
self,
|
|
79
|
+
message: str = "Password",
|
|
80
|
+
confirm: bool = False,
|
|
81
|
+
min_length: int = 0,
|
|
82
|
+
style: str = "htcli.prompt",
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Prompt for password input."""
|
|
85
|
+
while True:
|
|
86
|
+
try:
|
|
87
|
+
password = getpass.getpass(prompt=f"{message}: ")
|
|
88
|
+
|
|
89
|
+
if min_length > 0 and len(password) < min_length:
|
|
90
|
+
print_error(f"Password must be at least {min_length} characters.")
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
if confirm:
|
|
94
|
+
confirm_password = getpass.getpass(prompt="Confirm password: ")
|
|
95
|
+
if password != confirm_password:
|
|
96
|
+
print_error("Passwords do not match. Please try again.")
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
return password
|
|
100
|
+
|
|
101
|
+
except (KeyboardInterrupt, EOFError):
|
|
102
|
+
self.console.print("\n[htcli.warning]Operation cancelled.[/]")
|
|
103
|
+
raise typer.Abort()
|
|
104
|
+
|
|
105
|
+
def confirm(
|
|
106
|
+
self,
|
|
107
|
+
message: str,
|
|
108
|
+
default: bool = False,
|
|
109
|
+
style: str = "htcli.prompt",
|
|
110
|
+
) -> bool:
|
|
111
|
+
"""Prompt for yes/no confirmation."""
|
|
112
|
+
try:
|
|
113
|
+
return Confirm.ask(
|
|
114
|
+
Text(message, style=style),
|
|
115
|
+
default=default,
|
|
116
|
+
console=self.console.console,
|
|
117
|
+
)
|
|
118
|
+
except (KeyboardInterrupt, EOFError):
|
|
119
|
+
self.console.print("\n[htcli.warning]Operation cancelled.[/]")
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
def select(
|
|
123
|
+
self,
|
|
124
|
+
message: str,
|
|
125
|
+
choices: list[Union[str, tuple]],
|
|
126
|
+
default: Any = None,
|
|
127
|
+
style: str = "htcli.prompt",
|
|
128
|
+
show_choices: bool = True,
|
|
129
|
+
) -> str:
|
|
130
|
+
"""Prompt for selection from a list of choices."""
|
|
131
|
+
# Convert choices to a consistent format
|
|
132
|
+
choice_map = {}
|
|
133
|
+
display_choices = []
|
|
134
|
+
|
|
135
|
+
for i, choice in enumerate(choices):
|
|
136
|
+
if isinstance(choice, tuple):
|
|
137
|
+
key, display, value = (
|
|
138
|
+
choice if len(choice) == 3 else (*choice, choice[0])
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
key = display = value = choice
|
|
142
|
+
|
|
143
|
+
choice_map[str(i + 1)] = value
|
|
144
|
+
choice_map[key.lower()] = value
|
|
145
|
+
display_choices.append(
|
|
146
|
+
f"[htcli.value]{i + 1}[/]. [htcli.highlight]{display}[/]"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if show_choices:
|
|
150
|
+
self.console.print("\n[htcli.subtitle]Available options:[/]")
|
|
151
|
+
for choice in display_choices:
|
|
152
|
+
self.console.print(f" {choice}")
|
|
153
|
+
self.console.print()
|
|
154
|
+
|
|
155
|
+
while True:
|
|
156
|
+
try:
|
|
157
|
+
result = self.text(
|
|
158
|
+
message,
|
|
159
|
+
default=str(default) if default else None,
|
|
160
|
+
required=True,
|
|
161
|
+
style=style,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Try to match the input
|
|
165
|
+
if result in choice_map:
|
|
166
|
+
return choice_map[result]
|
|
167
|
+
elif result.lower() in choice_map:
|
|
168
|
+
return choice_map[result.lower()]
|
|
169
|
+
else:
|
|
170
|
+
print_error(
|
|
171
|
+
"Invalid selection. Please choose from the available options."
|
|
172
|
+
)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
except typer.Abort:
|
|
176
|
+
# Re-raise typer.Abort to let it propagate
|
|
177
|
+
raise
|
|
178
|
+
except (KeyboardInterrupt, EOFError):
|
|
179
|
+
self.console.print("\n[htcli.warning]Operation cancelled.[/]")
|
|
180
|
+
raise typer.Abort()
|
|
181
|
+
except Exception as e:
|
|
182
|
+
# Don't continue on EOF errors - they should be handled above
|
|
183
|
+
error_msg = str(e).lower()
|
|
184
|
+
if "eof" in error_msg or "end of file" in error_msg:
|
|
185
|
+
self.console.print("\n[htcli.warning]Operation cancelled.[/]")
|
|
186
|
+
raise typer.Abort()
|
|
187
|
+
# Check if it's a cancellation error from typer
|
|
188
|
+
if isinstance(e, typer.Abort):
|
|
189
|
+
raise
|
|
190
|
+
print_error(f"Selection error: {str(e)}")
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
def integer(
|
|
194
|
+
self,
|
|
195
|
+
message: str,
|
|
196
|
+
default: int = None,
|
|
197
|
+
min_value: int = None,
|
|
198
|
+
max_value: int = None,
|
|
199
|
+
style: str = "htcli.prompt",
|
|
200
|
+
allow_none: bool = False,
|
|
201
|
+
) -> Optional[int]:
|
|
202
|
+
"""Prompt for integer input with validation.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
message: Prompt message
|
|
206
|
+
default: Default value
|
|
207
|
+
min_value: Minimum allowed value
|
|
208
|
+
max_value: Maximum allowed value
|
|
209
|
+
style: Prompt style
|
|
210
|
+
allow_none: If True, allow empty input and return None
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
int if input provided, Optional[int] (None) if allow_none=True and input is empty
|
|
214
|
+
"""
|
|
215
|
+
while True:
|
|
216
|
+
try:
|
|
217
|
+
result = IntPrompt.ask(
|
|
218
|
+
Text(message, style=style),
|
|
219
|
+
default=default,
|
|
220
|
+
console=self.console.console,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Handle None result (empty input)
|
|
224
|
+
if result is None:
|
|
225
|
+
if allow_none:
|
|
226
|
+
return None
|
|
227
|
+
print_error("Input is required. Please enter a valid integer.")
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if min_value is not None and result < min_value:
|
|
231
|
+
print_error(f"Value must be at least {min_value}.")
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
if max_value is not None and result > max_value:
|
|
235
|
+
print_error(f"Value must be at most {max_value}.")
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
except (KeyboardInterrupt, EOFError):
|
|
241
|
+
self.console.print("\n[htcli.warning]Operation cancelled.[/]")
|
|
242
|
+
raise typer.Abort()
|
|
243
|
+
except Exception as e:
|
|
244
|
+
print_error(f"Input error: {str(e)}" if str(e) else "Unknown error")
|
|
245
|
+
raise typer.Abort()
|
|
246
|
+
|
|
247
|
+
# Backwards-compatibility helpers (legacy code still calls these)
|
|
248
|
+
def integer_prompt(
|
|
249
|
+
self,
|
|
250
|
+
message: str,
|
|
251
|
+
default: int = None,
|
|
252
|
+
min_value: int = None,
|
|
253
|
+
max_value: int = None,
|
|
254
|
+
style: str = "htcli.prompt",
|
|
255
|
+
allow_none: bool = False,
|
|
256
|
+
) -> Optional[int]:
|
|
257
|
+
"""Alias for `integer` to support existing prompt usage."""
|
|
258
|
+
return self.integer(
|
|
259
|
+
message,
|
|
260
|
+
default=default,
|
|
261
|
+
min_value=min_value,
|
|
262
|
+
max_value=max_value,
|
|
263
|
+
style=style,
|
|
264
|
+
allow_none=allow_none,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def text_prompt(
|
|
268
|
+
self,
|
|
269
|
+
message: str,
|
|
270
|
+
default: str = None,
|
|
271
|
+
placeholder: str = None,
|
|
272
|
+
validator: Callable[[str], bool] = None,
|
|
273
|
+
error_message: str = "Invalid input. Please try again.",
|
|
274
|
+
style: str = "htcli.prompt",
|
|
275
|
+
required: bool = True,
|
|
276
|
+
) -> str:
|
|
277
|
+
"""Alias for `text` to support existing prompt usage."""
|
|
278
|
+
return self.text(
|
|
279
|
+
message,
|
|
280
|
+
default=default,
|
|
281
|
+
placeholder=placeholder,
|
|
282
|
+
validator=validator,
|
|
283
|
+
error_message=error_message,
|
|
284
|
+
style=style,
|
|
285
|
+
required=required,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def float(
|
|
289
|
+
self,
|
|
290
|
+
message: str,
|
|
291
|
+
default: float = None,
|
|
292
|
+
min_value: float = None,
|
|
293
|
+
max_value: float = None,
|
|
294
|
+
style: str = "htcli.prompt",
|
|
295
|
+
) -> float:
|
|
296
|
+
"""Prompt for float input with validation."""
|
|
297
|
+
while True:
|
|
298
|
+
try:
|
|
299
|
+
result = FloatPrompt.ask(
|
|
300
|
+
Text(message, style=style),
|
|
301
|
+
default=default,
|
|
302
|
+
console=self.console.console,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if result is None:
|
|
306
|
+
if default is not None:
|
|
307
|
+
result = default
|
|
308
|
+
else:
|
|
309
|
+
print_error("Value is required.")
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
if min_value is not None and result < min_value:
|
|
313
|
+
print_error(f"Value must be at least {min_value}.")
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
if max_value is not None and result > max_value:
|
|
317
|
+
print_error(f"Value must be at most {max_value}.")
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
except (KeyboardInterrupt, EOFError):
|
|
323
|
+
self.console.print("\n[htcli.warning]Operation cancelled.[/]")
|
|
324
|
+
raise typer.Abort()
|
|
325
|
+
except Exception as e:
|
|
326
|
+
print_error(f"Input error: {str(e)}" if str(e) else "Unknown error")
|
|
327
|
+
raise typer.Abort()
|
|
328
|
+
|
|
329
|
+
def address(
|
|
330
|
+
self,
|
|
331
|
+
message: str = "Enter blockchain address",
|
|
332
|
+
default: str = None,
|
|
333
|
+
allow_empty: bool = False,
|
|
334
|
+
style: str = "htcli.prompt",
|
|
335
|
+
) -> str:
|
|
336
|
+
"""Prompt for blockchain address with validation."""
|
|
337
|
+
|
|
338
|
+
def validate_address(addr: str) -> bool:
|
|
339
|
+
if not addr and allow_empty:
|
|
340
|
+
return True
|
|
341
|
+
# Basic validation - can be enhanced
|
|
342
|
+
if len(addr) < 20:
|
|
343
|
+
return False
|
|
344
|
+
if addr.startswith("0x") and len(addr) == 42:
|
|
345
|
+
return True # EVM address
|
|
346
|
+
if len(addr) in [47, 48] and addr.startswith("5"):
|
|
347
|
+
return True # Substrate address
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
return self.text(
|
|
351
|
+
message,
|
|
352
|
+
default=default,
|
|
353
|
+
validator=validate_address,
|
|
354
|
+
error_message="Invalid blockchain address format.",
|
|
355
|
+
style=style,
|
|
356
|
+
required=not allow_empty,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def amount(
|
|
360
|
+
self,
|
|
361
|
+
message: str = "Enter amount",
|
|
362
|
+
currency: str = "TENSOR",
|
|
363
|
+
min_amount: float = 0.0,
|
|
364
|
+
max_amount: float = None,
|
|
365
|
+
default: float = None,
|
|
366
|
+
decimals: int = 6,
|
|
367
|
+
style: str = "htcli.prompt",
|
|
368
|
+
) -> float:
|
|
369
|
+
"""Prompt for token amount with validation."""
|
|
370
|
+
full_message = f"{message} ({currency})"
|
|
371
|
+
|
|
372
|
+
return self.float(
|
|
373
|
+
full_message,
|
|
374
|
+
default=default,
|
|
375
|
+
min_value=min_amount,
|
|
376
|
+
max_value=max_amount,
|
|
377
|
+
style=style,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def wallet_selection(
|
|
381
|
+
self,
|
|
382
|
+
wallets: list[dict[str, Any]],
|
|
383
|
+
message: str = "Select wallet",
|
|
384
|
+
style: str = "htcli.prompt",
|
|
385
|
+
) -> dict[str, Any]:
|
|
386
|
+
"""Prompt for wallet selection from available wallets."""
|
|
387
|
+
if not wallets:
|
|
388
|
+
raise ValueError("No wallets available for selection.")
|
|
389
|
+
|
|
390
|
+
choices = []
|
|
391
|
+
for i, wallet in enumerate(wallets):
|
|
392
|
+
name = wallet.get("name", f"Wallet {i + 1}")
|
|
393
|
+
address = wallet.get("address", "Unknown")
|
|
394
|
+
display_addr = (
|
|
395
|
+
f"{address[:8]}...{address[-8:]}" if len(address) > 16 else address
|
|
396
|
+
)
|
|
397
|
+
choices.append((str(i + 1), f"{name} ({display_addr})", wallet))
|
|
398
|
+
|
|
399
|
+
self.console.print("\n[htcli.subtitle]Available wallets:[/]")
|
|
400
|
+
for i, wallet in enumerate(wallets):
|
|
401
|
+
name = wallet.get("name", f"Wallet {i + 1}")
|
|
402
|
+
address = wallet.get("address", "Unknown")
|
|
403
|
+
balance = wallet.get("balance", "Unknown")
|
|
404
|
+
self.console.print(
|
|
405
|
+
f" [htcli.value]{i + 1}[/]. [htcli.highlight]{name}[/] "
|
|
406
|
+
f"([htcli.address]{address[:8]}...{address[-8:]}[/]) "
|
|
407
|
+
f"- [htcli.amount]{balance} TENSOR[/]"
|
|
408
|
+
)
|
|
409
|
+
self.console.print()
|
|
410
|
+
|
|
411
|
+
while True:
|
|
412
|
+
try:
|
|
413
|
+
selection = self.integer(
|
|
414
|
+
message,
|
|
415
|
+
min_value=1,
|
|
416
|
+
max_value=len(wallets),
|
|
417
|
+
style=style,
|
|
418
|
+
)
|
|
419
|
+
return wallets[selection - 1]
|
|
420
|
+
|
|
421
|
+
except (KeyboardInterrupt, EOFError):
|
|
422
|
+
self.console.print("\n[htcli.warning]Operation cancelled.[/]")
|
|
423
|
+
raise typer.Abort()
|
|
424
|
+
|
|
425
|
+
def confirmation_panel(
|
|
426
|
+
self,
|
|
427
|
+
title: str,
|
|
428
|
+
details: dict[str, Any],
|
|
429
|
+
warning: str = None,
|
|
430
|
+
require_confirmation: bool = True,
|
|
431
|
+
) -> bool:
|
|
432
|
+
"""Show a confirmation panel with transaction details."""
|
|
433
|
+
# Create content for the panel
|
|
434
|
+
content = []
|
|
435
|
+
for key, value in details.items():
|
|
436
|
+
if key.lower() in ["amount", "balance", "stake"]:
|
|
437
|
+
formatted_value = f"[htcli.amount]{value}[/]"
|
|
438
|
+
elif key.lower() in ["address", "to", "from", "account"]:
|
|
439
|
+
formatted_value = f"[htcli.address]{value}[/]"
|
|
440
|
+
elif key.lower() in ["fee", "gas"]:
|
|
441
|
+
formatted_value = f"[htcli.fee]{value}[/]"
|
|
442
|
+
else:
|
|
443
|
+
formatted_value = f"[htcli.value]{value}[/]"
|
|
444
|
+
|
|
445
|
+
content.append(f"[htcli.subtitle]{key}:[/] {formatted_value}")
|
|
446
|
+
|
|
447
|
+
panel_content = "\n".join(content)
|
|
448
|
+
|
|
449
|
+
if warning:
|
|
450
|
+
panel_content += f"\n\n[htcli.warning]⚠️ {warning}[/]"
|
|
451
|
+
|
|
452
|
+
# Display the confirmation panel
|
|
453
|
+
panel = Panel(
|
|
454
|
+
panel_content,
|
|
455
|
+
title=f"[htcli.title]{title}[/]",
|
|
456
|
+
border_style="htcli.primary",
|
|
457
|
+
padding=(1, 2),
|
|
458
|
+
)
|
|
459
|
+
self.console.console.print(panel)
|
|
460
|
+
|
|
461
|
+
if not require_confirmation:
|
|
462
|
+
return True
|
|
463
|
+
|
|
464
|
+
return self.confirm(
|
|
465
|
+
"\nProceed with this transaction?",
|
|
466
|
+
default=False,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# Global prompt instance
|
|
471
|
+
_prompt = HTCLIPrompt()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Convenience functions
|
|
475
|
+
def text_prompt(
|
|
476
|
+
message: str, default: str = None, validator: Callable[[str], bool] = None, **kwargs
|
|
477
|
+
) -> str:
|
|
478
|
+
"""Quick text prompt."""
|
|
479
|
+
return _prompt.text(message, default=default, validator=validator, **kwargs)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def password_prompt(
|
|
483
|
+
message: str = "Password",
|
|
484
|
+
confirm: bool = False,
|
|
485
|
+
min_length: int = 8,
|
|
486
|
+
) -> str:
|
|
487
|
+
"""Quick password prompt."""
|
|
488
|
+
return _prompt.password(message, confirm=confirm, min_length=min_length)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def confirm_prompt(message: str, default: bool = False) -> bool:
|
|
492
|
+
"""Quick confirmation prompt."""
|
|
493
|
+
return _prompt.confirm(message, default=default)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def integer_prompt(
|
|
497
|
+
message: str,
|
|
498
|
+
default: int = None,
|
|
499
|
+
min_value: int = None,
|
|
500
|
+
max_value: int = None,
|
|
501
|
+
allow_none: bool = False,
|
|
502
|
+
) -> Optional[int]:
|
|
503
|
+
"""Quick integer prompt.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
message: Prompt message
|
|
507
|
+
default: Default value
|
|
508
|
+
min_value: Minimum allowed value
|
|
509
|
+
max_value: Maximum allowed value
|
|
510
|
+
allow_none: If True, allow empty input and return None
|
|
511
|
+
"""
|
|
512
|
+
return _prompt.integer(
|
|
513
|
+
message,
|
|
514
|
+
default=default,
|
|
515
|
+
min_value=min_value,
|
|
516
|
+
max_value=max_value,
|
|
517
|
+
allow_none=allow_none,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def select_prompt(
|
|
522
|
+
message: str,
|
|
523
|
+
choices: list[Union[str, tuple]],
|
|
524
|
+
default: Any = None,
|
|
525
|
+
) -> str:
|
|
526
|
+
"""Quick selection prompt."""
|
|
527
|
+
return _prompt.select(message, choices, default=default)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def amount_prompt(
|
|
531
|
+
message: str = "Enter amount",
|
|
532
|
+
currency: str = "TENSOR",
|
|
533
|
+
min_amount: float = 0.0,
|
|
534
|
+
max_amount: float = None,
|
|
535
|
+
default: float = None,
|
|
536
|
+
) -> float:
|
|
537
|
+
"""Quick amount prompt."""
|
|
538
|
+
return _prompt.amount(message, currency, min_amount, max_amount, default=default)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def address_prompt(
|
|
542
|
+
message: str = "Enter blockchain address",
|
|
543
|
+
default: str = None,
|
|
544
|
+
allow_empty: bool = False,
|
|
545
|
+
) -> str:
|
|
546
|
+
"""Quick address prompt."""
|
|
547
|
+
return _prompt.address(message, default=default, allow_empty=allow_empty)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def wallet_selection_prompt(
|
|
551
|
+
wallets: list[dict[str, Any]],
|
|
552
|
+
message: str = "Select wallet",
|
|
553
|
+
) -> dict[str, Any]:
|
|
554
|
+
"""Quick wallet selection prompt."""
|
|
555
|
+
return _prompt.wallet_selection(wallets, message)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def confirmation_panel_prompt(
|
|
559
|
+
title: str,
|
|
560
|
+
details: dict[str, Any],
|
|
561
|
+
warning: str = None,
|
|
562
|
+
require_confirmation: bool = True,
|
|
563
|
+
) -> bool:
|
|
564
|
+
"""Quick confirmation panel prompt."""
|
|
565
|
+
return _prompt.confirmation_panel(title, details, warning, require_confirmation)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
# Additional utility functions for complex prompting scenarios
|
|
569
|
+
def prompt_with_validation(
|
|
570
|
+
param_name: str,
|
|
571
|
+
provided_value: Optional[Any],
|
|
572
|
+
param_type: type,
|
|
573
|
+
validator: Optional[Callable[[Any], tuple[bool, Optional[str]]]] = None,
|
|
574
|
+
prompt_func: Optional[Callable] = None,
|
|
575
|
+
**prompt_kwargs
|
|
576
|
+
) -> Any:
|
|
577
|
+
"""
|
|
578
|
+
Helper function that validates provided value and prompts if invalid or None.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
param_name: Name of the parameter
|
|
582
|
+
provided_value: Value provided via CLI (may be None)
|
|
583
|
+
param_type: Expected type (str, int, float, bool)
|
|
584
|
+
validator: Optional validation function that returns (bool, Optional[str])
|
|
585
|
+
prompt_func: Function to call for prompting (defaults to prompt_for_required)
|
|
586
|
+
**prompt_kwargs: Additional arguments to pass to prompt function
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Validated value from provided_value or from interactive prompt
|
|
590
|
+
"""
|
|
591
|
+
# Validate provided value if it exists
|
|
592
|
+
if provided_value is not None and validator:
|
|
593
|
+
is_valid, error_msg = validator(provided_value)
|
|
594
|
+
if not is_valid:
|
|
595
|
+
print_error(f"Invalid {param_name}: {error_msg}")
|
|
596
|
+
provided_value = None # Force interactive prompt
|
|
597
|
+
|
|
598
|
+
# Prompt if None (either originally or after failed validation)
|
|
599
|
+
if provided_value is None:
|
|
600
|
+
if prompt_func:
|
|
601
|
+
return prompt_func(param_name, param_type=param_type, **prompt_kwargs)
|
|
602
|
+
else:
|
|
603
|
+
# Default to prompt_for_required
|
|
604
|
+
return prompt_for_required(
|
|
605
|
+
param_name, param_type=param_type, **prompt_kwargs
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
return provided_value
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def prompt_for_required(
|
|
612
|
+
param_name: str,
|
|
613
|
+
param_type: type,
|
|
614
|
+
help_text: str = "",
|
|
615
|
+
validator: Optional[Callable] = None,
|
|
616
|
+
default: Optional[Any] = None,
|
|
617
|
+
allow_empty: bool = False,
|
|
618
|
+
provided_value: Optional[Any] = None,
|
|
619
|
+
) -> Any:
|
|
620
|
+
"""
|
|
621
|
+
Prompt user for a required parameter value.
|
|
622
|
+
|
|
623
|
+
If provided_value is given and validator is provided, validates it first.
|
|
624
|
+
If validation fails, prompts interactively.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
param_name: Name of the parameter
|
|
628
|
+
param_type: Expected type (str, int, float, bool)
|
|
629
|
+
help_text: Help text to display
|
|
630
|
+
validator: Optional validation function (returns bool or (bool, Optional[str]))
|
|
631
|
+
default: Default value to suggest
|
|
632
|
+
allow_empty: Whether empty values are allowed
|
|
633
|
+
provided_value: Value provided via CLI (validated if validator is provided)
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
The user input converted to the specified type
|
|
637
|
+
"""
|
|
638
|
+
# Validate provided value if it exists
|
|
639
|
+
if provided_value is not None and validator:
|
|
640
|
+
try:
|
|
641
|
+
result = validator(provided_value)
|
|
642
|
+
# Handle both bool and tuple return types
|
|
643
|
+
if isinstance(result, tuple):
|
|
644
|
+
is_valid, error_msg = result
|
|
645
|
+
if not is_valid:
|
|
646
|
+
error_display = error_msg if error_msg else f"Invalid {param_name}: {provided_value}"
|
|
647
|
+
print_error(f"Invalid {param_name}: {error_display}")
|
|
648
|
+
provided_value = None # Force interactive prompt
|
|
649
|
+
else:
|
|
650
|
+
# Boolean return type
|
|
651
|
+
if not result:
|
|
652
|
+
print_error(f"Invalid {param_name}: {provided_value}")
|
|
653
|
+
provided_value = None # Force interactive prompt
|
|
654
|
+
except Exception as e:
|
|
655
|
+
print_error(f"Validation error for {param_name}: {str(e)}")
|
|
656
|
+
provided_value = None # Force interactive prompt
|
|
657
|
+
|
|
658
|
+
# If we have a valid provided value, return it
|
|
659
|
+
if provided_value is not None:
|
|
660
|
+
return provided_value
|
|
661
|
+
|
|
662
|
+
while True:
|
|
663
|
+
# Build the prompt text
|
|
664
|
+
prompt_text = param_name
|
|
665
|
+
if help_text:
|
|
666
|
+
prompt_text += f" ({help_text})"
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
if param_type == str:
|
|
670
|
+
result = _prompt.text(
|
|
671
|
+
prompt_text,
|
|
672
|
+
default=str(default) if default is not None else None,
|
|
673
|
+
validator=validator,
|
|
674
|
+
required=not allow_empty,
|
|
675
|
+
)
|
|
676
|
+
return result if result else (None if allow_empty else "")
|
|
677
|
+
elif param_type == int:
|
|
678
|
+
return _prompt.integer(
|
|
679
|
+
prompt_text,
|
|
680
|
+
default=default,
|
|
681
|
+
)
|
|
682
|
+
elif param_type == float:
|
|
683
|
+
return _prompt.float(
|
|
684
|
+
prompt_text,
|
|
685
|
+
default=default,
|
|
686
|
+
)
|
|
687
|
+
elif param_type == bool:
|
|
688
|
+
return _prompt.confirm(
|
|
689
|
+
prompt_text,
|
|
690
|
+
default=default if default is not None else False,
|
|
691
|
+
)
|
|
692
|
+
else:
|
|
693
|
+
result = _prompt.text(
|
|
694
|
+
prompt_text,
|
|
695
|
+
default=str(default) if default is not None else None,
|
|
696
|
+
validator=validator,
|
|
697
|
+
required=not allow_empty,
|
|
698
|
+
)
|
|
699
|
+
return result
|
|
700
|
+
except (KeyboardInterrupt, EOFError, typer.Abort):
|
|
701
|
+
# Re-raise cancellation exceptions to properly exit
|
|
702
|
+
raise
|
|
703
|
+
except Exception as e:
|
|
704
|
+
print_error(f"Input error: {str(e)}")
|
|
705
|
+
continue
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def prompt_for_optional(
|
|
709
|
+
param_name: str,
|
|
710
|
+
param_type: type,
|
|
711
|
+
help_text: str = "",
|
|
712
|
+
validator: Optional[Callable] = None,
|
|
713
|
+
default: Optional[Any] = None,
|
|
714
|
+
provided_value: Optional[Any] = None,
|
|
715
|
+
) -> Optional[Any]:
|
|
716
|
+
"""
|
|
717
|
+
Prompt user for an optional parameter value.
|
|
718
|
+
|
|
719
|
+
If provided_value is given and validator is provided, validates it first.
|
|
720
|
+
If validation fails, prompts interactively.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
param_name: Name of the parameter
|
|
724
|
+
param_type: Expected type (str, int, float, bool)
|
|
725
|
+
help_text: Help text to display
|
|
726
|
+
validator: Optional validation function (returns bool or (bool, Optional[str]))
|
|
727
|
+
default: Default value to suggest
|
|
728
|
+
provided_value: Value provided via CLI (validated if validator is provided)
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
The user input converted to the specified type, or None if skipped
|
|
732
|
+
"""
|
|
733
|
+
# Validate provided value if it exists
|
|
734
|
+
if provided_value is not None and validator:
|
|
735
|
+
try:
|
|
736
|
+
result = validator(provided_value)
|
|
737
|
+
# Handle both bool and tuple return types
|
|
738
|
+
if isinstance(result, tuple):
|
|
739
|
+
is_valid, error_msg = result
|
|
740
|
+
if not is_valid:
|
|
741
|
+
error_display = error_msg if error_msg else f"Invalid {param_name}: {provided_value}"
|
|
742
|
+
print_error(f"Invalid {param_name}: {error_display}")
|
|
743
|
+
provided_value = None # Force interactive prompt
|
|
744
|
+
else:
|
|
745
|
+
# Boolean return type
|
|
746
|
+
if not result:
|
|
747
|
+
print_error(f"Invalid {param_name}: {provided_value}")
|
|
748
|
+
provided_value = None # Force interactive prompt
|
|
749
|
+
except Exception as e:
|
|
750
|
+
print_error(f"Validation error for {param_name}: {str(e)}")
|
|
751
|
+
provided_value = None # Force interactive prompt
|
|
752
|
+
|
|
753
|
+
# If we have a valid provided value, return it
|
|
754
|
+
if provided_value is not None:
|
|
755
|
+
return provided_value
|
|
756
|
+
|
|
757
|
+
prompt_text = f"{param_name} (optional)"
|
|
758
|
+
if help_text:
|
|
759
|
+
prompt_text += f" ({help_text})"
|
|
760
|
+
|
|
761
|
+
try:
|
|
762
|
+
if param_type == str:
|
|
763
|
+
result = _prompt.text(
|
|
764
|
+
prompt_text,
|
|
765
|
+
default=str(default) if default is not None else None,
|
|
766
|
+
required=False,
|
|
767
|
+
)
|
|
768
|
+
return result if result and result.strip() else None
|
|
769
|
+
elif param_type == int:
|
|
770
|
+
return _prompt.integer(
|
|
771
|
+
prompt_text,
|
|
772
|
+
default=default,
|
|
773
|
+
)
|
|
774
|
+
elif param_type == float:
|
|
775
|
+
return _prompt.float(
|
|
776
|
+
prompt_text,
|
|
777
|
+
default=default,
|
|
778
|
+
)
|
|
779
|
+
elif param_type == bool:
|
|
780
|
+
return _prompt.confirm(
|
|
781
|
+
prompt_text,
|
|
782
|
+
default=default if default is not None else False,
|
|
783
|
+
)
|
|
784
|
+
else:
|
|
785
|
+
result = _prompt.text(
|
|
786
|
+
prompt_text,
|
|
787
|
+
default=str(default) if default is not None else None,
|
|
788
|
+
required=False,
|
|
789
|
+
)
|
|
790
|
+
return result if result.strip() else None
|
|
791
|
+
except (KeyboardInterrupt, EOFError, typer.Abort):
|
|
792
|
+
# Re-raise cancellation exceptions to properly exit
|
|
793
|
+
raise
|
|
794
|
+
except Exception:
|
|
795
|
+
return None
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def prompt_for_list(
|
|
799
|
+
param_name: str,
|
|
800
|
+
help_text: str,
|
|
801
|
+
separator: str = ",",
|
|
802
|
+
validator: Optional[Callable[[Any], bool]] = None,
|
|
803
|
+
default: Optional[str] = None,
|
|
804
|
+
) -> list[str]:
|
|
805
|
+
"""
|
|
806
|
+
Prompt user for a list of values.
|
|
807
|
+
|
|
808
|
+
Args:
|
|
809
|
+
param_name: Name of the parameter
|
|
810
|
+
help_text: Help text to display
|
|
811
|
+
separator: Character to separate list items
|
|
812
|
+
validator: Optional validation function for individual items
|
|
813
|
+
default: Default value to suggest
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
List of user input values
|
|
817
|
+
"""
|
|
818
|
+
prompt_text = param_name
|
|
819
|
+
if help_text:
|
|
820
|
+
prompt_text += f" ({help_text})"
|
|
821
|
+
prompt_text += f" (separate with '{separator}')"
|
|
822
|
+
|
|
823
|
+
while True:
|
|
824
|
+
try:
|
|
825
|
+
user_input = _prompt.text(
|
|
826
|
+
prompt_text,
|
|
827
|
+
default=default,
|
|
828
|
+
required=True,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
# Split the input
|
|
832
|
+
items = [
|
|
833
|
+
item.strip() for item in user_input.split(separator) if item.strip()
|
|
834
|
+
]
|
|
835
|
+
|
|
836
|
+
if not items:
|
|
837
|
+
print_error("No valid items found. Please provide at least one value.")
|
|
838
|
+
continue
|
|
839
|
+
|
|
840
|
+
# Validate individual items if validator provided
|
|
841
|
+
if validator:
|
|
842
|
+
invalid_items = [item for item in items if not validator(item)]
|
|
843
|
+
if invalid_items:
|
|
844
|
+
print_error(f"Invalid items: {', '.join(invalid_items)}")
|
|
845
|
+
continue
|
|
846
|
+
|
|
847
|
+
return items
|
|
848
|
+
except (KeyboardInterrupt, EOFError, typer.Abort):
|
|
849
|
+
# Re-raise cancellation exceptions to properly exit
|
|
850
|
+
raise
|
|
851
|
+
except Exception as e:
|
|
852
|
+
print_error(f"Input error: {str(e)}")
|
|
853
|
+
continue
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def prompt_for_confirmation(
|
|
857
|
+
message: str,
|
|
858
|
+
default: bool = False,
|
|
859
|
+
) -> bool:
|
|
860
|
+
"""
|
|
861
|
+
Prompt user for confirmation.
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
message: Message to display
|
|
865
|
+
default: Default value
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
True if user confirms, False otherwise
|
|
869
|
+
"""
|
|
870
|
+
return _prompt.confirm(message, default=default)
|