hippius 0.2.2__py3-none-any.whl → 0.2.4__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.
- {hippius-0.2.2.dist-info → hippius-0.2.4.dist-info}/METADATA +213 -156
- hippius-0.2.4.dist-info/RECORD +16 -0
- hippius_sdk/__init__.py +10 -21
- hippius_sdk/cli.py +282 -2627
- hippius_sdk/cli_assets.py +10 -0
- hippius_sdk/cli_handlers.py +2773 -0
- hippius_sdk/cli_parser.py +607 -0
- hippius_sdk/cli_rich.py +247 -0
- hippius_sdk/client.py +70 -22
- hippius_sdk/config.py +109 -142
- hippius_sdk/ipfs.py +435 -58
- hippius_sdk/ipfs_core.py +22 -1
- hippius_sdk/substrate.py +234 -553
- hippius_sdk/utils.py +84 -2
- hippius-0.2.2.dist-info/RECORD +0 -12
- {hippius-0.2.2.dist-info → hippius-0.2.4.dist-info}/WHEEL +0 -0
- {hippius-0.2.2.dist-info → hippius-0.2.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,2773 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Command Line Interface handlers for Hippius SDK.
|
4
|
+
|
5
|
+
This module provides handler functions for CLI commands, including
|
6
|
+
file operations, marketplace interactions, configuration management, etc.
|
7
|
+
"""
|
8
|
+
import asyncio
|
9
|
+
import base64
|
10
|
+
import getpass
|
11
|
+
import json
|
12
|
+
import os
|
13
|
+
import tempfile
|
14
|
+
import time
|
15
|
+
from typing import Any, List, Optional
|
16
|
+
|
17
|
+
from hippius_sdk import (
|
18
|
+
HippiusClient,
|
19
|
+
decrypt_seed_phrase,
|
20
|
+
delete_account,
|
21
|
+
encrypt_seed_phrase,
|
22
|
+
format_size,
|
23
|
+
get_account_address,
|
24
|
+
get_active_account,
|
25
|
+
get_all_config,
|
26
|
+
get_config_value,
|
27
|
+
list_accounts,
|
28
|
+
load_config,
|
29
|
+
reset_config,
|
30
|
+
save_config,
|
31
|
+
set_active_account,
|
32
|
+
set_config_value,
|
33
|
+
set_seed_phrase,
|
34
|
+
)
|
35
|
+
from hippius_sdk.cli_parser import get_default_address
|
36
|
+
from hippius_sdk.cli_rich import (
|
37
|
+
console,
|
38
|
+
create_progress,
|
39
|
+
error,
|
40
|
+
info,
|
41
|
+
log,
|
42
|
+
print_panel,
|
43
|
+
print_table,
|
44
|
+
success,
|
45
|
+
warning,
|
46
|
+
)
|
47
|
+
|
48
|
+
try:
|
49
|
+
import nacl.secret
|
50
|
+
import nacl.utils
|
51
|
+
except ImportError:
|
52
|
+
ENCRYPTION_AVAILABLE = False
|
53
|
+
else:
|
54
|
+
ENCRYPTION_AVAILABLE = True
|
55
|
+
|
56
|
+
|
57
|
+
# Client creation helper function
|
58
|
+
def create_client(args: Any) -> HippiusClient:
|
59
|
+
"""Create a HippiusClient instance from command line arguments."""
|
60
|
+
# Process encryption flags
|
61
|
+
encrypt = None
|
62
|
+
if hasattr(args, "encrypt") and args.encrypt:
|
63
|
+
encrypt = True
|
64
|
+
elif hasattr(args, "no_encrypt") and args.no_encrypt:
|
65
|
+
encrypt = False
|
66
|
+
|
67
|
+
# Process encryption key if provided
|
68
|
+
encryption_key = None
|
69
|
+
if hasattr(args, "encryption_key") and args.encryption_key:
|
70
|
+
try:
|
71
|
+
encryption_key = base64.b64decode(args.encryption_key)
|
72
|
+
if hasattr(args, "verbose") and args.verbose:
|
73
|
+
print("Using provided encryption key")
|
74
|
+
except Exception as e:
|
75
|
+
print(f"Warning: Could not decode encryption key: {e}")
|
76
|
+
print("Using default encryption key from configuration if available")
|
77
|
+
|
78
|
+
# Get API URL based on local_ipfs flag if the flag exists
|
79
|
+
api_url = None
|
80
|
+
if hasattr(args, "local_ipfs") and args.local_ipfs:
|
81
|
+
api_url = "http://localhost:5001"
|
82
|
+
elif hasattr(args, "api_url"):
|
83
|
+
api_url = args.api_url
|
84
|
+
elif hasattr(args, "ipfs_api"):
|
85
|
+
api_url = args.ipfs_api
|
86
|
+
|
87
|
+
# Get gateway URL
|
88
|
+
gateway = None
|
89
|
+
if hasattr(args, "gateway"):
|
90
|
+
gateway = args.gateway
|
91
|
+
elif hasattr(args, "ipfs_gateway"):
|
92
|
+
gateway = args.ipfs_gateway
|
93
|
+
|
94
|
+
# Get substrate URL
|
95
|
+
substrate_url = args.substrate_url if hasattr(args, "substrate_url") else None
|
96
|
+
|
97
|
+
# Skip password if we're doing erasure-code with --no-publish
|
98
|
+
# This avoids prompting for password when we don't need to interact with the blockchain
|
99
|
+
password = None
|
100
|
+
if (
|
101
|
+
hasattr(args, "command")
|
102
|
+
and args.command == "erasure-code"
|
103
|
+
and hasattr(args, "no_publish")
|
104
|
+
and args.no_publish
|
105
|
+
):
|
106
|
+
# Don't need a password in this case
|
107
|
+
password = None
|
108
|
+
else:
|
109
|
+
# Use password from args if provided
|
110
|
+
password = args.password if hasattr(args, "password") else None
|
111
|
+
|
112
|
+
# Initialize client with provided parameters
|
113
|
+
client = HippiusClient(
|
114
|
+
ipfs_gateway=gateway,
|
115
|
+
ipfs_api_url=api_url,
|
116
|
+
substrate_url=substrate_url,
|
117
|
+
substrate_seed_phrase=(
|
118
|
+
args.seed_phrase if hasattr(args, "seed_phrase") else None
|
119
|
+
),
|
120
|
+
seed_phrase_password=password,
|
121
|
+
account_name=args.account if hasattr(args, "account") else None,
|
122
|
+
encrypt_by_default=encrypt,
|
123
|
+
encryption_key=encryption_key,
|
124
|
+
)
|
125
|
+
|
126
|
+
return client
|
127
|
+
|
128
|
+
|
129
|
+
#
|
130
|
+
# IPFS File Operation Handlers
|
131
|
+
#
|
132
|
+
|
133
|
+
|
134
|
+
async def handle_download(
|
135
|
+
client: HippiusClient, cid: str, output_path: str, decrypt: Optional[bool] = None
|
136
|
+
) -> int:
|
137
|
+
"""Handle the download command"""
|
138
|
+
info(f"Downloading [bold cyan]{cid}[/bold cyan] to [bold]{output_path}[/bold]...")
|
139
|
+
|
140
|
+
# Use the enhanced download method which returns formatted information
|
141
|
+
result = await client.download_file(cid, output_path, decrypt=decrypt)
|
142
|
+
|
143
|
+
# Create a success panel with download information
|
144
|
+
details = [
|
145
|
+
f"Download successful in [bold green]{result['elapsed_seconds']}[/bold green] seconds!",
|
146
|
+
f"Saved to: [bold]{result['output_path']}[/bold]",
|
147
|
+
f"Size: [bold cyan]{result['size_bytes']:,}[/bold cyan] bytes ([bold cyan]{result['size_formatted']}[/bold cyan])",
|
148
|
+
]
|
149
|
+
|
150
|
+
if result.get("decrypted"):
|
151
|
+
details.append("[bold yellow]File was decrypted during download[/bold yellow]")
|
152
|
+
|
153
|
+
print_panel("\n".join(details), title="Download Complete")
|
154
|
+
|
155
|
+
return 0
|
156
|
+
|
157
|
+
|
158
|
+
async def handle_exists(client: HippiusClient, cid: str) -> int:
|
159
|
+
"""Handle the exists command"""
|
160
|
+
info(f"Checking if CID [bold cyan]{cid}[/bold cyan] exists on IPFS...")
|
161
|
+
result = await client.exists(cid)
|
162
|
+
|
163
|
+
# Use the formatted CID from the result
|
164
|
+
formatted_cid = result["formatted_cid"]
|
165
|
+
exists = result["exists"]
|
166
|
+
|
167
|
+
if exists:
|
168
|
+
success(f"CID [bold cyan]{formatted_cid}[/bold cyan] exists on IPFS")
|
169
|
+
|
170
|
+
if result.get("gateway_url"):
|
171
|
+
log(f"Gateway URL: [link]{result['gateway_url']}[/link]")
|
172
|
+
|
173
|
+
# Display download command in a panel
|
174
|
+
command = f"[bold green underline]hippius download {formatted_cid} <output_path>[/bold green underline]"
|
175
|
+
print_panel(command, title="Download Command")
|
176
|
+
else:
|
177
|
+
error(f"CID [bold cyan]{formatted_cid}[/bold cyan] does not exist on IPFS")
|
178
|
+
|
179
|
+
return 0
|
180
|
+
|
181
|
+
|
182
|
+
async def handle_cat(
|
183
|
+
client: HippiusClient, cid: str, max_size: int, decrypt: Optional[bool] = None
|
184
|
+
) -> int:
|
185
|
+
"""Handle the cat command"""
|
186
|
+
info(f"Displaying content of CID [bold cyan]{cid}[/bold cyan]...")
|
187
|
+
with tempfile.NamedTemporaryFile() as temp:
|
188
|
+
temp_path = temp.name
|
189
|
+
download_result = await client.download_file(cid, temp_path, decrypt=decrypt)
|
190
|
+
file_size = os.path.getsize(temp_path)
|
191
|
+
|
192
|
+
# Read content based on max size
|
193
|
+
with open(temp_path, "rb") as f:
|
194
|
+
content = f.read(max_size)
|
195
|
+
|
196
|
+
# Try to display as text, fall back to binary info
|
197
|
+
try:
|
198
|
+
decoded = content.decode("utf-8")
|
199
|
+
log(
|
200
|
+
f"\nContent (first [bold]{min(max_size, file_size):,}[/bold] bytes):",
|
201
|
+
style="blue",
|
202
|
+
)
|
203
|
+
console.print("--------------------------------------------", style="dim")
|
204
|
+
console.print(decoded)
|
205
|
+
console.print("--------------------------------------------", style="dim")
|
206
|
+
except UnicodeDecodeError:
|
207
|
+
log("\nBinary content (showing size information only):", style="yellow")
|
208
|
+
log(
|
209
|
+
f"Total size: [bold cyan]{file_size:,}[/bold cyan] bytes ([bold cyan]{download_result['size_formatted']}[/bold cyan])"
|
210
|
+
)
|
211
|
+
log("Content type appears to be binary", style="yellow")
|
212
|
+
|
213
|
+
notes = []
|
214
|
+
if file_size > max_size:
|
215
|
+
notes.append(
|
216
|
+
f"Content truncated. Total file size: [bold]{file_size:,}[/bold] bytes"
|
217
|
+
)
|
218
|
+
notes.append(
|
219
|
+
f"Use '[bold]hippius download {cid} <output_path>[/bold]' to download the entire file"
|
220
|
+
)
|
221
|
+
|
222
|
+
if download_result.get("decrypted"):
|
223
|
+
notes.append(
|
224
|
+
"[bold yellow]File was decrypted during download[/bold yellow]"
|
225
|
+
)
|
226
|
+
|
227
|
+
if notes:
|
228
|
+
print_panel("\n".join(notes), title="Notes")
|
229
|
+
|
230
|
+
|
231
|
+
async def handle_store(
|
232
|
+
client: HippiusClient,
|
233
|
+
file_path: str,
|
234
|
+
miner_ids: Optional[List[str]] = None,
|
235
|
+
encrypt: Optional[bool] = None,
|
236
|
+
) -> int:
|
237
|
+
"""Handle the store command (upload file to IPFS and store on Substrate)"""
|
238
|
+
if not os.path.exists(file_path):
|
239
|
+
error(f"File [bold]{file_path}[/bold] does not exist")
|
240
|
+
return 1
|
241
|
+
|
242
|
+
if not os.path.isfile(file_path):
|
243
|
+
error(f"[bold]{file_path}[/bold] is not a file")
|
244
|
+
return 1
|
245
|
+
|
246
|
+
# Get file size for display
|
247
|
+
file_size = os.path.getsize(file_path)
|
248
|
+
file_name = os.path.basename(file_path)
|
249
|
+
|
250
|
+
# Format size for display
|
251
|
+
if file_size >= 1024 * 1024:
|
252
|
+
size_formatted = f"{file_size / (1024 * 1024):.2f} MB"
|
253
|
+
else:
|
254
|
+
size_formatted = f"{file_size / 1024:.2f} KB"
|
255
|
+
|
256
|
+
# Upload information panel
|
257
|
+
upload_info = [
|
258
|
+
f"File: [bold]{file_name}[/bold]",
|
259
|
+
f"Size: [bold cyan]{size_formatted}[/bold cyan] ({file_size:,} bytes)",
|
260
|
+
]
|
261
|
+
|
262
|
+
# Add encryption status
|
263
|
+
if encrypt is True:
|
264
|
+
upload_info.append("[bold green]Encryption: Enabled[/bold green]")
|
265
|
+
elif encrypt is False:
|
266
|
+
upload_info.append("[bold red]Encryption: Disabled[/bold red]")
|
267
|
+
else:
|
268
|
+
upload_info.append(
|
269
|
+
"[bold yellow]Encryption: Using default setting[/bold yellow]"
|
270
|
+
)
|
271
|
+
|
272
|
+
# Parse miner IDs if provided
|
273
|
+
miner_id_list = None
|
274
|
+
if miner_ids:
|
275
|
+
miner_id_list = [m.strip() for m in miner_ids if m.strip()]
|
276
|
+
upload_info.append(
|
277
|
+
f"Targeting [bold]{len(miner_id_list)}[/bold] miners for storage"
|
278
|
+
)
|
279
|
+
|
280
|
+
# Display upload information panel
|
281
|
+
print_panel("\n".join(upload_info), title="Upload Operation")
|
282
|
+
|
283
|
+
# Create progress for the upload process
|
284
|
+
with create_progress() as progress:
|
285
|
+
# Add a task for the upload
|
286
|
+
task = progress.add_task("[cyan]Uploading...", total=100)
|
287
|
+
|
288
|
+
# We can't track actual progress from the client.store_file method yet,
|
289
|
+
# so we'll update the progress periodically
|
290
|
+
start_time = time.time()
|
291
|
+
|
292
|
+
# Create a task to update the progress while waiting for the upload
|
293
|
+
async def update_progress():
|
294
|
+
while not progress.finished:
|
295
|
+
# Since we don't have actual progress data, we'll use time as a proxy
|
296
|
+
# The progress will move faster at first, then slow down
|
297
|
+
elapsed = time.time() - start_time
|
298
|
+
# Use a logarithmic function to simulate progress
|
299
|
+
# This is just an estimation and not actual progress
|
300
|
+
pct = min(95, 100 * (1 - 1 / (1 + elapsed / 10)))
|
301
|
+
progress.update(task, completed=pct)
|
302
|
+
await asyncio.sleep(0.1)
|
303
|
+
|
304
|
+
# Start the progress updater task
|
305
|
+
updater = asyncio.create_task(update_progress())
|
306
|
+
|
307
|
+
try:
|
308
|
+
# Use the store_file method
|
309
|
+
result = await client.upload_file(
|
310
|
+
file_path=file_path,
|
311
|
+
encrypt=encrypt,
|
312
|
+
# miner_ids=miner_id_list
|
313
|
+
)
|
314
|
+
|
315
|
+
progress.update(task, completed=100)
|
316
|
+
updater.cancel()
|
317
|
+
|
318
|
+
elapsed_time = time.time() - start_time
|
319
|
+
|
320
|
+
# Success panel with results
|
321
|
+
success_info = [
|
322
|
+
f"Upload completed in [bold green]{elapsed_time:.2f}[/bold green] seconds!",
|
323
|
+
f"IPFS CID: [bold cyan]{result['cid']}[/bold cyan]",
|
324
|
+
]
|
325
|
+
|
326
|
+
if result.get("gateway_url"):
|
327
|
+
success_info.append(
|
328
|
+
f"Gateway URL: [link]{result['gateway_url']}[/link]"
|
329
|
+
)
|
330
|
+
|
331
|
+
if result.get("encrypted"):
|
332
|
+
success_info.append(
|
333
|
+
"[bold yellow]File was encrypted during upload[/bold yellow]"
|
334
|
+
)
|
335
|
+
|
336
|
+
print_panel("\n".join(success_info), title="Upload Successful")
|
337
|
+
|
338
|
+
# If we stored in the marketplace
|
339
|
+
if "transaction_hash" in result:
|
340
|
+
log(
|
341
|
+
f"\nStored in marketplace. Transaction hash: [bold]{result['transaction_hash']}[/bold]"
|
342
|
+
)
|
343
|
+
|
344
|
+
# Display download command in a panel
|
345
|
+
command = f"[bold green underline]hippius download {result['cid']} <output_path>[/bold green underline]"
|
346
|
+
print_panel(command, title="Download Command")
|
347
|
+
|
348
|
+
return 0
|
349
|
+
|
350
|
+
except Exception as e:
|
351
|
+
# Cancel the updater task in case of error
|
352
|
+
updater.cancel()
|
353
|
+
error(f"Upload failed: {str(e)}")
|
354
|
+
|
355
|
+
|
356
|
+
async def handle_store_dir(
|
357
|
+
client: HippiusClient,
|
358
|
+
dir_path: str,
|
359
|
+
miner_ids: Optional[List[str]] = None,
|
360
|
+
encrypt: Optional[bool] = None,
|
361
|
+
) -> int:
|
362
|
+
"""Handle the store directory command"""
|
363
|
+
if not os.path.exists(dir_path):
|
364
|
+
error(f"Directory [bold]{dir_path}[/bold] does not exist")
|
365
|
+
return 1
|
366
|
+
|
367
|
+
if not os.path.isdir(dir_path):
|
368
|
+
error(f"[bold]{dir_path}[/bold] is not a directory")
|
369
|
+
return 1
|
370
|
+
|
371
|
+
# Upload information panel
|
372
|
+
upload_info = [f"Directory: [bold]{dir_path}[/bold]"]
|
373
|
+
|
374
|
+
# Add encryption status
|
375
|
+
if encrypt is True:
|
376
|
+
upload_info.append("[bold green]Encryption: Enabled[/bold green]")
|
377
|
+
elif encrypt is False:
|
378
|
+
upload_info.append("[bold red]Encryption: Disabled[/bold red]")
|
379
|
+
else:
|
380
|
+
upload_info.append(
|
381
|
+
"[bold yellow]Encryption: Using default setting[/bold yellow]"
|
382
|
+
)
|
383
|
+
|
384
|
+
# Parse miner IDs if provided
|
385
|
+
miner_id_list = None
|
386
|
+
if miner_ids:
|
387
|
+
miner_id_list = [m.strip() for m in miner_ids if m.strip()]
|
388
|
+
upload_info.append(
|
389
|
+
f"Targeting [bold]{len(miner_id_list)}[/bold] miners for storage"
|
390
|
+
)
|
391
|
+
|
392
|
+
# Display upload information panel
|
393
|
+
print_panel("\n".join(upload_info), title="Directory Upload Operation")
|
394
|
+
|
395
|
+
# Create progress for the directory upload process
|
396
|
+
with create_progress() as progress:
|
397
|
+
# Add a task for the directory upload
|
398
|
+
task = progress.add_task("[cyan]Uploading directory...", total=100)
|
399
|
+
|
400
|
+
# We can't track actual progress from the client.store_directory method yet,
|
401
|
+
# so we'll update the progress periodically
|
402
|
+
start_time = time.time()
|
403
|
+
|
404
|
+
# Create a task to update the progress while waiting for the upload
|
405
|
+
async def update_progress():
|
406
|
+
while not progress.finished:
|
407
|
+
# Since we don't have actual progress data, we'll use time as a proxy
|
408
|
+
# The progress will move faster at first, then slow down
|
409
|
+
elapsed = time.time() - start_time
|
410
|
+
# Use a logarithmic function to simulate progress
|
411
|
+
# This is just an estimation and not actual progress
|
412
|
+
pct = min(95, 100 * (1 - 1 / (1 + elapsed / 10)))
|
413
|
+
progress.update(task, completed=pct)
|
414
|
+
await asyncio.sleep(0.1)
|
415
|
+
|
416
|
+
# Start the progress updater task
|
417
|
+
updater = asyncio.create_task(update_progress())
|
418
|
+
|
419
|
+
try:
|
420
|
+
# Use the store_directory method
|
421
|
+
result = await client.ipfs_client.upload_directory(
|
422
|
+
dir_path=dir_path,
|
423
|
+
encrypt=encrypt,
|
424
|
+
)
|
425
|
+
|
426
|
+
# Complete the progress
|
427
|
+
progress.update(task, completed=100)
|
428
|
+
# Cancel the updater task
|
429
|
+
updater.cancel()
|
430
|
+
|
431
|
+
elapsed_time = time.time() - start_time
|
432
|
+
|
433
|
+
# Success panel with results
|
434
|
+
success_info = [
|
435
|
+
f"Upload completed in [bold green]{elapsed_time:.2f}[/bold green] seconds!",
|
436
|
+
f"Directory CID: [bold cyan]{result['cid']}[/bold cyan]",
|
437
|
+
]
|
438
|
+
|
439
|
+
if result.get("gateway_url"):
|
440
|
+
success_info.append(
|
441
|
+
f"Gateway URL: [link]{result['gateway_url']}[/link]"
|
442
|
+
)
|
443
|
+
|
444
|
+
print_panel("\n".join(success_info), title="Directory Upload Successful")
|
445
|
+
|
446
|
+
# Display uploaded files in a table
|
447
|
+
if "files" in result:
|
448
|
+
table_data = []
|
449
|
+
for i, file_info in enumerate(result["files"], 1):
|
450
|
+
table_data.append(
|
451
|
+
{
|
452
|
+
"Index": str(i),
|
453
|
+
"Filename": file_info["name"],
|
454
|
+
"CID": file_info["cid"],
|
455
|
+
}
|
456
|
+
)
|
457
|
+
|
458
|
+
print_table(
|
459
|
+
f"Uploaded {len(result['files'])} Files",
|
460
|
+
table_data,
|
461
|
+
["Index", "Filename", "CID"],
|
462
|
+
)
|
463
|
+
|
464
|
+
# If we stored in the marketplace
|
465
|
+
if "transaction_hash" in result:
|
466
|
+
log(
|
467
|
+
f"\nStored in marketplace. Transaction hash: [bold]{result['transaction_hash']}[/bold]"
|
468
|
+
)
|
469
|
+
|
470
|
+
return 0
|
471
|
+
|
472
|
+
except Exception as e:
|
473
|
+
# Cancel the updater task in case of error
|
474
|
+
updater.cancel()
|
475
|
+
error(f"Directory upload failed: {str(e)}")
|
476
|
+
return 1
|
477
|
+
|
478
|
+
|
479
|
+
async def handle_credits(
|
480
|
+
client: HippiusClient, account_address: Optional[str] = None
|
481
|
+
) -> int:
|
482
|
+
"""Handle the credits command"""
|
483
|
+
info("Checking free credits for the account...")
|
484
|
+
try:
|
485
|
+
# Get the account address we're querying
|
486
|
+
if account_address is None:
|
487
|
+
# If no address provided, first try to get from keypair (if available)
|
488
|
+
if (
|
489
|
+
hasattr(client.substrate_client, "_keypair")
|
490
|
+
and client.substrate_client._keypair is not None
|
491
|
+
):
|
492
|
+
account_address = client.substrate_client._keypair.ss58_address
|
493
|
+
else:
|
494
|
+
# Try to get the default address
|
495
|
+
default_address = get_default_address()
|
496
|
+
if default_address:
|
497
|
+
account_address = default_address
|
498
|
+
else:
|
499
|
+
has_default = get_default_address() is not None
|
500
|
+
|
501
|
+
error("No account address provided, and client has no keypair.")
|
502
|
+
|
503
|
+
if has_default:
|
504
|
+
warning(
|
505
|
+
"Please provide an account address with '--account_address' or the default address may be invalid."
|
506
|
+
)
|
507
|
+
else:
|
508
|
+
warning(
|
509
|
+
"Please provide an account address with '--account_address' or set a default with:"
|
510
|
+
)
|
511
|
+
log(
|
512
|
+
" [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
|
513
|
+
)
|
514
|
+
|
515
|
+
return 1
|
516
|
+
|
517
|
+
credits = await client.substrate_client.get_free_credits(account_address)
|
518
|
+
|
519
|
+
# Create a panel with credit information
|
520
|
+
credit_info = [
|
521
|
+
f"Free credits: [bold green]{credits:.6f}[/bold green]",
|
522
|
+
f"Raw value: [dim]{int(credits * 1_000_000_000_000_000_000):,}[/dim]",
|
523
|
+
f"Account address: [bold cyan]{account_address}[/bold cyan]",
|
524
|
+
]
|
525
|
+
|
526
|
+
print_panel("\n".join(credit_info), title="Account Credits")
|
527
|
+
|
528
|
+
except Exception as e:
|
529
|
+
error(f"Error checking credits: {e}")
|
530
|
+
return 1
|
531
|
+
|
532
|
+
return 0
|
533
|
+
|
534
|
+
|
535
|
+
async def handle_files(
|
536
|
+
client: HippiusClient,
|
537
|
+
account_address: Optional[str] = None,
|
538
|
+
show_all_miners: bool = False,
|
539
|
+
file_cid: str = None,
|
540
|
+
) -> int:
|
541
|
+
"""Handle the files command"""
|
542
|
+
# Get the account address we're querying
|
543
|
+
if account_address is None:
|
544
|
+
# If no address provided, first try to get from keypair (if available)
|
545
|
+
if (
|
546
|
+
hasattr(client.substrate_client, "_keypair")
|
547
|
+
and client.substrate_client._keypair is not None
|
548
|
+
):
|
549
|
+
account_address = client.substrate_client._keypair.ss58_address
|
550
|
+
else:
|
551
|
+
# Try to get the default address
|
552
|
+
default_address = get_default_address()
|
553
|
+
if default_address:
|
554
|
+
account_address = default_address
|
555
|
+
else:
|
556
|
+
has_default = get_default_address() is not None
|
557
|
+
|
558
|
+
error("No account address provided, and client has no keypair.")
|
559
|
+
|
560
|
+
if has_default:
|
561
|
+
warning(
|
562
|
+
"Please provide an account address with '--account_address' or the default address may be invalid."
|
563
|
+
)
|
564
|
+
else:
|
565
|
+
info(
|
566
|
+
"Please provide an account address with '--account_address' or set a default with:"
|
567
|
+
)
|
568
|
+
log(
|
569
|
+
" [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
|
570
|
+
)
|
571
|
+
return 1
|
572
|
+
|
573
|
+
# Get files from the marketplace
|
574
|
+
info(f"Getting files for account: [bold]{account_address}[/bold]")
|
575
|
+
files = await client.substrate_client.get_user_files(account_address)
|
576
|
+
|
577
|
+
if not files:
|
578
|
+
info("No files found for this account")
|
579
|
+
return 0
|
580
|
+
|
581
|
+
# Display summary
|
582
|
+
success(f"Found [bold]{len(files)}[/bold] files")
|
583
|
+
|
584
|
+
# Display file information
|
585
|
+
for i, file in enumerate(files, 1):
|
586
|
+
# Extract file details
|
587
|
+
cid = file["cid"]
|
588
|
+
|
589
|
+
if file_cid and file_cid != cid:
|
590
|
+
continue
|
591
|
+
|
592
|
+
size_formatted = file["size_formatted"]
|
593
|
+
size_raw = file["file_size"]
|
594
|
+
file_name = file["file_name"]
|
595
|
+
file_hash = file["file_hash"]
|
596
|
+
selected_validator = file["selected_validator"]
|
597
|
+
|
598
|
+
# Create a panel for each file
|
599
|
+
file_info = [
|
600
|
+
f"CID: [bold cyan]{cid}[/bold cyan]",
|
601
|
+
f"Size: [bold]{size_raw}[/bold] bytes ([bold cyan]{size_formatted}[/bold cyan])",
|
602
|
+
f"File name: [bold]{file_name}[/bold]",
|
603
|
+
f"File hash: {file_hash}",
|
604
|
+
f"Selected validator: {selected_validator}",
|
605
|
+
]
|
606
|
+
|
607
|
+
# Show miners if requested
|
608
|
+
if show_all_miners and "miner_ids" in file:
|
609
|
+
miners = file.get("miner_ids", [])
|
610
|
+
if miners:
|
611
|
+
file_info.append(f"Stored on [bold]{len(miners)}[/bold] miners:")
|
612
|
+
miners_list = []
|
613
|
+
for j, miner in enumerate(miners, 1):
|
614
|
+
miners_list.append(f" {j}. {miner}")
|
615
|
+
file_info.append("\n".join(miners_list))
|
616
|
+
else:
|
617
|
+
file_info.append("No miners assigned yet")
|
618
|
+
|
619
|
+
print_panel("\n".join(file_info), title=f"File #{i}: {file_name}")
|
620
|
+
|
621
|
+
|
622
|
+
async def handle_pinning_status(
|
623
|
+
client: HippiusClient,
|
624
|
+
account_address: Optional[str] = None,
|
625
|
+
verbose: bool = False,
|
626
|
+
show_contents: bool = True,
|
627
|
+
) -> int:
|
628
|
+
"""Handle the pinning-status command"""
|
629
|
+
try:
|
630
|
+
info("Checking pinning status of files...")
|
631
|
+
|
632
|
+
# Use the get_pinning_status method from the substrate client
|
633
|
+
pins = client.substrate_client.get_pinning_status(account_address)
|
634
|
+
|
635
|
+
if not pins:
|
636
|
+
log("No active pins found")
|
637
|
+
return 0
|
638
|
+
|
639
|
+
log(f"\nFound {len(pins)} pinning requests:")
|
640
|
+
|
641
|
+
for i, pin in enumerate(pins, 1):
|
642
|
+
try:
|
643
|
+
# Get the CID from the pin data
|
644
|
+
cid = pin.get("cid")
|
645
|
+
|
646
|
+
# Display pin information
|
647
|
+
log(f"\n{i}. CID: [bold]{cid}[/bold]")
|
648
|
+
log(f" File Name: {pin['file_name']}")
|
649
|
+
status = "Assigned" if pin["is_assigned"] else "Pending"
|
650
|
+
log(f" Status: {status}")
|
651
|
+
log(f" Created At Block: {pin['created_at']}")
|
652
|
+
log(f" Last Charged At Block: {pin['last_charged_at']}")
|
653
|
+
log(f" Owner: {pin['owner']}")
|
654
|
+
log(f" Total Replicas: {pin['total_replicas']}")
|
655
|
+
log(f" Selected Validator: {pin['selected_validator']}")
|
656
|
+
miners = pin["miner_ids"]
|
657
|
+
if miners:
|
658
|
+
log(f" Miners: {', '.join(miners[:3])}")
|
659
|
+
if len(miners) > 3:
|
660
|
+
log(f" ... and {len(miners) - 3} more")
|
661
|
+
else:
|
662
|
+
log(" Miners: None assigned yet")
|
663
|
+
|
664
|
+
# Show content info if requested
|
665
|
+
if show_contents:
|
666
|
+
try:
|
667
|
+
content_info = await client.ipfs_client.get_content_info(cid)
|
668
|
+
|
669
|
+
if content_info:
|
670
|
+
if "size_formatted" in content_info:
|
671
|
+
log(f" Size: {content_info['size_formatted']}")
|
672
|
+
if "gateway_url" in content_info:
|
673
|
+
log(f" Gateway URL: {content_info['gateway_url']}")
|
674
|
+
except Exception as e:
|
675
|
+
if verbose:
|
676
|
+
warning(f" Error getting content info: {e}")
|
677
|
+
except Exception as e:
|
678
|
+
warning(f"Error processing pin {i}: {e}")
|
679
|
+
if verbose:
|
680
|
+
log(f"Raw pin data: {pin}")
|
681
|
+
|
682
|
+
return 0
|
683
|
+
|
684
|
+
except Exception as e:
|
685
|
+
error(f"Error checking pinning status: {str(e)}")
|
686
|
+
return 1
|
687
|
+
|
688
|
+
|
689
|
+
async def handle_ec_files(
|
690
|
+
client: HippiusClient,
|
691
|
+
account_address: Optional[str] = None,
|
692
|
+
show_all_miners: bool = False,
|
693
|
+
show_chunks: bool = False,
|
694
|
+
filter_metadata_cid: str = None,
|
695
|
+
) -> int:
|
696
|
+
"""Handle the ec-files command"""
|
697
|
+
if account_address is None:
|
698
|
+
# If no address provided, first try to get from keypair (if available)
|
699
|
+
if (
|
700
|
+
hasattr(client.substrate_client, "_keypair")
|
701
|
+
and client.substrate_client._keypair is not None
|
702
|
+
):
|
703
|
+
account_address = client.substrate_client._keypair.ss58_address
|
704
|
+
else:
|
705
|
+
# Try to get the default address
|
706
|
+
default_address = get_default_address()
|
707
|
+
if default_address:
|
708
|
+
account_address = default_address
|
709
|
+
else:
|
710
|
+
has_default = get_default_address() is not None
|
711
|
+
|
712
|
+
error("No account address provided, and client has no keypair.")
|
713
|
+
|
714
|
+
if has_default:
|
715
|
+
warning(
|
716
|
+
"Please provide an account address with '--account_address' or the default address may be invalid."
|
717
|
+
)
|
718
|
+
else:
|
719
|
+
info(
|
720
|
+
"Please provide an account address with '--account_address' or set a default with:"
|
721
|
+
)
|
722
|
+
log(
|
723
|
+
" [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
|
724
|
+
)
|
725
|
+
return 1
|
726
|
+
|
727
|
+
info(f"Getting erasure-coded files for account: [bold]{account_address}[/bold]")
|
728
|
+
|
729
|
+
# Get all files from the marketplace
|
730
|
+
files = await client.substrate_client.get_user_files(account_address)
|
731
|
+
|
732
|
+
# Separate metadata files and chunks
|
733
|
+
ec_metadata_files = []
|
734
|
+
chunk_files = []
|
735
|
+
|
736
|
+
for file in files:
|
737
|
+
if file["file_name"].endswith(".ec_metadata"):
|
738
|
+
ec_metadata_files.append(file)
|
739
|
+
elif file["file_name"].endswith(".ec"):
|
740
|
+
chunk_files.append(file)
|
741
|
+
|
742
|
+
if not ec_metadata_files:
|
743
|
+
info("No erasure-coded files found for this account")
|
744
|
+
return 0
|
745
|
+
|
746
|
+
# Display summary
|
747
|
+
success(f"Found [bold]{len(ec_metadata_files)}[/bold] erasure-coded files")
|
748
|
+
if chunk_files:
|
749
|
+
log(f"Found [bold]{len(chunk_files)}[/bold] chunk files")
|
750
|
+
|
751
|
+
# Store metadata CIDs for reconstruction command at the end
|
752
|
+
metadata_cids = []
|
753
|
+
|
754
|
+
# Process each metadata file
|
755
|
+
for i, metadata_file in enumerate(ec_metadata_files, 1):
|
756
|
+
metadata_cid = metadata_file["cid"]
|
757
|
+
|
758
|
+
if filter_metadata_cid and metadata_cid != filter_metadata_cid:
|
759
|
+
continue
|
760
|
+
|
761
|
+
metadata_file_name = metadata_file["file_name"]
|
762
|
+
metadata_size = metadata_file["file_size"]
|
763
|
+
metadata_size_formatted = metadata_file["size_formatted"]
|
764
|
+
|
765
|
+
# Store metadata CID for reconstruction command
|
766
|
+
metadata_cids.append(metadata_cid)
|
767
|
+
|
768
|
+
# Basic file info panel (always show this)
|
769
|
+
file_info = [
|
770
|
+
f"Metadata filename: [bold]{metadata_file_name}[/bold]",
|
771
|
+
f"Metadata CID: [bold cyan]{metadata_cid}[/bold cyan]",
|
772
|
+
f"Metadata size: [bold]{metadata_size}[/bold] bytes ([bold cyan]{metadata_size_formatted}[/bold cyan])",
|
773
|
+
f"Selected validator: {metadata_file['selected_validator']}",
|
774
|
+
]
|
775
|
+
|
776
|
+
# Add miners info if available and requested - with consistent formatting
|
777
|
+
if show_all_miners and metadata_file["miner_ids"]:
|
778
|
+
miners = metadata_file["miner_ids"]
|
779
|
+
file_info.append(f"Metadata stored on [bold]{len(miners)}[/bold] miners:")
|
780
|
+
miners_list = []
|
781
|
+
for j, miner in enumerate(miners, 1):
|
782
|
+
miners_list.append(f" {j}. {miner}")
|
783
|
+
file_info.append("\n".join(miners_list))
|
784
|
+
|
785
|
+
# If show_chunks is enabled, download the metadata file and get chunk information
|
786
|
+
if show_chunks:
|
787
|
+
with tempfile.NamedTemporaryFile() as temp:
|
788
|
+
temp_path = temp.name
|
789
|
+
|
790
|
+
# Download the metadata file without logging
|
791
|
+
await client.ipfs_client.download_file(metadata_cid, temp_path)
|
792
|
+
|
793
|
+
# Open and parse the metadata file
|
794
|
+
with open(temp_path, "r") as f:
|
795
|
+
metadata_content = json.load(f)
|
796
|
+
|
797
|
+
# Extract the original file information
|
798
|
+
original_file = metadata_content["original_file"]
|
799
|
+
original_file_name = original_file["name"]
|
800
|
+
original_file_size = original_file["size"]
|
801
|
+
|
802
|
+
# Extract the erasure coding parameters
|
803
|
+
erasure_coding = metadata_content["erasure_coding"]
|
804
|
+
file_id = erasure_coding["file_id"]
|
805
|
+
k = erasure_coding["k"]
|
806
|
+
m = erasure_coding["m"]
|
807
|
+
chunk_size = erasure_coding["chunk_size"]
|
808
|
+
encrypted = erasure_coding["encrypted"]
|
809
|
+
|
810
|
+
# Extract the chunks information
|
811
|
+
chunks_info = metadata_content["chunks"]
|
812
|
+
|
813
|
+
# Update file_info with detailed metadata information
|
814
|
+
file_info = [
|
815
|
+
f"Original file: [bold]{original_file_name}[/bold]",
|
816
|
+
f"Size: [bold]{original_file_size}[/bold] bytes ([bold cyan]{format_size(original_file_size)}[/bold cyan])",
|
817
|
+
f"File hash: {original_file['hash']}",
|
818
|
+
f"Metadata CID: [bold cyan]{metadata_cid}[/bold cyan]",
|
819
|
+
f"File ID: [bold yellow]{file_id}[/bold yellow]",
|
820
|
+
f"Erasure coding: k=[bold]{k}[/bold], m=[bold]{m}[/bold] (need {k} of {m} chunks to reconstruct)",
|
821
|
+
f"Chunk size: [bold]{format_size(chunk_size)}[/bold]",
|
822
|
+
f"Encrypted: [bold]{'Yes' if encrypted else 'No'}[/bold]",
|
823
|
+
f"Total chunks from metadata: [bold]{len(chunks_info)}[/bold]",
|
824
|
+
]
|
825
|
+
|
826
|
+
# Match chunks from the blockchain with the metadata by CID
|
827
|
+
matching_chunks = []
|
828
|
+
chunk_cids_in_metadata = []
|
829
|
+
|
830
|
+
# Create a mapping of CIDs from metadata
|
831
|
+
for chunk in chunks_info:
|
832
|
+
# Extract CID (handle both string and dict formats)
|
833
|
+
chunk_cid = chunk["cid"]
|
834
|
+
if isinstance(chunk_cid, dict) and "cid" in chunk_cid:
|
835
|
+
chunk_cid = chunk_cid["cid"]
|
836
|
+
chunk_cids_in_metadata.append(chunk_cid)
|
837
|
+
|
838
|
+
# Find matching chunks
|
839
|
+
for chunk_file in chunk_files:
|
840
|
+
if (
|
841
|
+
chunk_file["cid"] in chunk_cids_in_metadata
|
842
|
+
or file_id in chunk_file["file_name"]
|
843
|
+
):
|
844
|
+
matching_chunks.append(chunk_file)
|
845
|
+
|
846
|
+
# Add information about matched chunks
|
847
|
+
if matching_chunks:
|
848
|
+
file_info.append(
|
849
|
+
f"Found [bold]{len(matching_chunks)}/{len(chunks_info)}[/bold] chunks in blockchain"
|
850
|
+
)
|
851
|
+
|
852
|
+
# Calculate if we have enough chunks for reconstruction
|
853
|
+
chunks_needed = k
|
854
|
+
if len(matching_chunks) >= chunks_needed:
|
855
|
+
file_info.append(
|
856
|
+
"[bold green]✓ Enough chunks available for reconstruction[/bold green]"
|
857
|
+
)
|
858
|
+
else:
|
859
|
+
file_info.append(
|
860
|
+
"[bold red]✗ Not enough chunks available for reconstruction[/bold red]"
|
861
|
+
)
|
862
|
+
else:
|
863
|
+
file_info.append(
|
864
|
+
"[bold yellow]No associated chunks found in blockchain[/bold yellow]"
|
865
|
+
)
|
866
|
+
|
867
|
+
# Display the panel with all file information
|
868
|
+
print_panel(
|
869
|
+
"\n".join(file_info), title=f"File #{i}: {original_file_name}"
|
870
|
+
)
|
871
|
+
|
872
|
+
# Display the chunks in a table if we found any
|
873
|
+
if matching_chunks:
|
874
|
+
# Limit the number of chunks displayed
|
875
|
+
MAX_DISPLAYED_CHUNKS = 25
|
876
|
+
chunk_table = []
|
877
|
+
|
878
|
+
# Sort chunks by original_chunk_idx and share_idx for better display
|
879
|
+
def chunk_sort_key(chunk):
|
880
|
+
chunk_name = chunk["file_name"]
|
881
|
+
chunk_parts = []
|
882
|
+
if "_chunk_" in chunk_name:
|
883
|
+
chunk_parts = (
|
884
|
+
chunk_name.split("_chunk_")[1].split(".")[0].split("_")
|
885
|
+
)
|
886
|
+
|
887
|
+
original_chunk_idx = (
|
888
|
+
int(chunk_parts[0])
|
889
|
+
if len(chunk_parts) > 0 and chunk_parts[0].isdigit()
|
890
|
+
else 0
|
891
|
+
)
|
892
|
+
share_idx = (
|
893
|
+
int(chunk_parts[1])
|
894
|
+
if len(chunk_parts) > 1 and chunk_parts[1].isdigit()
|
895
|
+
else 0
|
896
|
+
)
|
897
|
+
return (original_chunk_idx, share_idx)
|
898
|
+
|
899
|
+
sorted_chunks = sorted(matching_chunks, key=chunk_sort_key)
|
900
|
+
displayed_chunks = sorted_chunks[:MAX_DISPLAYED_CHUNKS]
|
901
|
+
|
902
|
+
for j, chunk in enumerate(displayed_chunks, 1):
|
903
|
+
# Extract information from the chunk filename
|
904
|
+
chunk_name = chunk["file_name"]
|
905
|
+
chunk_parts = []
|
906
|
+
|
907
|
+
# Try to extract original_chunk_idx and share_idx
|
908
|
+
if "_chunk_" in chunk_name:
|
909
|
+
chunk_parts = (
|
910
|
+
chunk_name.split("_chunk_")[1].split(".")[0].split("_")
|
911
|
+
)
|
912
|
+
|
913
|
+
original_chunk_idx = (
|
914
|
+
chunk_parts[0] if len(chunk_parts) > 0 else "?"
|
915
|
+
)
|
916
|
+
share_idx = chunk_parts[1] if len(chunk_parts) > 1 else "?"
|
917
|
+
|
918
|
+
chunk_table.append(
|
919
|
+
{
|
920
|
+
"Index": str(j),
|
921
|
+
"Name": chunk_name,
|
922
|
+
"Original": original_chunk_idx,
|
923
|
+
"Share": share_idx,
|
924
|
+
"Size": chunk["size_formatted"],
|
925
|
+
"CID": chunk["cid"][:10] + "..." + chunk["cid"][-6:]
|
926
|
+
if len(chunk["cid"]) > 20
|
927
|
+
else chunk["cid"],
|
928
|
+
}
|
929
|
+
)
|
930
|
+
|
931
|
+
# Print chunk table without title, using dim (grey) styling
|
932
|
+
print_table(
|
933
|
+
"", # Empty title
|
934
|
+
chunk_table,
|
935
|
+
["Index", "Name", "Original", "Share", "Size", "CID"],
|
936
|
+
style="dim", # Use dim (grey) styling for chunk tables
|
937
|
+
)
|
938
|
+
|
939
|
+
# If there are more chunks than the display limit, show a compact note in dim text
|
940
|
+
if len(matching_chunks) > MAX_DISPLAYED_CHUNKS:
|
941
|
+
log(
|
942
|
+
f"[dim](Showing {MAX_DISPLAYED_CHUNKS} of {len(matching_chunks)} chunks. Use 'hippius ipfs download {metadata_cid}' to view all.)[/dim]"
|
943
|
+
)
|
944
|
+
|
945
|
+
else:
|
946
|
+
# If show_chunks is disabled, just display the basic file information
|
947
|
+
print_panel("\n".join(file_info), title=f"File #{i}: {metadata_file_name}")
|
948
|
+
|
949
|
+
# Show a generic reconstruction command at the end
|
950
|
+
if metadata_cids:
|
951
|
+
# Include a real example with the first metadata CID
|
952
|
+
example_cid = metadata_cids[0] if metadata_cids else "<METADATA_CID>"
|
953
|
+
print_panel(
|
954
|
+
f"[bold green underline]hippius reconstruct <METADATA_CID> <OUTPUT_FILENAME>[/bold green underline]\n\nExample:\n[bold green underline]hippius reconstruct {example_cid} reconstructed_file.bin[/bold green underline]",
|
955
|
+
title="Reconstruction Command",
|
956
|
+
)
|
957
|
+
|
958
|
+
return 0
|
959
|
+
|
960
|
+
|
961
|
+
async def handle_erasure_code(
|
962
|
+
client: HippiusClient,
|
963
|
+
file_path: str,
|
964
|
+
k: int,
|
965
|
+
m: int,
|
966
|
+
chunk_size: int,
|
967
|
+
miner_ids: Optional[List[str]] = None,
|
968
|
+
encrypt: Optional[bool] = None,
|
969
|
+
publish: bool = True,
|
970
|
+
verbose: bool = False,
|
971
|
+
) -> int:
|
972
|
+
"""Handle the erasure-code command"""
|
973
|
+
if not os.path.exists(file_path):
|
974
|
+
error(f"File [bold]{file_path}[/bold] does not exist")
|
975
|
+
return 1
|
976
|
+
|
977
|
+
if not os.path.isfile(file_path):
|
978
|
+
error(f"[bold]{file_path}[/bold] is not a file")
|
979
|
+
return 1
|
980
|
+
|
981
|
+
# Check if zfec is installed
|
982
|
+
try:
|
983
|
+
import zfec
|
984
|
+
except ImportError:
|
985
|
+
error("zfec is required for erasure coding")
|
986
|
+
log(
|
987
|
+
"Install it with: [bold green underline]pip install zfec[/bold green underline]"
|
988
|
+
)
|
989
|
+
log(
|
990
|
+
"Then update your environment: [bold green underline]poetry add zfec[/bold green underline]"
|
991
|
+
)
|
992
|
+
return 1
|
993
|
+
|
994
|
+
# Get file size
|
995
|
+
file_size = os.path.getsize(file_path)
|
996
|
+
file_name = os.path.basename(file_path)
|
997
|
+
|
998
|
+
# Convert chunk size from MB to bytes if needed
|
999
|
+
if chunk_size < 1024: # Assume it's in MB if small
|
1000
|
+
chunk_size = chunk_size * 1024 * 1024
|
1001
|
+
|
1002
|
+
# Calculate potential chunks
|
1003
|
+
potential_chunks = file_size / chunk_size
|
1004
|
+
if potential_chunks < k:
|
1005
|
+
warning("File is too small for the requested parameters.")
|
1006
|
+
|
1007
|
+
# Calculate new chunk size to get exactly k chunks
|
1008
|
+
new_chunk_size = file_size / k
|
1009
|
+
|
1010
|
+
new_chunk_size = int(new_chunk_size)
|
1011
|
+
new_chunk_size = max(1, new_chunk_size)
|
1012
|
+
|
1013
|
+
# Create a panel with parameter adjustment information
|
1014
|
+
adjustment_info = [
|
1015
|
+
f"Original parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold], chunk size=[bold]{chunk_size / 1024 / 1024:.2f} MB[/bold]",
|
1016
|
+
f"Would create only [bold red]{potential_chunks:.2f}[/bold red] chunks, which is less than k=[bold]{k}[/bold]",
|
1017
|
+
f"Automatically adjusting chunk size to [bold green]{new_chunk_size / 1024 / 1024:.6f} MB[/bold green] to create at least {k} chunks",
|
1018
|
+
]
|
1019
|
+
print_panel("\n".join(adjustment_info), title="Parameter Adjustment")
|
1020
|
+
|
1021
|
+
chunk_size = new_chunk_size
|
1022
|
+
|
1023
|
+
# Create parameter information panel
|
1024
|
+
param_info = [
|
1025
|
+
f"File: [bold]{file_name}[/bold] ([bold cyan]{file_size / 1024 / 1024:.2f} MB[/bold cyan])",
|
1026
|
+
f"Parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold] (need {k} of {m} chunks to reconstruct)",
|
1027
|
+
f"Chunk size: [bold cyan]{chunk_size / 1024 / 1024:.6f} MB[/bold cyan]",
|
1028
|
+
]
|
1029
|
+
|
1030
|
+
# Add encryption status
|
1031
|
+
if encrypt:
|
1032
|
+
param_info.append("[bold green]Encryption: Enabled[/bold green]")
|
1033
|
+
else:
|
1034
|
+
param_info.append("[bold yellow]Encryption: Disabled[/bold yellow]")
|
1035
|
+
|
1036
|
+
# Add publish status
|
1037
|
+
if publish:
|
1038
|
+
param_info.append(
|
1039
|
+
"[bold blue]Publishing: Enabled[/bold blue] (will store on blockchain)"
|
1040
|
+
)
|
1041
|
+
else:
|
1042
|
+
param_info.append(
|
1043
|
+
"[bold cyan]Publishing: Disabled[/bold cyan] (local only, no password needed)"
|
1044
|
+
)
|
1045
|
+
|
1046
|
+
# Parse miner IDs if provided
|
1047
|
+
miner_id_list = None
|
1048
|
+
if miner_ids:
|
1049
|
+
miner_id_list = [m.strip() for m in miner_ids if m.strip()]
|
1050
|
+
param_info.append(
|
1051
|
+
f"Targeting [bold]{len(miner_id_list)}[/bold] miners for storage"
|
1052
|
+
)
|
1053
|
+
|
1054
|
+
# Display parameter information panel
|
1055
|
+
print_panel("\n".join(param_info), title="Erasure Coding Operation")
|
1056
|
+
|
1057
|
+
start_time = time.time()
|
1058
|
+
|
1059
|
+
# Create progress for the erasure coding operation
|
1060
|
+
with create_progress() as progress:
|
1061
|
+
# Add tasks for the different stages
|
1062
|
+
processing_task = progress.add_task(
|
1063
|
+
"[cyan]Processing file...", total=100, visible=False
|
1064
|
+
)
|
1065
|
+
encoding_task = progress.add_task(
|
1066
|
+
"[green]Encoding chunks...", total=100, visible=False
|
1067
|
+
)
|
1068
|
+
upload_task = progress.add_task(
|
1069
|
+
"[blue]Uploading chunks...", total=100, visible=False
|
1070
|
+
)
|
1071
|
+
|
1072
|
+
# Progress callback function to update the appropriate task
|
1073
|
+
def update_progress_bar(stage, current, total):
|
1074
|
+
pct = min(100, int(current / total * 100))
|
1075
|
+
|
1076
|
+
if stage == "processing":
|
1077
|
+
progress.update(processing_task, completed=pct)
|
1078
|
+
if pct >= 100 and not progress.tasks[encoding_task].visible:
|
1079
|
+
progress.update(encoding_task, visible=True)
|
1080
|
+
|
1081
|
+
elif stage == "encoding":
|
1082
|
+
progress.update(encoding_task, completed=pct)
|
1083
|
+
if pct >= 100 and not progress.tasks[upload_task].visible:
|
1084
|
+
progress.update(upload_task, visible=True)
|
1085
|
+
|
1086
|
+
elif stage == "upload":
|
1087
|
+
progress.update(upload_task, completed=pct)
|
1088
|
+
|
1089
|
+
# As a fallback, create a task to update the general progress if no callbacks are received
|
1090
|
+
async def update_general_progress():
|
1091
|
+
while not progress.finished:
|
1092
|
+
elapsed = time.time() - start_time
|
1093
|
+
# If we haven't shown the encoding task yet, update the processing task
|
1094
|
+
if not progress.tasks[encoding_task].visible:
|
1095
|
+
pct = min(95, 100 * (1 - 1 / (1 + elapsed / 5)))
|
1096
|
+
progress.update(processing_task, completed=pct)
|
1097
|
+
if pct > 90:
|
1098
|
+
progress.update(encoding_task, visible=True)
|
1099
|
+
# If we haven't shown the upload task yet, update the encoding task
|
1100
|
+
elif not progress.tasks[upload_task].visible and elapsed > 3:
|
1101
|
+
pct = min(95, 100 * (1 - 1 / (1 + (elapsed - 3) / 5)))
|
1102
|
+
progress.update(encoding_task, completed=pct)
|
1103
|
+
if pct > 90:
|
1104
|
+
progress.update(upload_task, visible=True)
|
1105
|
+
|
1106
|
+
await asyncio.sleep(0.1)
|
1107
|
+
|
1108
|
+
# Start the fallback progress updater task
|
1109
|
+
updater = asyncio.create_task(update_general_progress())
|
1110
|
+
|
1111
|
+
try:
|
1112
|
+
# Use the store_erasure_coded_file method directly from HippiusClient
|
1113
|
+
result = await client.store_erasure_coded_file(
|
1114
|
+
file_path=file_path,
|
1115
|
+
k=k,
|
1116
|
+
m=m,
|
1117
|
+
chunk_size=chunk_size,
|
1118
|
+
encrypt=encrypt,
|
1119
|
+
miner_ids=miner_id_list,
|
1120
|
+
max_retries=3,
|
1121
|
+
verbose=verbose,
|
1122
|
+
progress_callback=update_progress_bar,
|
1123
|
+
publish=publish,
|
1124
|
+
)
|
1125
|
+
|
1126
|
+
# Complete all progress tasks
|
1127
|
+
progress.update(processing_task, completed=100)
|
1128
|
+
progress.update(encoding_task, completed=100, visible=True)
|
1129
|
+
progress.update(upload_task, completed=100, visible=True)
|
1130
|
+
# Cancel the updater task
|
1131
|
+
updater.cancel()
|
1132
|
+
|
1133
|
+
# Store the original result before potentially overwriting it with publish result
|
1134
|
+
storage_result = result.copy()
|
1135
|
+
metadata_cid = storage_result.get("metadata_cid", "unknown")
|
1136
|
+
|
1137
|
+
# If publish flag is set, publish to the global IPFS network
|
1138
|
+
if publish:
|
1139
|
+
if metadata_cid != "unknown":
|
1140
|
+
info("Publishing to global IPFS network...")
|
1141
|
+
try:
|
1142
|
+
# Publish the metadata to the global IPFS network
|
1143
|
+
publish_result = await client.ipfs_client.publish_global(
|
1144
|
+
metadata_cid
|
1145
|
+
)
|
1146
|
+
if publish_result.get("published", False):
|
1147
|
+
success("Successfully published to global IPFS network")
|
1148
|
+
log(
|
1149
|
+
f"Access URL: [link]{client.ipfs_client.gateway}/ipfs/{metadata_cid}[/link]"
|
1150
|
+
)
|
1151
|
+
else:
|
1152
|
+
warning(
|
1153
|
+
f"{publish_result.get('message', 'Failed to publish to global network')}"
|
1154
|
+
)
|
1155
|
+
except Exception as e:
|
1156
|
+
warning(f"Failed to publish to global IPFS network: {str(e)}")
|
1157
|
+
|
1158
|
+
elapsed_time = time.time() - start_time
|
1159
|
+
|
1160
|
+
# Display metadata
|
1161
|
+
metadata = storage_result.get("metadata", {})
|
1162
|
+
total_files_stored = storage_result.get("total_files_stored", 0)
|
1163
|
+
|
1164
|
+
original_file = metadata.get("original_file", {})
|
1165
|
+
erasure_coding = metadata.get("erasure_coding", {})
|
1166
|
+
|
1167
|
+
# Create a summary panel with the erasure coding results
|
1168
|
+
summary_lines = [
|
1169
|
+
f"Completed in [bold green]{elapsed_time:.2f}[/bold green] seconds!"
|
1170
|
+
]
|
1171
|
+
|
1172
|
+
# If metadata_cid is known but metadata is empty, try to get file info from result directly
|
1173
|
+
if metadata_cid != "unknown" and not original_file:
|
1174
|
+
file_name = os.path.basename(file_path)
|
1175
|
+
file_size = (
|
1176
|
+
os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
1177
|
+
)
|
1178
|
+
|
1179
|
+
# Use direct values from input parameters when metadata is not available
|
1180
|
+
summary_lines.extend(
|
1181
|
+
[
|
1182
|
+
f"Original file: [bold]{file_name}[/bold] ([bold cyan]{file_size / 1024 / 1024:.2f} MB[/bold cyan])",
|
1183
|
+
f"Parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold]",
|
1184
|
+
f"Total files stored in marketplace: [bold]{total_files_stored}[/bold]",
|
1185
|
+
f"Metadata CID: [bold cyan]{metadata_cid}[/bold cyan]",
|
1186
|
+
]
|
1187
|
+
)
|
1188
|
+
|
1189
|
+
# Add publish status if applicable
|
1190
|
+
if publish:
|
1191
|
+
summary_lines.extend(
|
1192
|
+
[
|
1193
|
+
"Published to global IPFS: [bold green]Yes[/bold green]",
|
1194
|
+
f"Global access URL: [link]{client.ipfs_client.gateway}/ipfs/{metadata_cid}[/link]",
|
1195
|
+
]
|
1196
|
+
)
|
1197
|
+
else:
|
1198
|
+
summary_lines.extend(
|
1199
|
+
[
|
1200
|
+
f"Original file: [bold]{original_file.get('name')}[/bold] ([bold cyan]{original_file.get('size', 0) / 1024 / 1024:.2f} MB[/bold cyan])",
|
1201
|
+
f"File ID: [bold]{erasure_coding.get('file_id')}[/bold]",
|
1202
|
+
f"Parameters: k=[bold]{erasure_coding.get('k')}[/bold], m=[bold]{erasure_coding.get('m')}[/bold]",
|
1203
|
+
f"Total chunks: [bold]{len(metadata.get('chunks', []))}[/bold]",
|
1204
|
+
f"Total files stored in marketplace: [bold]{total_files_stored}[/bold]",
|
1205
|
+
f"Metadata CID: [bold cyan]{metadata_cid}[/bold cyan]",
|
1206
|
+
]
|
1207
|
+
)
|
1208
|
+
|
1209
|
+
# Add publish status if applicable
|
1210
|
+
if publish:
|
1211
|
+
summary_lines.extend(
|
1212
|
+
[
|
1213
|
+
"Published to global IPFS: [bold green]Yes[/bold green]",
|
1214
|
+
f"Global access URL: [link]{client.ipfs_client.gateway}/ipfs/{metadata_cid}[/link]",
|
1215
|
+
]
|
1216
|
+
)
|
1217
|
+
|
1218
|
+
# If we stored in the marketplace
|
1219
|
+
if "transaction_hash" in result:
|
1220
|
+
summary_lines.append(
|
1221
|
+
f"Transaction hash: [bold]{result['transaction_hash']}[/bold]"
|
1222
|
+
)
|
1223
|
+
|
1224
|
+
# Display the summary panel
|
1225
|
+
print_panel("\n".join(summary_lines), title="Erasure Coding Summary")
|
1226
|
+
|
1227
|
+
# Get file name, either from metadata or directly from file path
|
1228
|
+
output_filename = original_file.get("name")
|
1229
|
+
if not output_filename:
|
1230
|
+
output_filename = os.path.basename(file_path)
|
1231
|
+
|
1232
|
+
# Create reconstruction instructions panel
|
1233
|
+
reconstruction_lines = [
|
1234
|
+
"You will need:",
|
1235
|
+
f" 1. The metadata CID: [bold cyan]{metadata_cid}[/bold cyan]",
|
1236
|
+
f" 2. Access to at least [bold]{k}[/bold] chunks for each original chunk",
|
1237
|
+
"",
|
1238
|
+
"Reconstruction command:",
|
1239
|
+
f"[bold green underline]hippius reconstruct {metadata_cid} reconstructed_{output_filename}[/bold green underline]",
|
1240
|
+
]
|
1241
|
+
|
1242
|
+
print_panel(
|
1243
|
+
"\n".join(reconstruction_lines), title="Reconstruction Instructions"
|
1244
|
+
)
|
1245
|
+
|
1246
|
+
return 0
|
1247
|
+
|
1248
|
+
except Exception as e:
|
1249
|
+
# Cancel the updater task in case of error
|
1250
|
+
updater.cancel()
|
1251
|
+
error(f"Erasure coding failed: {str(e)}")
|
1252
|
+
|
1253
|
+
# Provide helpful advice based on the error
|
1254
|
+
if "Wrong length" in str(e) and "input blocks" in str(e):
|
1255
|
+
# Create an advice panel for small file errors
|
1256
|
+
advice_lines = [
|
1257
|
+
"This error typically occurs with very small files.",
|
1258
|
+
"",
|
1259
|
+
"Suggestions:",
|
1260
|
+
" 1. Try using a smaller chunk size: [bold]--chunk-size 4096[/bold]",
|
1261
|
+
" 2. Try using a smaller k value: [bold]--k 2[/bold]",
|
1262
|
+
" 3. For very small files, consider using regular storage instead of erasure coding.",
|
1263
|
+
]
|
1264
|
+
print_panel("\n".join(advice_lines), title="Troubleshooting")
|
1265
|
+
|
1266
|
+
|
1267
|
+
async def handle_reconstruct(
|
1268
|
+
client: HippiusClient, metadata_cid: str, output_file: str, verbose: bool = False
|
1269
|
+
) -> int:
|
1270
|
+
"""Handle the reconstruct command"""
|
1271
|
+
# Create initial parameters panel
|
1272
|
+
param_info = [
|
1273
|
+
f"Metadata CID: [bold cyan]{metadata_cid}[/bold cyan]",
|
1274
|
+
f"Output file: [bold]{output_file}[/bold]",
|
1275
|
+
]
|
1276
|
+
|
1277
|
+
if verbose:
|
1278
|
+
param_info.append(
|
1279
|
+
"[bold yellow]Verbose mode enabled[/bold yellow] - will show detailed progress"
|
1280
|
+
)
|
1281
|
+
|
1282
|
+
print_panel("\n".join(param_info), title="Reconstruction Operation")
|
1283
|
+
|
1284
|
+
start_time = time.time()
|
1285
|
+
|
1286
|
+
# Create progress for the reconstruction operation
|
1287
|
+
with create_progress() as progress:
|
1288
|
+
# Add a task for the reconstruction process
|
1289
|
+
download_task = progress.add_task("[cyan]Downloading metadata...", total=100)
|
1290
|
+
reconstruct_task = progress.add_task(
|
1291
|
+
"[green]Reconstructing file...", total=100, visible=False
|
1292
|
+
)
|
1293
|
+
|
1294
|
+
# Create a task to update the progress while waiting for the operation
|
1295
|
+
async def update_progress():
|
1296
|
+
# Phase 1: Downloading metadata and chunks (first 40% of process)
|
1297
|
+
# Assume an approximate timing: phase1 = 5 seconds, phase2 = 10 seconds
|
1298
|
+
phase1_duration = 5
|
1299
|
+
total_duration = 15 # Estimation for both phases
|
1300
|
+
|
1301
|
+
while not progress.finished:
|
1302
|
+
elapsed = time.time() - start_time
|
1303
|
+
|
1304
|
+
# Phase 1: Downloading metadata and chunks (0-40%)
|
1305
|
+
if elapsed <= phase1_duration:
|
1306
|
+
# Calculate progress for phase 1 (0-100%)
|
1307
|
+
download_pct = min(100, elapsed / phase1_duration * 100)
|
1308
|
+
progress.update(download_task, completed=download_pct)
|
1309
|
+
|
1310
|
+
# Make reconstruction task visible when metadata download starts progressing
|
1311
|
+
if (
|
1312
|
+
download_pct > 30
|
1313
|
+
and not progress.tasks[reconstruct_task].visible
|
1314
|
+
):
|
1315
|
+
progress.update(reconstruct_task, completed=0, visible=True)
|
1316
|
+
else:
|
1317
|
+
# Ensure download task shows complete
|
1318
|
+
progress.update(download_task, completed=100)
|
1319
|
+
|
1320
|
+
# Phase 2: Reconstructing (0-100%)
|
1321
|
+
remaining_time = total_duration - phase1_duration
|
1322
|
+
phase2_elapsed = elapsed - phase1_duration
|
1323
|
+
if phase2_elapsed >= 0:
|
1324
|
+
# Calculate progress for phase 2 (0-100%)
|
1325
|
+
reconstruct_pct = min(95, phase2_elapsed / remaining_time * 100)
|
1326
|
+
progress.update(
|
1327
|
+
reconstruct_task, completed=reconstruct_pct, visible=True
|
1328
|
+
)
|
1329
|
+
|
1330
|
+
await asyncio.sleep(0.1)
|
1331
|
+
|
1332
|
+
# Start the progress updater task
|
1333
|
+
updater = asyncio.create_task(update_progress())
|
1334
|
+
|
1335
|
+
try:
|
1336
|
+
# Use the reconstruct_erasure_coded_file method
|
1337
|
+
result = await client.reconstruct_from_erasure_code(
|
1338
|
+
metadata_cid=metadata_cid, output_file=output_file, verbose=verbose
|
1339
|
+
)
|
1340
|
+
|
1341
|
+
# Complete all progress tasks
|
1342
|
+
progress.update(download_task, completed=100)
|
1343
|
+
progress.update(reconstruct_task, completed=100)
|
1344
|
+
# Cancel the updater task
|
1345
|
+
updater.cancel()
|
1346
|
+
|
1347
|
+
elapsed_time = time.time() - start_time
|
1348
|
+
|
1349
|
+
# Display reconstruction results
|
1350
|
+
output_path = result.get("output_path", output_file)
|
1351
|
+
file_size = result.get("size_bytes", 0)
|
1352
|
+
size_formatted = format_size(file_size)
|
1353
|
+
|
1354
|
+
# Create success panel
|
1355
|
+
success_info = [
|
1356
|
+
f"Reconstruction completed in [bold green]{elapsed_time:.2f}[/bold green] seconds!",
|
1357
|
+
f"Saved to: [bold]{output_path}[/bold]",
|
1358
|
+
f"Size: [bold cyan]{file_size:,}[/bold cyan] bytes ([bold cyan]{size_formatted}[/bold cyan])",
|
1359
|
+
]
|
1360
|
+
|
1361
|
+
if result.get("decrypted"):
|
1362
|
+
success_info.append(
|
1363
|
+
"[bold yellow]File was decrypted during reconstruction[/bold yellow]"
|
1364
|
+
)
|
1365
|
+
|
1366
|
+
print_panel("\n".join(success_info), title="Reconstruction Successful")
|
1367
|
+
|
1368
|
+
return 0
|
1369
|
+
|
1370
|
+
except Exception as e:
|
1371
|
+
# Cancel the updater task in case of error
|
1372
|
+
updater.cancel()
|
1373
|
+
error(f"Reconstruction failed: {str(e)}")
|
1374
|
+
|
1375
|
+
if "No metadata found for CID" in str(e):
|
1376
|
+
advice_lines = [
|
1377
|
+
"The metadata CID could not be found. Please check:",
|
1378
|
+
" 1. The CID is correct",
|
1379
|
+
" 2. The IPFS gateway is accessible",
|
1380
|
+
" 3. If the file was published to the global IPFS network",
|
1381
|
+
]
|
1382
|
+
print_panel("\n".join(advice_lines), title="Troubleshooting")
|
1383
|
+
|
1384
|
+
elif "Failed to download chunk" in str(e):
|
1385
|
+
advice_lines = [
|
1386
|
+
"Failed to download enough chunks for reconstruction. Please check:",
|
1387
|
+
" 1. Your connection to the IPFS network",
|
1388
|
+
" 2. If enough chunks are available (need at least k chunks)",
|
1389
|
+
" 3. If the chunks are still pinned in the network",
|
1390
|
+
]
|
1391
|
+
print_panel("\n".join(advice_lines), title="Troubleshooting")
|
1392
|
+
|
1393
|
+
return 1
|
1394
|
+
|
1395
|
+
|
1396
|
+
async def handle_delete(client: HippiusClient, cid: str, force: bool = False) -> int:
|
1397
|
+
"""Handle the delete command"""
|
1398
|
+
info(f"Preparing to delete file with CID: [bold cyan]{cid}[/bold cyan]")
|
1399
|
+
|
1400
|
+
if not force:
|
1401
|
+
warning("This will cancel storage and remove the file from the marketplace.")
|
1402
|
+
confirm = input("Continue? (y/n): ").strip().lower()
|
1403
|
+
if confirm != "y":
|
1404
|
+
log("Deletion cancelled", style="yellow")
|
1405
|
+
return 0
|
1406
|
+
|
1407
|
+
info("Deleting file from marketplace...")
|
1408
|
+
result = await client.delete_file(cid)
|
1409
|
+
|
1410
|
+
if result.get("success"):
|
1411
|
+
success("File successfully deleted")
|
1412
|
+
|
1413
|
+
details = []
|
1414
|
+
if "transaction_hash" in result:
|
1415
|
+
details.append(
|
1416
|
+
f"Transaction hash: [bold]{result['transaction_hash']}[/bold]"
|
1417
|
+
)
|
1418
|
+
|
1419
|
+
# Create an informative panel with notes
|
1420
|
+
notes = [
|
1421
|
+
"1. The file is now unpinned from the marketplace",
|
1422
|
+
"2. The CID may still resolve temporarily until garbage collection occurs",
|
1423
|
+
"3. If the file was published to the global IPFS network, it may still be",
|
1424
|
+
" available through other nodes that pinned it",
|
1425
|
+
]
|
1426
|
+
|
1427
|
+
if details:
|
1428
|
+
print_panel("\n".join(details), title="Transaction Details")
|
1429
|
+
|
1430
|
+
print_panel("\n".join(notes), title="Important Notes")
|
1431
|
+
|
1432
|
+
return 0
|
1433
|
+
else:
|
1434
|
+
error(f"Failed to delete file: {result}")
|
1435
|
+
|
1436
|
+
|
1437
|
+
async def handle_ec_delete(
|
1438
|
+
client: HippiusClient, metadata_cid: str, force: bool = False
|
1439
|
+
) -> int:
|
1440
|
+
"""Handle the ec-delete command"""
|
1441
|
+
info(
|
1442
|
+
f"Preparing to delete erasure-coded file with metadata CID: [bold cyan]{metadata_cid}[/bold cyan]"
|
1443
|
+
)
|
1444
|
+
|
1445
|
+
if not force:
|
1446
|
+
warning("This will delete the metadata and all chunks from the marketplace.")
|
1447
|
+
confirm = input("Continue? (y/n): ").strip().lower()
|
1448
|
+
if confirm != "y":
|
1449
|
+
log("Deletion cancelled", style="yellow")
|
1450
|
+
return 0
|
1451
|
+
|
1452
|
+
try:
|
1453
|
+
info("Deleting erasure-coded file from marketplace...")
|
1454
|
+
result = await client.delete_ec_file(metadata_cid)
|
1455
|
+
|
1456
|
+
if result.get("success"):
|
1457
|
+
success("Erasure-coded file successfully deleted")
|
1458
|
+
|
1459
|
+
# Show detailed results
|
1460
|
+
details = []
|
1461
|
+
chunks_deleted = result.get("chunks_deleted", 0)
|
1462
|
+
details.append(f"Deleted [bold]{chunks_deleted}[/bold] chunks")
|
1463
|
+
|
1464
|
+
if "transaction_hash" in result:
|
1465
|
+
details.append(
|
1466
|
+
f"Transaction hash: [bold]{result['transaction_hash']}[/bold]"
|
1467
|
+
)
|
1468
|
+
|
1469
|
+
print_panel("\n".join(details), title="Deletion Results")
|
1470
|
+
|
1471
|
+
return 0
|
1472
|
+
else:
|
1473
|
+
error(
|
1474
|
+
f"Failed to delete erasure-coded file: {result.get('message', 'Unknown error')}"
|
1475
|
+
)
|
1476
|
+
return 1
|
1477
|
+
|
1478
|
+
except Exception as e:
|
1479
|
+
error(f"Failed to delete erasure-coded file: {e}")
|
1480
|
+
return 1
|
1481
|
+
|
1482
|
+
|
1483
|
+
#
|
1484
|
+
# Configuration Handlers
|
1485
|
+
#
|
1486
|
+
|
1487
|
+
|
1488
|
+
def handle_config_get(section: str, key: str) -> int:
|
1489
|
+
"""Handle the config get command"""
|
1490
|
+
try:
|
1491
|
+
value = get_config_value(section, key)
|
1492
|
+
log(
|
1493
|
+
f"[bold cyan]{section}[/bold cyan].[bold green]{key}[/bold green] = [bold]{value}[/bold]"
|
1494
|
+
)
|
1495
|
+
return 0
|
1496
|
+
except Exception as e:
|
1497
|
+
error(f"Error getting configuration value: {e}")
|
1498
|
+
return 1
|
1499
|
+
|
1500
|
+
|
1501
|
+
def handle_config_set(section: str, key: str, value: str) -> int:
|
1502
|
+
"""Handle the config set command"""
|
1503
|
+
try:
|
1504
|
+
# Convert string "true"/"false" to boolean if applicable
|
1505
|
+
if value.lower() == "true":
|
1506
|
+
value = True
|
1507
|
+
elif value.lower() == "false":
|
1508
|
+
value = False
|
1509
|
+
|
1510
|
+
# Set the configuration value
|
1511
|
+
set_config_value(section, key, value)
|
1512
|
+
success(
|
1513
|
+
f"Set [bold cyan]{section}[/bold cyan].[bold green]{key}[/bold green] = [bold]{value}[/bold]"
|
1514
|
+
)
|
1515
|
+
return 0
|
1516
|
+
except Exception as e:
|
1517
|
+
error(f"Error setting configuration value: {e}")
|
1518
|
+
return 1
|
1519
|
+
|
1520
|
+
|
1521
|
+
def handle_config_list() -> int:
|
1522
|
+
"""Handle the config list command"""
|
1523
|
+
try:
|
1524
|
+
config = get_all_config()
|
1525
|
+
|
1526
|
+
# Format the configuration as a multi-line string
|
1527
|
+
config_lines = ["Current configuration:"]
|
1528
|
+
|
1529
|
+
for section, values in config.items():
|
1530
|
+
config_lines.append(f"\n[bold cyan]{section}[/bold cyan]")
|
1531
|
+
for key, value in values.items():
|
1532
|
+
config_lines.append(
|
1533
|
+
f" [bold green]{key}[/bold green] = [bold]{value}[/bold]"
|
1534
|
+
)
|
1535
|
+
|
1536
|
+
# Print as a panel
|
1537
|
+
print_panel("\n".join(config_lines), title="Configuration")
|
1538
|
+
|
1539
|
+
return 0
|
1540
|
+
except Exception as e:
|
1541
|
+
error(f"Error listing configuration: {e}")
|
1542
|
+
return 1
|
1543
|
+
|
1544
|
+
|
1545
|
+
def handle_config_reset() -> int:
|
1546
|
+
"""Handle the config reset command"""
|
1547
|
+
try:
|
1548
|
+
reset_config()
|
1549
|
+
success("Configuration reset to default values")
|
1550
|
+
return 0
|
1551
|
+
except Exception as e:
|
1552
|
+
error(f"Error resetting configuration: {e}")
|
1553
|
+
return 1
|
1554
|
+
|
1555
|
+
|
1556
|
+
#
|
1557
|
+
# Seed Phrase Handlers
|
1558
|
+
#
|
1559
|
+
|
1560
|
+
|
1561
|
+
def handle_seed_phrase_set(
|
1562
|
+
seed_phrase: str, encode: bool = False, account_name: Optional[str] = None
|
1563
|
+
) -> int:
|
1564
|
+
"""Handle the seed set command"""
|
1565
|
+
try:
|
1566
|
+
# Validate the seed phrase
|
1567
|
+
if not seed_phrase or len(seed_phrase.split()) not in [12, 24]:
|
1568
|
+
error("Seed phrase must be 12 or 24 words")
|
1569
|
+
return 1
|
1570
|
+
|
1571
|
+
# If account name is provided, create a new account
|
1572
|
+
if account_name:
|
1573
|
+
info(f"Setting seed phrase for account: [bold]{account_name}[/bold]")
|
1574
|
+
else:
|
1575
|
+
info("Setting default seed phrase")
|
1576
|
+
|
1577
|
+
# Encrypt if requested
|
1578
|
+
password = None
|
1579
|
+
if encode:
|
1580
|
+
log("\nYou've chosen to encrypt this seed phrase.", style="yellow")
|
1581
|
+
password = getpass.getpass("Enter a password for encryption: ")
|
1582
|
+
confirm = getpass.getpass("Confirm password: ")
|
1583
|
+
|
1584
|
+
if password != confirm:
|
1585
|
+
error("Passwords do not match")
|
1586
|
+
return 1
|
1587
|
+
|
1588
|
+
if not password:
|
1589
|
+
error("Password cannot be empty for encryption")
|
1590
|
+
return 1
|
1591
|
+
|
1592
|
+
# Set the seed phrase
|
1593
|
+
set_seed_phrase(seed_phrase, password, account_name)
|
1594
|
+
|
1595
|
+
# Gather information for the success panel
|
1596
|
+
status_info = []
|
1597
|
+
|
1598
|
+
# Display success message
|
1599
|
+
if encode:
|
1600
|
+
status_info.append(
|
1601
|
+
"[bold green]Seed phrase set and encrypted successfully[/bold green]"
|
1602
|
+
)
|
1603
|
+
else:
|
1604
|
+
status_info.append("[bold green]Seed phrase set successfully[/bold green]")
|
1605
|
+
status_info.append(
|
1606
|
+
"\n[bold yellow]Warning:[/bold yellow] Seed phrase is stored in plaintext. Consider encrypting it with:"
|
1607
|
+
)
|
1608
|
+
status_info.append(
|
1609
|
+
f" [bold]hippius seed encode{' --account ' + account_name if account_name else ''}[/bold]"
|
1610
|
+
)
|
1611
|
+
|
1612
|
+
# If this is a new account, show the address
|
1613
|
+
try:
|
1614
|
+
address = get_account_address(account_name)
|
1615
|
+
status_info.append(f"\nAccount address: [bold cyan]{address}[/bold cyan]")
|
1616
|
+
except:
|
1617
|
+
pass
|
1618
|
+
|
1619
|
+
print_panel("\n".join(status_info), title="Seed Phrase Status")
|
1620
|
+
|
1621
|
+
return 0
|
1622
|
+
except Exception as e:
|
1623
|
+
error(f"Error setting seed phrase: {e}")
|
1624
|
+
return 1
|
1625
|
+
|
1626
|
+
|
1627
|
+
def handle_seed_phrase_encode(account_name: Optional[str] = None) -> int:
|
1628
|
+
"""Handle the seed encode command"""
|
1629
|
+
try:
|
1630
|
+
# Check if account exists and get its encryption status
|
1631
|
+
config = load_config()
|
1632
|
+
accounts = config.get("substrate", {}).get("accounts", {})
|
1633
|
+
|
1634
|
+
# If account name not specified, use active account
|
1635
|
+
if not account_name:
|
1636
|
+
account_name = config.get("substrate", {}).get("active_account")
|
1637
|
+
if not account_name:
|
1638
|
+
error("No account specified and no active account")
|
1639
|
+
return 1
|
1640
|
+
|
1641
|
+
# Check if the account exists
|
1642
|
+
if account_name not in accounts:
|
1643
|
+
error(f"Account '{account_name}' not found")
|
1644
|
+
return 1
|
1645
|
+
|
1646
|
+
# Get account details
|
1647
|
+
account = accounts.get(account_name, {})
|
1648
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
1649
|
+
seed_phrase = account.get("seed_phrase")
|
1650
|
+
|
1651
|
+
# Check if we have a seed phrase
|
1652
|
+
if not seed_phrase:
|
1653
|
+
error(f"Account '{account_name}' doesn't have a seed phrase")
|
1654
|
+
info(
|
1655
|
+
f"Set a seed phrase first with: [bold green underline]hippius seed set <seed_phrase> --account {account_name}[/bold green underline]"
|
1656
|
+
)
|
1657
|
+
return 1
|
1658
|
+
|
1659
|
+
# Check if the seed phrase is already encrypted
|
1660
|
+
if is_encrypted:
|
1661
|
+
info("Seed phrase is already encrypted")
|
1662
|
+
confirm = (
|
1663
|
+
input("Do you want to re-encrypt it with a new password? (y/n): ")
|
1664
|
+
.strip()
|
1665
|
+
.lower()
|
1666
|
+
)
|
1667
|
+
if confirm != "y":
|
1668
|
+
info("Encryption cancelled")
|
1669
|
+
return 0
|
1670
|
+
|
1671
|
+
# Need to decrypt with old password first
|
1672
|
+
old_password = getpass.getpass("Enter your current password to decrypt: ")
|
1673
|
+
decrypted_seed_phrase = decrypt_seed_phrase(old_password, account_name)
|
1674
|
+
|
1675
|
+
if not decrypted_seed_phrase:
|
1676
|
+
error("Unable to decrypt the seed phrase. Incorrect password?")
|
1677
|
+
return 1
|
1678
|
+
|
1679
|
+
# Now we have the decrypted seed phrase
|
1680
|
+
seed_phrase = decrypted_seed_phrase
|
1681
|
+
|
1682
|
+
# Get new password for encryption
|
1683
|
+
info("\nYou are about to encrypt your seed phrase.")
|
1684
|
+
password = getpass.getpass("Enter a password for encryption: ")
|
1685
|
+
confirm = getpass.getpass("Confirm password: ")
|
1686
|
+
|
1687
|
+
if password != confirm:
|
1688
|
+
error("Passwords do not match")
|
1689
|
+
return 1
|
1690
|
+
|
1691
|
+
if not password:
|
1692
|
+
error("Password cannot be empty for encryption")
|
1693
|
+
return 1
|
1694
|
+
|
1695
|
+
# Now encrypt the seed phrase - key fix here passing correct parameters
|
1696
|
+
success = encrypt_seed_phrase(seed_phrase, password, account_name)
|
1697
|
+
|
1698
|
+
# Security: Clear the plaintext seed phrase from memory
|
1699
|
+
# This is a best-effort approach, as Python's garbage collection may still keep copies
|
1700
|
+
seed_phrase = None
|
1701
|
+
|
1702
|
+
if success:
|
1703
|
+
# Create success panel with encryption information
|
1704
|
+
encryption_info = [
|
1705
|
+
f"Account: [bold]{account_name}[/bold]",
|
1706
|
+
"[bold green]Seed phrase encrypted successfully[/bold green]",
|
1707
|
+
"",
|
1708
|
+
"You will need to provide this password when using the account for:",
|
1709
|
+
" - Pinning files to IPFS",
|
1710
|
+
" - Erasure coding with publishing",
|
1711
|
+
" - Any other blockchain operations",
|
1712
|
+
"",
|
1713
|
+
"[bold yellow underline]Security note:[/bold yellow underline] The original unencrypted seed phrase is NOT stored in the config.",
|
1714
|
+
]
|
1715
|
+
|
1716
|
+
# Try to get the address for display
|
1717
|
+
try:
|
1718
|
+
address = get_account_address(account_name)
|
1719
|
+
if address:
|
1720
|
+
encryption_info.append("")
|
1721
|
+
encryption_info.append(
|
1722
|
+
f"Account address: [bold cyan]{address}[/bold cyan]"
|
1723
|
+
)
|
1724
|
+
except Exception:
|
1725
|
+
pass
|
1726
|
+
|
1727
|
+
print_panel("\n".join(encryption_info), title="Encryption Successful")
|
1728
|
+
return 0
|
1729
|
+
else:
|
1730
|
+
error("Failed to encrypt seed phrase")
|
1731
|
+
return 1
|
1732
|
+
|
1733
|
+
except Exception as e:
|
1734
|
+
error(f"Error encrypting seed phrase: {e}")
|
1735
|
+
return 1
|
1736
|
+
|
1737
|
+
|
1738
|
+
def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
|
1739
|
+
"""Handle the seed decode command - temporarily decrypts and displays the seed phrase"""
|
1740
|
+
try:
|
1741
|
+
# Check if seed phrase exists and is encrypted
|
1742
|
+
config = load_config()
|
1743
|
+
accounts = config.get("substrate", {}).get("accounts", {})
|
1744
|
+
|
1745
|
+
if account_name:
|
1746
|
+
account = accounts.get(account_name, {})
|
1747
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
1748
|
+
else:
|
1749
|
+
# Get active account
|
1750
|
+
active_account = config.get("substrate", {}).get("active_account")
|
1751
|
+
if active_account and active_account in accounts:
|
1752
|
+
is_encrypted = accounts[active_account].get(
|
1753
|
+
"seed_phrase_encoded", False
|
1754
|
+
)
|
1755
|
+
else:
|
1756
|
+
# Legacy mode
|
1757
|
+
is_encrypted = config.get("substrate", {}).get(
|
1758
|
+
"seed_phrase_encoded", False
|
1759
|
+
)
|
1760
|
+
|
1761
|
+
if not is_encrypted:
|
1762
|
+
info("Seed phrase is not encrypted")
|
1763
|
+
return 0
|
1764
|
+
|
1765
|
+
# Get password for decryption
|
1766
|
+
password = getpass.getpass("Enter your password to decrypt the seed phrase: ")
|
1767
|
+
|
1768
|
+
if not password:
|
1769
|
+
error("Password cannot be empty")
|
1770
|
+
return 1
|
1771
|
+
|
1772
|
+
# Try to decrypt the seed phrase
|
1773
|
+
try:
|
1774
|
+
seed_phrase = decrypt_seed_phrase(password, account_name)
|
1775
|
+
|
1776
|
+
if seed_phrase:
|
1777
|
+
# Create info panel for the decrypted seed phrase
|
1778
|
+
seed_info = [
|
1779
|
+
f"Decrypted seed phrase: [bold yellow]{seed_phrase}[/bold yellow]",
|
1780
|
+
"",
|
1781
|
+
"[bold green underline]NOTE: This is a temporary decryption only. Your seed phrase remains encrypted in the config.[/bold green underline]",
|
1782
|
+
"",
|
1783
|
+
"[bold red underline]SECURITY WARNING:[/bold red underline]",
|
1784
|
+
"- Your seed phrase gives full access to your account funds",
|
1785
|
+
"- Never share it with anyone or store it in an insecure location",
|
1786
|
+
"- Be aware that displaying it on screen could expose it to screen capture",
|
1787
|
+
"- Consider clearing your terminal history after this operation",
|
1788
|
+
]
|
1789
|
+
|
1790
|
+
print_panel("\n".join(seed_info), title="Seed Phrase Decoded")
|
1791
|
+
|
1792
|
+
# Security: Clear the plaintext seed phrase from memory
|
1793
|
+
# This is a best-effort approach, as Python's garbage collection may still keep copies
|
1794
|
+
seed_phrase = None
|
1795
|
+
|
1796
|
+
return 0
|
1797
|
+
else:
|
1798
|
+
error("Failed to decrypt seed phrase")
|
1799
|
+
return 1
|
1800
|
+
|
1801
|
+
except Exception as e:
|
1802
|
+
error(f"Error decrypting seed phrase: {e}")
|
1803
|
+
|
1804
|
+
if "decryption failed" in str(e).lower():
|
1805
|
+
warning("Incorrect password")
|
1806
|
+
|
1807
|
+
return 1
|
1808
|
+
|
1809
|
+
except Exception as e:
|
1810
|
+
error(f"{e}")
|
1811
|
+
return 1
|
1812
|
+
|
1813
|
+
|
1814
|
+
def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
|
1815
|
+
"""Handle the seed status command"""
|
1816
|
+
try:
|
1817
|
+
# Load configuration
|
1818
|
+
config = load_config()
|
1819
|
+
|
1820
|
+
if account_name:
|
1821
|
+
print(f"Checking seed phrase status for account: {account_name}")
|
1822
|
+
|
1823
|
+
# Check if account exists
|
1824
|
+
accounts = config.get("substrate", {}).get("accounts", {})
|
1825
|
+
if account_name not in accounts:
|
1826
|
+
print(f"Account '{account_name}' not found")
|
1827
|
+
return 1
|
1828
|
+
|
1829
|
+
account = accounts[account_name]
|
1830
|
+
has_seed = "seed_phrase" in account
|
1831
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
1832
|
+
is_active = account_name == get_active_account()
|
1833
|
+
|
1834
|
+
print("\nAccount Status:")
|
1835
|
+
print(f" Account Name: {account_name}")
|
1836
|
+
print(f" Has Seed Phrase: {'Yes' if has_seed else 'No'}")
|
1837
|
+
print(f" Encrypted: {'Yes' if is_encrypted else 'No'}")
|
1838
|
+
print(f" Active: {'Yes' if is_active else 'No'}")
|
1839
|
+
|
1840
|
+
if has_seed:
|
1841
|
+
try:
|
1842
|
+
# Try to get the address (will use cached if available)
|
1843
|
+
address = get_account_address(account_name)
|
1844
|
+
print(f" Address: {address}")
|
1845
|
+
except Exception as e:
|
1846
|
+
if is_encrypted:
|
1847
|
+
print(" Address: Encrypted (password required to view)")
|
1848
|
+
else:
|
1849
|
+
print(f" Address: Unable to derive (Error: {e})")
|
1850
|
+
|
1851
|
+
else:
|
1852
|
+
print("Checking active account seed phrase status")
|
1853
|
+
|
1854
|
+
# Get the active account
|
1855
|
+
active_account = get_active_account()
|
1856
|
+
if active_account:
|
1857
|
+
accounts = config.get("substrate", {}).get("accounts", {})
|
1858
|
+
if active_account in accounts:
|
1859
|
+
account = accounts[active_account]
|
1860
|
+
has_seed = "seed_phrase" in account
|
1861
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
1862
|
+
|
1863
|
+
print(f"\nActive Account: {active_account}")
|
1864
|
+
print(f" Has Seed Phrase: {'Yes' if has_seed else 'No'}")
|
1865
|
+
print(f" Encrypted: {'Yes' if is_encrypted else 'No'}")
|
1866
|
+
|
1867
|
+
if has_seed:
|
1868
|
+
try:
|
1869
|
+
# Try to get the address (will use cached if available)
|
1870
|
+
address = get_account_address(active_account)
|
1871
|
+
print(f" Address: {address}")
|
1872
|
+
except Exception as e:
|
1873
|
+
if is_encrypted:
|
1874
|
+
print(
|
1875
|
+
" Address: Encrypted (password required to view)"
|
1876
|
+
)
|
1877
|
+
else:
|
1878
|
+
print(f" Address: Unable to derive (Error: {e})")
|
1879
|
+
else:
|
1880
|
+
print(
|
1881
|
+
f"\nActive account '{active_account}' not found in configuration"
|
1882
|
+
)
|
1883
|
+
else:
|
1884
|
+
print("\nNo active account set")
|
1885
|
+
|
1886
|
+
return 0
|
1887
|
+
|
1888
|
+
except Exception as e:
|
1889
|
+
print(f"Error checking seed phrase status: {e}")
|
1890
|
+
return 1
|
1891
|
+
|
1892
|
+
|
1893
|
+
#
|
1894
|
+
# Account Management Handlers
|
1895
|
+
#
|
1896
|
+
|
1897
|
+
|
1898
|
+
def handle_account_info(account_name: Optional[str] = None) -> int:
|
1899
|
+
"""Handle the account info command - displays detailed information about an account"""
|
1900
|
+
try:
|
1901
|
+
# Load configuration
|
1902
|
+
config = load_config()
|
1903
|
+
|
1904
|
+
# If account name not specified, use active account
|
1905
|
+
if not account_name:
|
1906
|
+
account_name = config.get("substrate", {}).get("active_account")
|
1907
|
+
if not account_name:
|
1908
|
+
error("No account specified and no active account")
|
1909
|
+
return 1
|
1910
|
+
|
1911
|
+
# Check if account exists
|
1912
|
+
accounts = config.get("substrate", {}).get("accounts", {})
|
1913
|
+
if account_name not in accounts:
|
1914
|
+
error(f"Account '{account_name}' not found")
|
1915
|
+
return 1
|
1916
|
+
|
1917
|
+
# Get account details
|
1918
|
+
account = accounts[account_name]
|
1919
|
+
has_seed = "seed_phrase" in account
|
1920
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
1921
|
+
is_active = account_name == get_active_account()
|
1922
|
+
ss58_address = account.get("ss58_address", "")
|
1923
|
+
|
1924
|
+
# Account information panel with rich formatting
|
1925
|
+
account_info = [
|
1926
|
+
f"Account Name: [bold]{account_name}[/bold]",
|
1927
|
+
f"Active: [bold cyan]{'Yes' if is_active else 'No'}[/bold cyan]",
|
1928
|
+
f"Has Seed Phrase: [bold]{'Yes' if has_seed else 'No'}[/bold]",
|
1929
|
+
f"Encryption: [bold {'green' if is_encrypted else 'yellow'}]{'Encrypted' if is_encrypted else 'Unencrypted'}[/bold {'green' if is_encrypted else 'yellow'}]",
|
1930
|
+
]
|
1931
|
+
|
1932
|
+
if ss58_address:
|
1933
|
+
account_info.append(f"SS58 Address: [bold cyan]{ss58_address}[/bold cyan]")
|
1934
|
+
elif has_seed:
|
1935
|
+
if is_encrypted:
|
1936
|
+
account_info.append(
|
1937
|
+
"[dim]Address: Encrypted (password required to view)[/dim]"
|
1938
|
+
)
|
1939
|
+
else:
|
1940
|
+
try:
|
1941
|
+
# Try to get the address
|
1942
|
+
address = get_account_address(account_name)
|
1943
|
+
account_info.append(
|
1944
|
+
f"SS58 Address: [bold cyan]{address}[/bold cyan]"
|
1945
|
+
)
|
1946
|
+
except Exception as e:
|
1947
|
+
account_info.append(
|
1948
|
+
f"[yellow]Unable to derive address: {e}[/yellow]"
|
1949
|
+
)
|
1950
|
+
|
1951
|
+
# Add suggestions based on account status
|
1952
|
+
account_info.append("")
|
1953
|
+
if is_active:
|
1954
|
+
account_info.append("[bold green]This is your active account[/bold green]")
|
1955
|
+
else:
|
1956
|
+
account_info.append(
|
1957
|
+
f"[dim]To use this account: [bold green underline]hippius account switch {account_name}[/bold green underline][/dim]"
|
1958
|
+
)
|
1959
|
+
|
1960
|
+
if has_seed and not is_encrypted:
|
1961
|
+
account_info.append(
|
1962
|
+
f"[bold yellow underline]WARNING:[/bold yellow underline] Seed phrase is not encrypted"
|
1963
|
+
)
|
1964
|
+
account_info.append(
|
1965
|
+
f"[dim]To encrypt: [bold green underline]hippius account encode --name {account_name}[/bold green underline][/dim]"
|
1966
|
+
)
|
1967
|
+
|
1968
|
+
# Print the panel with rich formatting
|
1969
|
+
print_panel(
|
1970
|
+
"\n".join(account_info), title=f"Account Information: {account_name}"
|
1971
|
+
)
|
1972
|
+
|
1973
|
+
return 0
|
1974
|
+
|
1975
|
+
except Exception as e:
|
1976
|
+
error(f"Error getting account info: {e}")
|
1977
|
+
return 1
|
1978
|
+
|
1979
|
+
|
1980
|
+
def handle_account_create(
|
1981
|
+
client: HippiusClient, name: str, encrypt: bool = False
|
1982
|
+
) -> int:
|
1983
|
+
"""Handle the account create command"""
|
1984
|
+
try:
|
1985
|
+
# Check if account already exists
|
1986
|
+
accounts = list_accounts()
|
1987
|
+
if name in accounts:
|
1988
|
+
print(f"Error: Account '{name}' already exists")
|
1989
|
+
return 1
|
1990
|
+
|
1991
|
+
print(f"Creating new account: {name}")
|
1992
|
+
|
1993
|
+
# Import Keypair at the beginning to ensure it's available
|
1994
|
+
from substrateinterface import Keypair
|
1995
|
+
|
1996
|
+
# Generate a new keypair (seed phrase)
|
1997
|
+
seed_phrase = client.substrate_client.generate_seed_phrase()
|
1998
|
+
|
1999
|
+
if not seed_phrase:
|
2000
|
+
print("Error: Failed to generate seed phrase")
|
2001
|
+
return 1
|
2002
|
+
|
2003
|
+
# Process encryption
|
2004
|
+
password = None
|
2005
|
+
if encrypt:
|
2006
|
+
print("\nYou've chosen to encrypt this seed phrase.")
|
2007
|
+
password = getpass.getpass("Enter a password for encryption: ")
|
2008
|
+
confirm = getpass.getpass("Confirm password: ")
|
2009
|
+
|
2010
|
+
if password != confirm:
|
2011
|
+
print("Error: Passwords do not match")
|
2012
|
+
return 1
|
2013
|
+
|
2014
|
+
if not password:
|
2015
|
+
print("Error: Password cannot be empty for encryption")
|
2016
|
+
return 1
|
2017
|
+
|
2018
|
+
# Set the seed phrase for the new account
|
2019
|
+
# First load the config to directly edit it
|
2020
|
+
config = load_config()
|
2021
|
+
|
2022
|
+
# Ensure accounts structure exists
|
2023
|
+
if "accounts" not in config["substrate"]:
|
2024
|
+
config["substrate"]["accounts"] = {}
|
2025
|
+
|
2026
|
+
# Create keypair directly from seed phrase
|
2027
|
+
keypair = Keypair.create_from_mnemonic(seed_phrase)
|
2028
|
+
address = keypair.ss58_address
|
2029
|
+
|
2030
|
+
# Add the new account
|
2031
|
+
config["substrate"]["accounts"][name] = {
|
2032
|
+
"seed_phrase": seed_phrase,
|
2033
|
+
"seed_phrase_encoded": False,
|
2034
|
+
"seed_phrase_salt": None,
|
2035
|
+
"ss58_address": address,
|
2036
|
+
}
|
2037
|
+
|
2038
|
+
# Set as active account
|
2039
|
+
config["substrate"]["active_account"] = name
|
2040
|
+
|
2041
|
+
# Save the config
|
2042
|
+
save_config(config)
|
2043
|
+
|
2044
|
+
# Print account information using rich formatting
|
2045
|
+
account_info = [
|
2046
|
+
f"Account: [bold]{name}[/bold]",
|
2047
|
+
f"Address: [bold cyan]{address}[/bold cyan]",
|
2048
|
+
f"Seed phrase: [bold yellow]{seed_phrase}[/bold yellow]",
|
2049
|
+
"",
|
2050
|
+
"[bold red underline]IMPORTANT:[/bold red underline] Keep your seed phrase safe. It's the only way to recover your account!",
|
2051
|
+
]
|
2052
|
+
|
2053
|
+
# Add encryption status
|
2054
|
+
if encrypt:
|
2055
|
+
account_info.append("")
|
2056
|
+
account_info.append(
|
2057
|
+
"[bold green]Your seed phrase is encrypted.[/bold green]"
|
2058
|
+
)
|
2059
|
+
account_info.append(
|
2060
|
+
"You'll need to provide the password whenever using this account."
|
2061
|
+
)
|
2062
|
+
else:
|
2063
|
+
account_info.append("")
|
2064
|
+
account_info.append(
|
2065
|
+
"[bold yellow underline]WARNING:[/bold yellow underline] Your seed phrase is stored unencrypted."
|
2066
|
+
)
|
2067
|
+
account_info.append(
|
2068
|
+
f"[bold green underline]Consider encrypting it with: hippius account encode --name {name}[/bold green underline]"
|
2069
|
+
)
|
2070
|
+
|
2071
|
+
account_info.append("")
|
2072
|
+
account_info.append(
|
2073
|
+
"This account is now active. Use it with: [bold]hippius <command>[/bold]"
|
2074
|
+
)
|
2075
|
+
|
2076
|
+
# Print the panel with rich formatting
|
2077
|
+
print_panel("\n".join(account_info), title="Account Created Successfully")
|
2078
|
+
|
2079
|
+
return 0
|
2080
|
+
|
2081
|
+
except Exception as e:
|
2082
|
+
error(f"Error creating account: {e}")
|
2083
|
+
return 1
|
2084
|
+
|
2085
|
+
|
2086
|
+
def handle_account_export(
|
2087
|
+
client: HippiusClient, name: Optional[str] = None, file_path: Optional[str] = None
|
2088
|
+
) -> int:
|
2089
|
+
"""Handle the account export command"""
|
2090
|
+
try:
|
2091
|
+
# Determine account to export
|
2092
|
+
account_name = name or get_active_account()
|
2093
|
+
|
2094
|
+
if not account_name:
|
2095
|
+
print("Error: No account specified and no active account found")
|
2096
|
+
print("Use --name to specify an account to export")
|
2097
|
+
return 1
|
2098
|
+
|
2099
|
+
print(f"Exporting account: {account_name}")
|
2100
|
+
|
2101
|
+
# Default file path if not provided
|
2102
|
+
if not file_path:
|
2103
|
+
file_path = f"{account_name}_hippius_account.json"
|
2104
|
+
|
2105
|
+
# Export the account
|
2106
|
+
config = load_config()
|
2107
|
+
accounts = config.get("substrate", {}).get("accounts", {})
|
2108
|
+
|
2109
|
+
if account_name not in accounts:
|
2110
|
+
print(f"Error: Account '{account_name}' not found")
|
2111
|
+
return 1
|
2112
|
+
|
2113
|
+
# Get the account data
|
2114
|
+
account_data = accounts[account_name]
|
2115
|
+
|
2116
|
+
# Create export data
|
2117
|
+
export_data = {
|
2118
|
+
"name": account_name,
|
2119
|
+
"encrypted": account_data.get("encrypted", False),
|
2120
|
+
"seed_phrase": account_data.get("seed_phrase", ""),
|
2121
|
+
"address": account_data.get("address", ""),
|
2122
|
+
}
|
2123
|
+
|
2124
|
+
# Save to file
|
2125
|
+
with open(file_path, "w") as f:
|
2126
|
+
json.dump(export_data, f, indent=2)
|
2127
|
+
|
2128
|
+
print(f"Account exported to: {file_path}")
|
2129
|
+
|
2130
|
+
# Security warning
|
2131
|
+
if not export_data.get("encrypted"):
|
2132
|
+
print("\nWARNING: This export file contains an unencrypted seed phrase.")
|
2133
|
+
print("Keep this file secure and never share it with anyone.")
|
2134
|
+
|
2135
|
+
return 0
|
2136
|
+
|
2137
|
+
except Exception as e:
|
2138
|
+
print(f"Error exporting account: {e}")
|
2139
|
+
return 1
|
2140
|
+
|
2141
|
+
|
2142
|
+
def handle_account_import(
|
2143
|
+
client: HippiusClient, file_path: str, encrypt: bool = False
|
2144
|
+
) -> int:
|
2145
|
+
"""Handle the account import command"""
|
2146
|
+
try:
|
2147
|
+
# Verify file exists
|
2148
|
+
if not os.path.exists(file_path):
|
2149
|
+
print(f"Error: File {file_path} not found")
|
2150
|
+
return 1
|
2151
|
+
|
2152
|
+
print(f"Importing account from: {file_path}")
|
2153
|
+
|
2154
|
+
# Read and parse the file
|
2155
|
+
try:
|
2156
|
+
with open(file_path, "r") as f:
|
2157
|
+
import_data = json.load(f)
|
2158
|
+
|
2159
|
+
# Validate data
|
2160
|
+
if not isinstance(import_data, dict):
|
2161
|
+
print("Error: Invalid account file format")
|
2162
|
+
return 1
|
2163
|
+
|
2164
|
+
account_name = import_data.get("name")
|
2165
|
+
seed_phrase = import_data.get("seed_phrase")
|
2166
|
+
is_encrypted = import_data.get("encrypted", False)
|
2167
|
+
|
2168
|
+
if not account_name:
|
2169
|
+
print("Error: Missing account name in import file")
|
2170
|
+
return 1
|
2171
|
+
|
2172
|
+
if not seed_phrase:
|
2173
|
+
print("Error: Missing seed phrase in import file")
|
2174
|
+
return 1
|
2175
|
+
|
2176
|
+
except Exception as e:
|
2177
|
+
print(f"Error reading account file: {e}")
|
2178
|
+
return 1
|
2179
|
+
|
2180
|
+
# Check if account already exists
|
2181
|
+
accounts = list_accounts()
|
2182
|
+
if account_name in accounts:
|
2183
|
+
print(f"Warning: Account '{account_name}' already exists")
|
2184
|
+
overwrite = input("Overwrite existing account? (y/n): ").strip().lower()
|
2185
|
+
if overwrite != "y":
|
2186
|
+
print("Import cancelled")
|
2187
|
+
return 0
|
2188
|
+
|
2189
|
+
# Handle encryption
|
2190
|
+
password = None
|
2191
|
+
|
2192
|
+
# If importing encrypted account
|
2193
|
+
if is_encrypted:
|
2194
|
+
print("\nThis account has an encrypted seed phrase.")
|
2195
|
+
if encrypt:
|
2196
|
+
# Re-encrypt with new password
|
2197
|
+
print("You've chosen to re-encrypt this account.")
|
2198
|
+
old_password = getpass.getpass("Enter the original password: ")
|
2199
|
+
|
2200
|
+
# Try to decrypt first
|
2201
|
+
try:
|
2202
|
+
# Create temporary decryption box
|
2203
|
+
if ENCRYPTION_AVAILABLE:
|
2204
|
+
# Derive key from password
|
2205
|
+
import hashlib
|
2206
|
+
|
2207
|
+
import nacl.secret
|
2208
|
+
import nacl.utils
|
2209
|
+
from nacl.exceptions import CryptoError
|
2210
|
+
|
2211
|
+
key = hashlib.sha256(old_password.encode()).digest()
|
2212
|
+
box = nacl.secret.SecretBox(key)
|
2213
|
+
|
2214
|
+
# Try decryption
|
2215
|
+
try:
|
2216
|
+
# Split the nonce and ciphertext
|
2217
|
+
data = base64.b64decode(seed_phrase)
|
2218
|
+
nonce = data[: box.NONCE_SIZE]
|
2219
|
+
ciphertext = data[box.NONCE_SIZE :]
|
2220
|
+
|
2221
|
+
# Decrypt
|
2222
|
+
decrypted = box.decrypt(ciphertext, nonce)
|
2223
|
+
seed_phrase = decrypted.decode("utf-8")
|
2224
|
+
|
2225
|
+
# Now get new password for re-encryption
|
2226
|
+
new_password = getpass.getpass(
|
2227
|
+
"Enter new password for encryption: "
|
2228
|
+
)
|
2229
|
+
confirm = getpass.getpass("Confirm new password: ")
|
2230
|
+
|
2231
|
+
if new_password != confirm:
|
2232
|
+
print("Error: Passwords do not match")
|
2233
|
+
return 1
|
2234
|
+
|
2235
|
+
password = new_password
|
2236
|
+
|
2237
|
+
except CryptoError:
|
2238
|
+
print("Error: Incorrect password for encrypted seed phrase")
|
2239
|
+
return 1
|
2240
|
+
else:
|
2241
|
+
print("Error: PyNaCl is required for encryption/decryption.")
|
2242
|
+
print("Install it with: pip install pynacl")
|
2243
|
+
return 1
|
2244
|
+
except Exception as e:
|
2245
|
+
print(f"Error decrypting seed phrase: {e}")
|
2246
|
+
return 1
|
2247
|
+
else:
|
2248
|
+
# Keep existing encryption
|
2249
|
+
print("Importing with existing encryption.")
|
2250
|
+
print("You'll need the original password to use this account.")
|
2251
|
+
elif encrypt:
|
2252
|
+
# Encrypt an unencrypted import
|
2253
|
+
print("\nYou've chosen to encrypt this account during import.")
|
2254
|
+
password = getpass.getpass("Enter a password for encryption: ")
|
2255
|
+
confirm = getpass.getpass("Confirm password: ")
|
2256
|
+
|
2257
|
+
if password != confirm:
|
2258
|
+
print("Error: Passwords do not match")
|
2259
|
+
return 1
|
2260
|
+
|
2261
|
+
if not password:
|
2262
|
+
print("Error: Password cannot be empty for encryption")
|
2263
|
+
return 1
|
2264
|
+
|
2265
|
+
# Import the account
|
2266
|
+
set_seed_phrase(seed_phrase, password, account_name)
|
2267
|
+
|
2268
|
+
# Set as active account
|
2269
|
+
set_active_account(account_name)
|
2270
|
+
|
2271
|
+
print(f"\nSuccessfully imported account: {account_name}")
|
2272
|
+
print("This account is now active.")
|
2273
|
+
|
2274
|
+
# If address is provided in import data, show it
|
2275
|
+
if "address" in import_data and import_data["address"]:
|
2276
|
+
print(f"Address: {import_data['address']}")
|
2277
|
+
else:
|
2278
|
+
# Try to get address
|
2279
|
+
try:
|
2280
|
+
address = get_account_address(account_name)
|
2281
|
+
print(f"Address: {address}")
|
2282
|
+
except:
|
2283
|
+
if is_encrypted or encrypt:
|
2284
|
+
print("Address: Encrypted (password required to view)")
|
2285
|
+
else:
|
2286
|
+
print("Address: Unable to derive")
|
2287
|
+
|
2288
|
+
return 0
|
2289
|
+
|
2290
|
+
except Exception as e:
|
2291
|
+
print(f"Error importing account: {e}")
|
2292
|
+
return 1
|
2293
|
+
|
2294
|
+
|
2295
|
+
def handle_account_list() -> int:
|
2296
|
+
"""Handle the account list command"""
|
2297
|
+
try:
|
2298
|
+
accounts = list_accounts()
|
2299
|
+
active_account = get_active_account()
|
2300
|
+
|
2301
|
+
if not accounts:
|
2302
|
+
log("No accounts found", style="yellow")
|
2303
|
+
return 0
|
2304
|
+
|
2305
|
+
info(f"Found [bold]{len(accounts)}[/bold] accounts:")
|
2306
|
+
|
2307
|
+
# Load config to get more details
|
2308
|
+
config = load_config()
|
2309
|
+
account_config = config.get("substrate", {}).get("accounts", {})
|
2310
|
+
|
2311
|
+
# Create data for a table
|
2312
|
+
account_data_list = []
|
2313
|
+
for i, account_name in enumerate(accounts, 1):
|
2314
|
+
account_data = account_config.get(account_name, {})
|
2315
|
+
|
2316
|
+
is_active = account_name == active_account
|
2317
|
+
has_seed = "seed_phrase" in account_data
|
2318
|
+
is_encrypted = account_data.get("seed_phrase_encoded", False)
|
2319
|
+
|
2320
|
+
# Get address
|
2321
|
+
address = account_data.get("ss58_address", "")
|
2322
|
+
|
2323
|
+
# Add to table data
|
2324
|
+
row = {
|
2325
|
+
"Index": str(i),
|
2326
|
+
"Name": account_name,
|
2327
|
+
"Status": "[bold green]Active[/bold green]" if is_active else "",
|
2328
|
+
"Encrypted": "[yellow]Yes[/yellow]" if is_encrypted else "No",
|
2329
|
+
"Address": address if address else "",
|
2330
|
+
"Has seed": has_seed,
|
2331
|
+
}
|
2332
|
+
account_data_list.append(row)
|
2333
|
+
|
2334
|
+
# Display accounts in a table
|
2335
|
+
print_table(
|
2336
|
+
title="Accounts",
|
2337
|
+
data=account_data_list,
|
2338
|
+
columns=["Index", "Name", "Status", "Encrypted", "Address", "Has seed"],
|
2339
|
+
)
|
2340
|
+
|
2341
|
+
# Show active account status
|
2342
|
+
if active_account:
|
2343
|
+
success(f"Active account: [bold]{active_account}[/bold]")
|
2344
|
+
else:
|
2345
|
+
warning("No active account selected")
|
2346
|
+
|
2347
|
+
# Instructions
|
2348
|
+
help_text = [
|
2349
|
+
"To switch accounts: [bold green underline]hippius account switch <account_name>[/bold green underline]",
|
2350
|
+
"To create a new account: [bold green underline]hippius account create --name <account_name>[/bold green underline]",
|
2351
|
+
]
|
2352
|
+
print_panel("\n".join(help_text), title="Account Management")
|
2353
|
+
|
2354
|
+
return 0
|
2355
|
+
|
2356
|
+
except Exception as e:
|
2357
|
+
error(f"Error listing accounts: {e}")
|
2358
|
+
return 1
|
2359
|
+
|
2360
|
+
|
2361
|
+
def handle_account_switch(account_name: str) -> int:
|
2362
|
+
"""Handle the account switch command"""
|
2363
|
+
try:
|
2364
|
+
# Check if account exists
|
2365
|
+
accounts = list_accounts()
|
2366
|
+
if account_name not in accounts:
|
2367
|
+
print(f"Error: Account '{account_name}' not found")
|
2368
|
+
print("Available accounts:")
|
2369
|
+
for account in accounts:
|
2370
|
+
print(f" {account}")
|
2371
|
+
return 1
|
2372
|
+
|
2373
|
+
# Set as active account
|
2374
|
+
set_active_account(account_name)
|
2375
|
+
|
2376
|
+
print(f"Switched to account: {account_name}")
|
2377
|
+
|
2378
|
+
# Show account address if possible
|
2379
|
+
try:
|
2380
|
+
address = get_account_address(account_name)
|
2381
|
+
print(f"Address: {address}")
|
2382
|
+
except Exception as e:
|
2383
|
+
# Check if encrypted
|
2384
|
+
config = load_config()
|
2385
|
+
account_config = (
|
2386
|
+
config.get("substrate", {}).get("accounts", {}).get(account_name, {})
|
2387
|
+
)
|
2388
|
+
|
2389
|
+
if account_config.get("encrypted", False):
|
2390
|
+
print("Address: Encrypted (password required to view)")
|
2391
|
+
else:
|
2392
|
+
print(f"Note: Unable to display address ({str(e)})")
|
2393
|
+
|
2394
|
+
return 0
|
2395
|
+
|
2396
|
+
except Exception as e:
|
2397
|
+
print(f"Error switching account: {e}")
|
2398
|
+
return 1
|
2399
|
+
|
2400
|
+
|
2401
|
+
def handle_account_login() -> int:
|
2402
|
+
"""Handle the account login command - prompts for account details and creates an account"""
|
2403
|
+
try:
|
2404
|
+
# Display the login banner
|
2405
|
+
from hippius_sdk.cli_assets import LOGIN_ASSET
|
2406
|
+
|
2407
|
+
console.print(LOGIN_ASSET, style="bold cyan")
|
2408
|
+
console.print(
|
2409
|
+
"\n[bold blue]Welcome to Hippius![/bold blue] Let's set up your account.\n"
|
2410
|
+
)
|
2411
|
+
|
2412
|
+
# Create a style for prompts
|
2413
|
+
prompt_style = "bold green"
|
2414
|
+
input_style = "bold cyan"
|
2415
|
+
|
2416
|
+
# Prompt for account name with nice formatting
|
2417
|
+
console.print(
|
2418
|
+
"[bold]Step 1:[/bold] Choose a name for your account", style=prompt_style
|
2419
|
+
)
|
2420
|
+
console.print(
|
2421
|
+
"This name will be used to identify your account in the Hippius system.",
|
2422
|
+
style="dim",
|
2423
|
+
)
|
2424
|
+
console.print("Account name:", style=input_style, end=" ")
|
2425
|
+
name = input().strip()
|
2426
|
+
|
2427
|
+
if not name:
|
2428
|
+
error("[bold red]Account name cannot be empty[/bold red]")
|
2429
|
+
return 1
|
2430
|
+
|
2431
|
+
# Check if account already exists
|
2432
|
+
accounts = list_accounts()
|
2433
|
+
if name in accounts:
|
2434
|
+
warning(f"Account '[bold]{name}[/bold]' already exists")
|
2435
|
+
console.print(
|
2436
|
+
"Do you want to overwrite it? (y/n):", style=input_style, end=" "
|
2437
|
+
)
|
2438
|
+
confirm = input().strip().lower()
|
2439
|
+
if confirm != "y":
|
2440
|
+
info("Login cancelled")
|
2441
|
+
return 0
|
2442
|
+
|
2443
|
+
# Prompt for seed phrase with detailed explanation
|
2444
|
+
console.print(
|
2445
|
+
"\n[bold]Step 2:[/bold] Enter your seed phrase", style=prompt_style
|
2446
|
+
)
|
2447
|
+
console.print(
|
2448
|
+
"Your seed phrase gives access to your blockchain account and funds.",
|
2449
|
+
style="dim",
|
2450
|
+
)
|
2451
|
+
console.print(
|
2452
|
+
"[yellow]Important:[/yellow] Must be 12 or 24 words separated by spaces.",
|
2453
|
+
style="dim",
|
2454
|
+
)
|
2455
|
+
console.print("Seed phrase:", style=input_style, end=" ")
|
2456
|
+
seed_phrase = input().strip()
|
2457
|
+
|
2458
|
+
# Validate the seed phrase
|
2459
|
+
if not seed_phrase or len(seed_phrase.split()) not in [12, 24]:
|
2460
|
+
error(
|
2461
|
+
"[bold red]Invalid seed phrase[/bold red] - must be 12 or 24 words separated by spaces"
|
2462
|
+
)
|
2463
|
+
return 1
|
2464
|
+
|
2465
|
+
# Prompt for encryption with security explanation
|
2466
|
+
console.print("\n[bold]Step 3:[/bold] Secure your account", style=prompt_style)
|
2467
|
+
console.print(
|
2468
|
+
"Encrypting your seed phrase adds an extra layer of security.", style="dim"
|
2469
|
+
)
|
2470
|
+
console.print(
|
2471
|
+
"[bold yellow]Strongly recommended[/bold yellow] to protect your account.",
|
2472
|
+
style="dim",
|
2473
|
+
)
|
2474
|
+
console.print(
|
2475
|
+
"Encrypt seed phrase? [bold green](Y/n)[/bold green]:",
|
2476
|
+
style=input_style,
|
2477
|
+
end=" ",
|
2478
|
+
)
|
2479
|
+
encrypt_input = input().strip().lower()
|
2480
|
+
encrypt = encrypt_input == "y" or encrypt_input == "" or encrypt_input == "yes"
|
2481
|
+
|
2482
|
+
# Set up encryption if requested
|
2483
|
+
password = None
|
2484
|
+
if encrypt:
|
2485
|
+
console.print(
|
2486
|
+
"\n[bold]Step 4:[/bold] Set encryption password", style=prompt_style
|
2487
|
+
)
|
2488
|
+
console.print(
|
2489
|
+
"This password will be required whenever you use your account for blockchain operations.",
|
2490
|
+
style="dim",
|
2491
|
+
)
|
2492
|
+
|
2493
|
+
password = getpass.getpass("Enter a password: ")
|
2494
|
+
confirm = getpass.getpass("Confirm password: ")
|
2495
|
+
|
2496
|
+
if password != confirm:
|
2497
|
+
error("[bold red]Passwords do not match[/bold red]")
|
2498
|
+
return 1
|
2499
|
+
|
2500
|
+
if not password:
|
2501
|
+
error("[bold red]Password cannot be empty for encryption[/bold red]")
|
2502
|
+
return 1
|
2503
|
+
|
2504
|
+
# Initialize address variable
|
2505
|
+
address = None
|
2506
|
+
|
2507
|
+
# Create and store the account
|
2508
|
+
with console.status("[cyan]Setting up your account...[/cyan]", spinner="dots"):
|
2509
|
+
# First, directly modify the config to ensure account is created
|
2510
|
+
config = load_config()
|
2511
|
+
|
2512
|
+
# Ensure accounts structure exists
|
2513
|
+
if "substrate" not in config:
|
2514
|
+
config["substrate"] = {}
|
2515
|
+
if "accounts" not in config["substrate"]:
|
2516
|
+
config["substrate"]["accounts"] = {}
|
2517
|
+
|
2518
|
+
# Create keypair and get address from seed phrase
|
2519
|
+
from substrateinterface import Keypair
|
2520
|
+
|
2521
|
+
keypair = Keypair.create_from_mnemonic(seed_phrase)
|
2522
|
+
address = keypair.ss58_address
|
2523
|
+
|
2524
|
+
# Add the new account
|
2525
|
+
config["substrate"]["accounts"][name] = {
|
2526
|
+
"seed_phrase": seed_phrase,
|
2527
|
+
"seed_phrase_encoded": False,
|
2528
|
+
"seed_phrase_salt": None,
|
2529
|
+
"ss58_address": address,
|
2530
|
+
}
|
2531
|
+
|
2532
|
+
# Set as active account
|
2533
|
+
config["substrate"]["active_account"] = name
|
2534
|
+
|
2535
|
+
# Save the config first
|
2536
|
+
save_config(config)
|
2537
|
+
|
2538
|
+
# Now encrypt if requested
|
2539
|
+
if encrypt:
|
2540
|
+
encrypt_seed_phrase(seed_phrase, password, name)
|
2541
|
+
|
2542
|
+
time.sleep(0.5) # Small delay for visual feedback
|
2543
|
+
|
2544
|
+
# Success panel with account information
|
2545
|
+
account_info = [
|
2546
|
+
f"[bold]Account Name:[/bold] [bold magenta]{name}[/bold magenta]",
|
2547
|
+
f"[bold]Blockchain Address:[/bold] [bold cyan]{address}[/bold cyan]",
|
2548
|
+
"",
|
2549
|
+
"[bold green]✓ Login successful![/bold green]",
|
2550
|
+
"[bold green]✓ Account set as active[/bold green]",
|
2551
|
+
]
|
2552
|
+
|
2553
|
+
if encrypt:
|
2554
|
+
account_info.append("[bold green]✓ Seed phrase encrypted[/bold green]")
|
2555
|
+
account_info.append("")
|
2556
|
+
account_info.append(
|
2557
|
+
"[dim]You'll need your password when using this account for blockchain operations.[/dim]"
|
2558
|
+
)
|
2559
|
+
else:
|
2560
|
+
account_info.append(
|
2561
|
+
"[bold yellow]⚠ Seed phrase not encrypted[/bold yellow]"
|
2562
|
+
)
|
2563
|
+
account_info.append("")
|
2564
|
+
account_info.append(
|
2565
|
+
"[dim]For better security, consider encrypting your seed phrase:[/dim]"
|
2566
|
+
)
|
2567
|
+
account_info.append(
|
2568
|
+
f"[dim] [bold green underline]hippius account encode --name {name}[/bold green underline][/dim]"
|
2569
|
+
)
|
2570
|
+
|
2571
|
+
# Add next steps
|
2572
|
+
account_info.append("")
|
2573
|
+
account_info.append("[bold blue]Next steps:[/bold blue]")
|
2574
|
+
account_info.append(
|
2575
|
+
"• [bold green underline]hippius credits[/bold green underline] - Check your account balance"
|
2576
|
+
)
|
2577
|
+
account_info.append(
|
2578
|
+
"• [bold green underline]hippius files[/bold green underline] - View your stored files"
|
2579
|
+
)
|
2580
|
+
account_info.append(
|
2581
|
+
"• [bold green underline]hippius store <file>[/bold green underline] - Upload a file to IPFS"
|
2582
|
+
)
|
2583
|
+
|
2584
|
+
print_panel(
|
2585
|
+
"\n".join(account_info), title="[bold green]Account Ready[/bold green]"
|
2586
|
+
)
|
2587
|
+
return 0
|
2588
|
+
|
2589
|
+
except Exception as e:
|
2590
|
+
error(f"[bold red]Error logging in:[/bold red] {e}")
|
2591
|
+
return 1
|
2592
|
+
|
2593
|
+
|
2594
|
+
def handle_account_delete(account_name: str) -> int:
|
2595
|
+
"""Handle the account delete command"""
|
2596
|
+
try:
|
2597
|
+
# Check if account exists
|
2598
|
+
accounts = list_accounts()
|
2599
|
+
if account_name not in accounts:
|
2600
|
+
print(f"Error: Account '{account_name}' not found")
|
2601
|
+
return 1
|
2602
|
+
|
2603
|
+
# Confirm deletion
|
2604
|
+
print(f"Warning: You are about to delete account '{account_name}'")
|
2605
|
+
print("This action cannot be undone unless you have exported the account.")
|
2606
|
+
confirm = input("Delete this account? (y/n): ").strip().lower()
|
2607
|
+
|
2608
|
+
if confirm != "y":
|
2609
|
+
print("Deletion cancelled")
|
2610
|
+
return 0
|
2611
|
+
|
2612
|
+
# Delete the account
|
2613
|
+
delete_account(account_name)
|
2614
|
+
|
2615
|
+
print(f"Account '{account_name}' deleted successfully")
|
2616
|
+
|
2617
|
+
# If this was the active account, notify user
|
2618
|
+
active_account = get_active_account()
|
2619
|
+
if active_account == account_name:
|
2620
|
+
print("This was the active account. No account is currently active.")
|
2621
|
+
|
2622
|
+
# If there are other accounts, suggest one
|
2623
|
+
remaining_accounts = list_accounts()
|
2624
|
+
if remaining_accounts:
|
2625
|
+
print(
|
2626
|
+
f"You can switch to another account with: hippius account switch {remaining_accounts[0]}"
|
2627
|
+
)
|
2628
|
+
|
2629
|
+
return 0
|
2630
|
+
|
2631
|
+
except Exception as e:
|
2632
|
+
print(f"Error deleting account: {e}")
|
2633
|
+
return 1
|
2634
|
+
|
2635
|
+
|
2636
|
+
async def handle_account_balance(
|
2637
|
+
client: HippiusClient, account_address: Optional[str] = None
|
2638
|
+
) -> int:
|
2639
|
+
"""Handle the account balance command"""
|
2640
|
+
info("Checking account balance...")
|
2641
|
+
# Get the account address we're querying
|
2642
|
+
if account_address is None:
|
2643
|
+
# If no address provided, first try to get from keypair (if available)
|
2644
|
+
if (
|
2645
|
+
hasattr(client.substrate_client, "_keypair")
|
2646
|
+
and client.substrate_client._keypair is not None
|
2647
|
+
):
|
2648
|
+
account_address = client.substrate_client._keypair.ss58_address
|
2649
|
+
else:
|
2650
|
+
# Try to get the default address
|
2651
|
+
default_address = get_default_address()
|
2652
|
+
if default_address:
|
2653
|
+
account_address = default_address
|
2654
|
+
else:
|
2655
|
+
has_default = get_default_address() is not None
|
2656
|
+
|
2657
|
+
error("No account address provided, and client has no keypair.")
|
2658
|
+
|
2659
|
+
if has_default:
|
2660
|
+
warning(
|
2661
|
+
"Please provide an account address with '--account_address' or the default address may be invalid."
|
2662
|
+
)
|
2663
|
+
else:
|
2664
|
+
warning(
|
2665
|
+
"Please provide an account address with '--account_address' or set a default with:"
|
2666
|
+
)
|
2667
|
+
log(
|
2668
|
+
" hippius address set-default <your_account_address>",
|
2669
|
+
style="bold blue",
|
2670
|
+
)
|
2671
|
+
|
2672
|
+
return 1
|
2673
|
+
|
2674
|
+
# Get the account balance
|
2675
|
+
balance = await client.substrate_client.get_account_balance(account_address)
|
2676
|
+
|
2677
|
+
# Create a panel with balance information
|
2678
|
+
balance_info = [
|
2679
|
+
f"Account address: [bold cyan]{account_address}[/bold cyan]",
|
2680
|
+
f"Free balance: [bold green]{balance['free']:.6f}[/bold green]",
|
2681
|
+
f"Reserved balance: [bold yellow]{balance['reserved']:.6f}[/bold yellow]",
|
2682
|
+
f"Frozen balance: [bold blue]{balance['frozen']:.6f}[/bold blue]",
|
2683
|
+
f"Total balance: [bold]{balance['total']:.6f}[/bold]",
|
2684
|
+
]
|
2685
|
+
|
2686
|
+
# Add the raw values in a more subtle format
|
2687
|
+
balance_info.append("\n[dim]Raw values:[/dim]")
|
2688
|
+
balance_info.append(f"[dim]Free: {balance['raw']['free']:,}[/dim]")
|
2689
|
+
balance_info.append(f"[dim]Reserved: {balance['raw']['reserved']:,}[/dim]")
|
2690
|
+
balance_info.append(f"[dim]Frozen: {balance['raw']['frozen']:,}[/dim]")
|
2691
|
+
|
2692
|
+
print_panel("\n".join(balance_info), title="Account Balance")
|
2693
|
+
|
2694
|
+
|
2695
|
+
#
|
2696
|
+
# Default Address Handlers
|
2697
|
+
#
|
2698
|
+
|
2699
|
+
|
2700
|
+
def handle_default_address_set(address: str) -> int:
|
2701
|
+
"""Handle the address set-default command"""
|
2702
|
+
try:
|
2703
|
+
# Validate address format
|
2704
|
+
if not address.startswith("5"):
|
2705
|
+
warning("The address does not appear to be a valid Substrate address")
|
2706
|
+
log("Substrate addresses typically start with '5'", style="yellow")
|
2707
|
+
confirm = input("Continue anyway? (y/n): ").strip().lower()
|
2708
|
+
if confirm != "y":
|
2709
|
+
return 1
|
2710
|
+
|
2711
|
+
# Update config
|
2712
|
+
config = load_config()
|
2713
|
+
|
2714
|
+
if "substrate" not in config:
|
2715
|
+
config["substrate"] = {}
|
2716
|
+
|
2717
|
+
config["substrate"]["default_address"] = address
|
2718
|
+
|
2719
|
+
# Save config
|
2720
|
+
save_config(config)
|
2721
|
+
|
2722
|
+
# Create success information
|
2723
|
+
details = [
|
2724
|
+
f"Default address set to: [bold cyan]{address}[/bold cyan]",
|
2725
|
+
"\nThis address will be used for read-only operations when no account is specified.",
|
2726
|
+
]
|
2727
|
+
|
2728
|
+
print_panel("\n".join(details), title="Default Address Updated")
|
2729
|
+
|
2730
|
+
return 0
|
2731
|
+
|
2732
|
+
except Exception as e:
|
2733
|
+
error(f"Error setting default address: {e}")
|
2734
|
+
return 1
|
2735
|
+
|
2736
|
+
|
2737
|
+
def handle_default_address_get() -> int:
|
2738
|
+
"""Handle the address get-default command"""
|
2739
|
+
try:
|
2740
|
+
address = get_default_address()
|
2741
|
+
|
2742
|
+
if address:
|
2743
|
+
info(f"Default address: [bold cyan]{address}[/bold cyan]")
|
2744
|
+
else:
|
2745
|
+
warning("No default address set")
|
2746
|
+
log(
|
2747
|
+
"You can set one with: [bold]hippius address set-default <address>[/bold]"
|
2748
|
+
)
|
2749
|
+
|
2750
|
+
return 0
|
2751
|
+
|
2752
|
+
except Exception as e:
|
2753
|
+
error(f"Error getting default address: {e}")
|
2754
|
+
return 1
|
2755
|
+
|
2756
|
+
|
2757
|
+
def handle_default_address_clear() -> int:
|
2758
|
+
"""Handle the address clear-default command"""
|
2759
|
+
try:
|
2760
|
+
config = load_config()
|
2761
|
+
|
2762
|
+
if "substrate" in config and "default_address" in config["substrate"]:
|
2763
|
+
del config["substrate"]["default_address"]
|
2764
|
+
save_config(config)
|
2765
|
+
success("Default address cleared")
|
2766
|
+
else:
|
2767
|
+
log("No default address was set", style="yellow")
|
2768
|
+
|
2769
|
+
return 0
|
2770
|
+
|
2771
|
+
except Exception as e:
|
2772
|
+
error(f"Error clearing default address: {e}")
|
2773
|
+
return 1
|