hippius 0.2.3__py3-none-any.whl → 0.2.5__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.
@@ -9,12 +9,11 @@ import asyncio
9
9
  import base64
10
10
  import getpass
11
11
  import json
12
+ import math
12
13
  import os
13
- import pprint
14
- import sys
15
14
  import tempfile
16
15
  import time
17
- from typing import Any, Dict, List, Optional, Tuple, Union
16
+ from typing import Any, List, Optional
18
17
 
19
18
  from hippius_sdk import (
20
19
  HippiusClient,
@@ -26,8 +25,6 @@ from hippius_sdk import (
26
25
  get_active_account,
27
26
  get_all_config,
28
27
  get_config_value,
29
- get_seed_phrase,
30
- initialize_from_env,
31
28
  list_accounts,
32
29
  load_config,
33
30
  reset_config,
@@ -48,6 +45,12 @@ from hippius_sdk.cli_rich import (
48
45
  success,
49
46
  warning,
50
47
  )
48
+ from hippius_sdk.errors import (
49
+ HippiusAlreadyDeletedError,
50
+ HippiusFailedIPFSUnpin,
51
+ HippiusFailedSubstrateDelete,
52
+ HippiusMetadataError,
53
+ )
51
54
 
52
55
  try:
53
56
  import nacl.secret
@@ -98,6 +101,21 @@ def create_client(args: Any) -> HippiusClient:
98
101
  # Get substrate URL
99
102
  substrate_url = args.substrate_url if hasattr(args, "substrate_url") else None
100
103
 
104
+ # Skip password if we're doing erasure-code with --no-publish
105
+ # This avoids prompting for password when we don't need to interact with the blockchain
106
+ password = None
107
+ if (
108
+ hasattr(args, "command")
109
+ and args.command == "erasure-code"
110
+ and hasattr(args, "no_publish")
111
+ and args.no_publish
112
+ ):
113
+ # Don't need a password in this case
114
+ password = None
115
+ else:
116
+ # Use password from args if provided
117
+ password = args.password if hasattr(args, "password") else None
118
+
101
119
  # Initialize client with provided parameters
102
120
  client = HippiusClient(
103
121
  ipfs_gateway=gateway,
@@ -106,7 +124,7 @@ def create_client(args: Any) -> HippiusClient:
106
124
  substrate_seed_phrase=(
107
125
  args.seed_phrase if hasattr(args, "seed_phrase") else None
108
126
  ),
109
- seed_phrase_password=args.password if hasattr(args, "password") else None,
127
+ seed_phrase_password=password,
110
128
  account_name=args.account if hasattr(args, "account") else None,
111
129
  encrypt_by_default=encrypt,
112
130
  encryption_key=encryption_key,
@@ -136,6 +154,12 @@ async def handle_download(
136
154
  f"Size: [bold cyan]{result['size_bytes']:,}[/bold cyan] bytes ([bold cyan]{result['size_formatted']}[/bold cyan])",
137
155
  ]
138
156
 
157
+ # Add details about content type
158
+ if result.get("is_directory", False):
159
+ details.append("[bold green]Content type: Directory[/bold green]")
160
+ else:
161
+ details.append("[bold blue]Content type: File[/bold blue]")
162
+
139
163
  if result.get("decrypted"):
140
164
  details.append("[bold yellow]File was decrypted during download[/bold yellow]")
141
165
 
@@ -160,7 +184,7 @@ async def handle_exists(client: HippiusClient, cid: str) -> int:
160
184
  log(f"Gateway URL: [link]{result['gateway_url']}[/link]")
161
185
 
162
186
  # Display download command in a panel
163
- command = f"hippius download {formatted_cid} <output_path>"
187
+ command = f"[bold green underline]hippius download {formatted_cid} <output_path>[/bold green underline]"
164
188
  print_panel(command, title="Download Command")
165
189
  else:
166
190
  error(f"CID [bold cyan]{formatted_cid}[/bold cyan] does not exist on IPFS")
@@ -331,7 +355,7 @@ async def handle_store(
331
355
  )
332
356
 
333
357
  # Display download command in a panel
334
- command = f"hippius download {result['cid']} <output_path>"
358
+ command = f"[bold green underline]hippius download {result['cid']} <output_path>[/bold green underline]"
335
359
  print_panel(command, title="Download Command")
336
360
 
337
361
  return 0
@@ -347,6 +371,7 @@ async def handle_store_dir(
347
371
  dir_path: str,
348
372
  miner_ids: Optional[List[str]] = None,
349
373
  encrypt: Optional[bool] = None,
374
+ publish: bool = True,
350
375
  ) -> int:
351
376
  """Handle the store directory command"""
352
377
  if not os.path.exists(dir_path):
@@ -406,12 +431,44 @@ async def handle_store_dir(
406
431
  updater = asyncio.create_task(update_progress())
407
432
 
408
433
  try:
434
+ # Upload info message based on publish flag
435
+ if not publish:
436
+ upload_info.append(
437
+ "[bold yellow]Publishing: Disabled (local upload only)[/bold yellow]"
438
+ )
439
+ log(
440
+ "\nUpload will be local only - not publishing to blockchain or pinning to IPFS"
441
+ )
442
+ else:
443
+ upload_info.append(
444
+ "[bold green]Publishing: Enabled (publishing to blockchain)[/bold green]"
445
+ )
446
+
447
+ # Display updated upload information panel
448
+ print_panel("\n".join(upload_info), title="Directory Upload Operation")
449
+
409
450
  # Use the store_directory method
410
451
  result = await client.ipfs_client.upload_directory(
411
452
  dir_path=dir_path,
412
453
  encrypt=encrypt,
413
454
  )
414
455
 
456
+ # Skip publishing to blockchain if publish is False
457
+ if not publish:
458
+ # Remove any blockchain-related data from result to ensure we don't try to use it
459
+ if "transaction_hash" in result:
460
+ del result["transaction_hash"]
461
+ else:
462
+ # If we want to publish, make sure files are pinned globally
463
+ for file_info in result.get("files", []):
464
+ if "cid" in file_info:
465
+ try:
466
+ await client.ipfs_client.publish_global(file_info["cid"])
467
+ except Exception as e:
468
+ warning(
469
+ f"Failed to publish file {file_info['name']} globally: {str(e)}"
470
+ )
471
+
415
472
  # Complete the progress
416
473
  progress.update(task, completed=100)
417
474
  # Cancel the updater task
@@ -450,11 +507,15 @@ async def handle_store_dir(
450
507
  ["Index", "Filename", "CID"],
451
508
  )
452
509
 
453
- # If we stored in the marketplace
454
- if "transaction_hash" in result:
510
+ # If publishing is enabled and we stored in the marketplace
511
+ if publish and "transaction_hash" in result:
455
512
  log(
456
513
  f"\nStored in marketplace. Transaction hash: [bold]{result['transaction_hash']}[/bold]"
457
514
  )
515
+ elif not publish:
516
+ log(
517
+ "\n[yellow]Files were uploaded locally only. No blockchain publication or IPFS pinning.[/yellow]"
518
+ )
458
519
 
459
520
  return 0
460
521
 
@@ -498,8 +559,7 @@ async def handle_credits(
498
559
  "Please provide an account address with '--account_address' or set a default with:"
499
560
  )
500
561
  log(
501
- " hippius address set-default <your_account_address>",
502
- style="bold blue",
562
+ " [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
503
563
  )
504
564
 
505
565
  return 1
@@ -555,7 +615,9 @@ async def handle_files(
555
615
  info(
556
616
  "Please provide an account address with '--account_address' or set a default with:"
557
617
  )
558
- log(" hippius address set-default <your_account_address>")
618
+ log(
619
+ " [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
620
+ )
559
621
  return 1
560
622
 
561
623
  # Get files from the marketplace
@@ -707,7 +769,9 @@ async def handle_ec_files(
707
769
  info(
708
770
  "Please provide an account address with '--account_address' or set a default with:"
709
771
  )
710
- log(" hippius address set-default <your_account_address>")
772
+ log(
773
+ " [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
774
+ )
711
775
  return 1
712
776
 
713
777
  info(f"Getting erasure-coded files for account: [bold]{account_address}[/bold]")
@@ -937,7 +1001,7 @@ async def handle_ec_files(
937
1001
  # Include a real example with the first metadata CID
938
1002
  example_cid = metadata_cids[0] if metadata_cids else "<METADATA_CID>"
939
1003
  print_panel(
940
- f"hippius reconstruct <METADATA_CID> <OUTPUT_FILENAME>\n\nExample:\nhippius reconstruct {example_cid} reconstructed_file.bin",
1004
+ 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]",
941
1005
  title="Reconstruction Command",
942
1006
  )
943
1007
 
@@ -969,10 +1033,44 @@ async def handle_erasure_code(
969
1033
  import zfec
970
1034
  except ImportError:
971
1035
  error("zfec is required for erasure coding")
972
- log("Install it with: [bold]pip install zfec[/bold]")
973
- log("Then update your environment: [bold]poetry add zfec[/bold]")
1036
+ log(
1037
+ "Install it with: [bold green underline]pip install zfec[/bold green underline]"
1038
+ )
1039
+ log(
1040
+ "Then update your environment: [bold green underline]poetry add zfec[/bold green underline]"
1041
+ )
974
1042
  return 1
975
1043
 
1044
+ # Request password early if we're going to publish to the blockchain
1045
+ if publish and client.substrate_client._seed_phrase is None:
1046
+ # First check if we have an encrypted seed phrase that will require a password
1047
+ config = load_config()
1048
+ account_name = client.substrate_client._account_name or get_active_account()
1049
+
1050
+ if account_name and account_name in config["substrate"].get("accounts", {}):
1051
+ account_data = config["substrate"]["accounts"][account_name]
1052
+ is_encoded = account_data.get("seed_phrase_encoded", False)
1053
+
1054
+ if is_encoded:
1055
+ warning("Wallet password will be required for publishing to blockchain")
1056
+ password = getpass.getpass(
1057
+ "Enter password to decrypt seed phrase: \n\n"
1058
+ )
1059
+
1060
+ # Store the password in client for later use
1061
+ client.substrate_client._seed_phrase_password = password
1062
+
1063
+ # Pre-authenticate to ensure the password is correct
1064
+ try:
1065
+ seed_phrase = decrypt_seed_phrase(password, account_name)
1066
+ if not seed_phrase:
1067
+ error("Failed to decrypt seed phrase. Incorrect password?")
1068
+ return 1
1069
+ client.substrate_client._seed_phrase = seed_phrase
1070
+ except Exception as e:
1071
+ error(f"Error decrypting seed phrase: {e}")
1072
+ return 1
1073
+
976
1074
  # Get file size
977
1075
  file_size = os.path.getsize(file_path)
978
1076
  file_name = os.path.basename(file_path)
@@ -989,21 +1087,31 @@ async def handle_erasure_code(
989
1087
  # Calculate new chunk size to get exactly k chunks
990
1088
  new_chunk_size = file_size / k
991
1089
 
1090
+ new_chunk_size = int(new_chunk_size)
1091
+ new_chunk_size = max(1, new_chunk_size)
1092
+
992
1093
  # Create a panel with parameter adjustment information
993
1094
  adjustment_info = [
994
- f"Original parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold], chunk size=[bold]{chunk_size/1024/1024:.2f} MB[/bold]",
1095
+ f"Original parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold], chunk size=[bold]{chunk_size / 1024 / 1024:.2f} MB[/bold]",
995
1096
  f"Would create only [bold red]{potential_chunks:.2f}[/bold red] chunks, which is less than k=[bold]{k}[/bold]",
996
- f"Automatically adjusting chunk size to [bold green]{new_chunk_size/1024/1024:.6f} MB[/bold green] to create at least {k} chunks",
1097
+ f"Automatically adjusting chunk size to [bold green]{new_chunk_size / 1024 / 1024:.6f} MB[/bold green] to create at least {k} chunks",
997
1098
  ]
998
1099
  print_panel("\n".join(adjustment_info), title="Parameter Adjustment")
999
1100
 
1000
1101
  chunk_size = new_chunk_size
1001
1102
 
1103
+ # Calculate total number of chunks that will be created
1104
+ total_original_chunks = max(1, int(math.ceil(file_size / chunk_size)))
1105
+ total_encoded_chunks = total_original_chunks * m
1106
+ estimated_size_per_chunk = min(chunk_size, file_size / total_original_chunks)
1107
+
1002
1108
  # Create parameter information panel
1003
1109
  param_info = [
1004
- f"File: [bold]{file_name}[/bold] ([bold cyan]{file_size/1024/1024:.2f} MB[/bold cyan])",
1110
+ f"File: [bold]{file_name}[/bold] ([bold cyan]{file_size / 1024 / 1024:.2f} MB[/bold cyan])",
1005
1111
  f"Parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold] (need {k} of {m} chunks to reconstruct)",
1006
- f"Chunk size: [bold cyan]{chunk_size/1024/1024:.6f} MB[/bold cyan]",
1112
+ f"Chunk size: [bold cyan]{chunk_size / 1024 / 1024:.6f} MB[/bold cyan]",
1113
+ f"Total chunks to be created: [bold yellow]{total_encoded_chunks}[/bold yellow] ({total_original_chunks} original chunks × {m} encoded chunks each)",
1114
+ f"Estimated storage required: [bold magenta]{(total_encoded_chunks * estimated_size_per_chunk) / (1024 * 1024):.2f} MB[/bold magenta]",
1007
1115
  ]
1008
1116
 
1009
1117
  # Add encryption status
@@ -1012,6 +1120,16 @@ async def handle_erasure_code(
1012
1120
  else:
1013
1121
  param_info.append("[bold yellow]Encryption: Disabled[/bold yellow]")
1014
1122
 
1123
+ # Add publish status
1124
+ if publish:
1125
+ param_info.append(
1126
+ "[bold blue]Publishing: Enabled[/bold blue] (will store on blockchain)"
1127
+ )
1128
+ else:
1129
+ param_info.append(
1130
+ "[bold cyan]Publishing: Disabled[/bold cyan] (local only, no password needed)"
1131
+ )
1132
+
1015
1133
  # Parse miner IDs if provided
1016
1134
  miner_id_list = None
1017
1135
  if miner_ids:
@@ -1028,7 +1146,9 @@ async def handle_erasure_code(
1028
1146
  # Create progress for the erasure coding operation
1029
1147
  with create_progress() as progress:
1030
1148
  # Add tasks for the different stages
1031
- processing_task = progress.add_task("[cyan]Processing file...", total=100)
1149
+ processing_task = progress.add_task(
1150
+ "[cyan]Processing file...", total=100, visible=False
1151
+ )
1032
1152
  encoding_task = progress.add_task(
1033
1153
  "[green]Encoding chunks...", total=100, visible=False
1034
1154
  )
@@ -1087,6 +1207,7 @@ async def handle_erasure_code(
1087
1207
  max_retries=3,
1088
1208
  verbose=verbose,
1089
1209
  progress_callback=update_progress_bar,
1210
+ publish=publish,
1090
1211
  )
1091
1212
 
1092
1213
  # Complete all progress tasks
@@ -1145,7 +1266,7 @@ async def handle_erasure_code(
1145
1266
  # Use direct values from input parameters when metadata is not available
1146
1267
  summary_lines.extend(
1147
1268
  [
1148
- f"Original file: [bold]{file_name}[/bold] ([bold cyan]{file_size/1024/1024:.2f} MB[/bold cyan])",
1269
+ f"Original file: [bold]{file_name}[/bold] ([bold cyan]{file_size / 1024 / 1024:.2f} MB[/bold cyan])",
1149
1270
  f"Parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold]",
1150
1271
  f"Total files stored in marketplace: [bold]{total_files_stored}[/bold]",
1151
1272
  f"Metadata CID: [bold cyan]{metadata_cid}[/bold cyan]",
@@ -1156,14 +1277,14 @@ async def handle_erasure_code(
1156
1277
  if publish:
1157
1278
  summary_lines.extend(
1158
1279
  [
1159
- f"Published to global IPFS: [bold green]Yes[/bold green]",
1280
+ "Published to global IPFS: [bold green]Yes[/bold green]",
1160
1281
  f"Global access URL: [link]{client.ipfs_client.gateway}/ipfs/{metadata_cid}[/link]",
1161
1282
  ]
1162
1283
  )
1163
1284
  else:
1164
1285
  summary_lines.extend(
1165
1286
  [
1166
- f"Original file: [bold]{original_file.get('name')}[/bold] ([bold cyan]{original_file.get('size', 0)/1024/1024:.2f} MB[/bold cyan])",
1287
+ f"Original file: [bold]{original_file.get('name')}[/bold] ([bold cyan]{original_file.get('size', 0) / 1024 / 1024:.2f} MB[/bold cyan])",
1167
1288
  f"File ID: [bold]{erasure_coding.get('file_id')}[/bold]",
1168
1289
  f"Parameters: k=[bold]{erasure_coding.get('k')}[/bold], m=[bold]{erasure_coding.get('m')}[/bold]",
1169
1290
  f"Total chunks: [bold]{len(metadata.get('chunks', []))}[/bold]",
@@ -1176,7 +1297,7 @@ async def handle_erasure_code(
1176
1297
  if publish:
1177
1298
  summary_lines.extend(
1178
1299
  [
1179
- f"Published to global IPFS: [bold green]Yes[/bold green]",
1300
+ "Published to global IPFS: [bold green]Yes[/bold green]",
1180
1301
  f"Global access URL: [link]{client.ipfs_client.gateway}/ipfs/{metadata_cid}[/link]",
1181
1302
  ]
1182
1303
  )
@@ -1202,7 +1323,7 @@ async def handle_erasure_code(
1202
1323
  f" 2. Access to at least [bold]{k}[/bold] chunks for each original chunk",
1203
1324
  "",
1204
1325
  "Reconstruction command:",
1205
- f"[bold]hippius reconstruct {metadata_cid} reconstructed_{output_filename}[/bold]",
1326
+ f"[bold green underline]hippius reconstruct {metadata_cid} reconstructed_{output_filename}[/bold green underline]",
1206
1327
  ]
1207
1328
 
1208
1329
  print_panel(
@@ -1360,89 +1481,210 @@ async def handle_reconstruct(
1360
1481
 
1361
1482
 
1362
1483
  async def handle_delete(client: HippiusClient, cid: str, force: bool = False) -> int:
1363
- """Handle the delete command"""
1364
- info(f"Preparing to delete file with CID: [bold cyan]{cid}[/bold cyan]")
1484
+ """Handle the delete command for files or directories"""
1485
+ info(f"Preparing to delete content with CID: [bold cyan]{cid}[/bold cyan]")
1486
+
1487
+ # First check if this is a directory
1488
+ try:
1489
+ exists_result = await client.exists(cid)
1490
+ if not exists_result["exists"]:
1491
+ error(f"CID [bold cyan]{cid}[/bold cyan] not found on IPFS")
1492
+ return 1
1493
+ except Exception as e:
1494
+ warning(f"Error checking if CID exists: {e}")
1365
1495
 
1366
1496
  if not force:
1367
- warning("This will cancel storage and remove the file from the marketplace.")
1497
+ warning("This will cancel storage and remove the content from the marketplace.")
1368
1498
  confirm = input("Continue? (y/n): ").strip().lower()
1369
1499
  if confirm != "y":
1370
1500
  log("Deletion cancelled", style="yellow")
1371
1501
  return 0
1372
1502
 
1373
- info("Deleting file from marketplace...")
1374
- result = await client.delete_file(cid)
1503
+ # Show spinner during deletion
1504
+ with console.status("[cyan]Deleting content...[/cyan]", spinner="dots") as status:
1505
+ result = await client.delete_file(cid)
1375
1506
 
1376
- if result.get("success"):
1377
- success("File successfully deleted")
1507
+ # Display results
1508
+ is_directory = result.get("is_directory", False)
1509
+ child_files = result.get("child_files", [])
1378
1510
 
1379
- details = []
1380
- if "transaction_hash" in result:
1381
- details.append(
1382
- f"Transaction hash: [bold]{result['transaction_hash']}[/bold]"
1383
- )
1384
-
1385
- # Create an informative panel with notes
1386
- notes = [
1387
- "1. The file is now unpinned from the marketplace",
1388
- "2. The CID may still resolve temporarily until garbage collection occurs",
1389
- "3. If the file was published to the global IPFS network, it may still be",
1390
- " available through other nodes that pinned it",
1511
+ if is_directory:
1512
+ # Directory deletion
1513
+ details = [
1514
+ f"Successfully deleted directory: [bold cyan]{cid}[/bold cyan]",
1515
+ f"Child files unpinned: [bold]{len(child_files)}[/bold]",
1391
1516
  ]
1392
1517
 
1393
- if details:
1394
- print_panel("\n".join(details), title="Transaction Details")
1518
+ # If there are child files, show them in a table
1519
+ if child_files:
1520
+ table_data = []
1521
+ for i, file in enumerate(
1522
+ child_files[:10], 1
1523
+ ): # Limit to first 10 files if many
1524
+ table_data.append(
1525
+ {
1526
+ "Index": str(i),
1527
+ "Filename": file.get("name", "unknown"),
1528
+ "CID": file.get("cid", "unknown"),
1529
+ }
1530
+ )
1395
1531
 
1396
- print_panel("\n".join(notes), title="Important Notes")
1532
+ if len(child_files) > 10:
1533
+ table_data.append(
1534
+ {
1535
+ "Index": "...",
1536
+ "Filename": f"({len(child_files) - 10} more files)",
1537
+ "CID": "...",
1538
+ }
1539
+ )
1397
1540
 
1398
- return 0
1541
+ print_table(
1542
+ "Unpinned Child Files", table_data, ["Index", "Filename", "CID"]
1543
+ )
1399
1544
  else:
1400
- error(f"Failed to delete file: {result}")
1545
+ # Regular file deletion
1546
+ details = [f"Successfully deleted file: [bold cyan]{cid}[/bold cyan]"]
1547
+
1548
+ if "duration_seconds" in result.get("timing", {}):
1549
+ details.append(
1550
+ f"Deletion completed in [bold green]{result['timing']['duration_seconds']:.2f}[/bold green] seconds"
1551
+ )
1552
+
1553
+ print_panel("\n".join(details), title="Deletion Complete")
1554
+
1555
+ # Create an informative panel with notes
1556
+ notes = [
1557
+ "1. The content is now unpinned from the marketplace",
1558
+ "2. The CID may still resolve temporarily until garbage collection occurs",
1559
+ "3. If the content was published to the global IPFS network, it may still be",
1560
+ " available through other nodes that pinned it",
1561
+ ]
1562
+
1563
+ print_panel("\n".join(notes), title="Important Notes")
1564
+
1565
+ return 0
1401
1566
 
1402
1567
 
1403
1568
  async def handle_ec_delete(
1404
1569
  client: HippiusClient, metadata_cid: str, force: bool = False
1405
1570
  ) -> int:
1406
- """Handle the ec-delete command"""
1407
- info(
1408
- f"Preparing to delete erasure-coded file with metadata CID: [bold cyan]{metadata_cid}[/bold cyan]"
1409
- )
1571
+ """Handle the erasure-code delete command"""
1572
+
1573
+ # Create a stylish header with the CID
1574
+ info(f"Preparing to delete erasure-coded file with metadata CID:")
1575
+ print_panel(f"[bold cyan]{metadata_cid}[/bold cyan]", title="Metadata CID")
1410
1576
 
1577
+ # Confirm the deletion if not forced
1411
1578
  if not force:
1412
- warning("This will delete the metadata and all chunks from the marketplace.")
1413
- confirm = input("Continue? (y/n): ").strip().lower()
1579
+ warning_text = [
1580
+ "This will cancel the storage of this file on the Hippius blockchain.",
1581
+ "The file metadata will be removed from blockchain storage tracking.",
1582
+ "[dim]Note: Only the metadata CID will be canceled; contents may remain on IPFS.[/dim]",
1583
+ ]
1584
+ print_panel("\n".join(warning_text), title="Warning")
1585
+
1586
+ confirm = input("Continue with deletion? (y/n): ").strip().lower()
1414
1587
  if confirm != "y":
1415
1588
  log("Deletion cancelled", style="yellow")
1416
1589
  return 0
1417
1590
 
1418
1591
  try:
1592
+ # First, pre-authenticate the client to get any password prompts out of the way
1593
+ # This accesses the substrate client to trigger authentication
1594
+ if not client.substrate_client._keypair:
1595
+ client.substrate_client._ensure_keypair()
1596
+
1597
+ # Now we can show the spinner after any password prompts
1419
1598
  info("Deleting erasure-coded file from marketplace...")
1420
- result = await client.delete_ec_file(metadata_cid)
1421
1599
 
1422
- if result.get("success"):
1423
- success("Erasure-coded file successfully deleted")
1600
+ # Create a more detailed spinner with phases
1601
+ with console.status(
1602
+ "[cyan]Processing file metadata and chunks...[/cyan]", spinner="dots"
1603
+ ) as status:
1604
+ try:
1605
+ # Use the specialized delete method that now throws specific exceptions
1606
+ await client.delete_ec_file(metadata_cid)
1424
1607
 
1425
- # Show detailed results
1426
- details = []
1427
- chunks_deleted = result.get("chunks_deleted", 0)
1428
- details.append(f"Deleted [bold]{chunks_deleted}[/bold] chunks")
1608
+ # If we get here, deletion was successful
1609
+ deletion_success = True
1610
+ already_deleted = False
1429
1611
 
1430
- if "transaction_hash" in result:
1431
- details.append(
1432
- f"Transaction hash: [bold]{result['transaction_hash']}[/bold]"
1612
+ except HippiusAlreadyDeletedError:
1613
+ # Special case - already deleted
1614
+ deletion_success = False
1615
+ already_deleted = True
1616
+
1617
+ except HippiusFailedSubstrateDelete as e:
1618
+ # Blockchain deletion failed
1619
+ error(f"Blockchain storage cancellation failed: {e}")
1620
+ return 1
1621
+
1622
+ except HippiusFailedIPFSUnpin as e:
1623
+ # IPFS unpinning failed, but blockchain deletion succeeded
1624
+ warning(
1625
+ f"Note: Some IPFS operations failed, but blockchain storage was successfully canceled"
1433
1626
  )
1627
+ # Consider this a success for the user since the more important blockchain part worked
1628
+ deletion_success = True
1629
+ already_deleted = False
1630
+
1631
+ except HippiusMetadataError as e:
1632
+ # Metadata parsing failed, but we can still continue
1633
+ warning(
1634
+ f"Note: Metadata file was corrupted, but blockchain storage was successfully canceled"
1635
+ )
1636
+ # Consider this a success for the user since the blockchain part worked
1637
+ deletion_success = True
1638
+ already_deleted = False
1434
1639
 
1435
- print_panel("\n".join(details), title="Deletion Results")
1640
+ except Exception as e:
1641
+ # Handle any unexpected errors
1642
+ error(f"Unexpected error: {e}")
1643
+ return 1
1436
1644
 
1645
+ # Show the result
1646
+ if deletion_success:
1647
+ # Create a success panel
1648
+ success_panel = [
1649
+ "[bold green]✓[/bold green] Metadata CID canceled from blockchain storage",
1650
+ f"[dim]This file is no longer tracked for storage payments[/dim]",
1651
+ "",
1652
+ "[dim]To purge file data completely:[/dim]",
1653
+ "• Individual chunks may still exist on IPFS and nodes",
1654
+ "• For complete deletion, all chunks should be unpinned manually",
1655
+ ]
1656
+ print_panel(
1657
+ "\n".join(success_panel), title="Storage Cancellation Successful"
1658
+ )
1659
+ return 0
1660
+ elif already_deleted:
1661
+ # Create a panel for the already deleted case
1662
+ already_panel = [
1663
+ "[bold yellow]![/bold yellow] This file has already been deleted from storage",
1664
+ "[dim]The CID was not found in the blockchain storage registry[/dim]",
1665
+ "",
1666
+ "This is expected if:",
1667
+ "• You previously deleted this file",
1668
+ "• The file was deleted by another process",
1669
+ "• The file was never stored in the first place",
1670
+ ]
1671
+ print_panel("\n".join(already_panel), title="Already Deleted")
1672
+ # Return 0 since this is not an error condition
1437
1673
  return 0
1438
1674
  else:
1439
- error(
1440
- f"Failed to delete erasure-coded file: {result.get('message', 'Unknown error')}"
1441
- )
1675
+ # Create an error panel for all other failures
1676
+ error_panel = [
1677
+ "[bold red]×[/bold red] File not found in blockchain storage",
1678
+ "[dim]The metadata CID was not found in the blockchain storage registry[/dim]",
1679
+ "",
1680
+ "Possible reasons:",
1681
+ "• The CID may be incorrect",
1682
+ "• You may not be the owner of this file",
1683
+ ]
1684
+ print_panel("\n".join(error_panel), title="Storage Cancellation Failed")
1442
1685
  return 1
1443
-
1444
1686
  except Exception as e:
1445
- error(f"Failed to delete erasure-coded file: {e}")
1687
+ error(f"Error deleting erasure-coded file: {e}")
1446
1688
  return 1
1447
1689
 
1448
1690
 
@@ -1593,67 +1835,116 @@ def handle_seed_phrase_set(
1593
1835
  def handle_seed_phrase_encode(account_name: Optional[str] = None) -> int:
1594
1836
  """Handle the seed encode command"""
1595
1837
  try:
1596
- # Check if seed phrase exists
1597
- seed_phrase = get_seed_phrase(account_name)
1838
+ # Check if account exists and get its encryption status
1839
+ config = load_config()
1840
+ accounts = config.get("substrate", {}).get("accounts", {})
1598
1841
 
1599
- if not seed_phrase:
1600
- print("Error: No seed phrase found to encrypt")
1601
- if account_name:
1602
- print(
1603
- f"Account '{account_name}' may not exist or doesn't have a seed phrase"
1604
- )
1605
- else:
1606
- print("Set a seed phrase first with: hippius seed set <seed_phrase>")
1842
+ # If account name not specified, use active account
1843
+ if not account_name:
1844
+ account_name = config.get("substrate", {}).get("active_account")
1845
+ if not account_name:
1846
+ error("No account specified and no active account")
1847
+ return 1
1848
+
1849
+ # Check if the account exists
1850
+ if account_name not in accounts:
1851
+ error(f"Account '{account_name}' not found")
1607
1852
  return 1
1608
1853
 
1609
- # Check if already encrypted
1610
- config = load_config()
1611
- accounts = config.get("substrate", {}).get("accounts", {})
1854
+ # Get account details
1855
+ account = accounts.get(account_name, {})
1856
+ is_encrypted = account.get("seed_phrase_encoded", False)
1857
+ seed_phrase = account.get("seed_phrase")
1612
1858
 
1613
- if account_name:
1614
- account = accounts.get(account_name, {})
1615
- is_encrypted = account.get("encrypted", False)
1616
- else:
1617
- is_encrypted = config.get("substrate", {}).get("encrypted", False)
1859
+ # Check if we have a seed phrase
1860
+ if not seed_phrase:
1861
+ error(f"Account '{account_name}' doesn't have a seed phrase")
1862
+ info(
1863
+ f"Set a seed phrase first with: [bold green underline]hippius seed set <seed_phrase> --account {account_name}[/bold green underline]"
1864
+ )
1865
+ return 1
1618
1866
 
1867
+ # Check if the seed phrase is already encrypted
1619
1868
  if is_encrypted:
1620
- print("Seed phrase is already encrypted")
1869
+ info("Seed phrase is already encrypted")
1621
1870
  confirm = (
1622
1871
  input("Do you want to re-encrypt it with a new password? (y/n): ")
1623
1872
  .strip()
1624
1873
  .lower()
1625
1874
  )
1626
1875
  if confirm != "y":
1627
- print("Encryption cancelled")
1876
+ info("Encryption cancelled")
1628
1877
  return 0
1629
1878
 
1630
- # Get password for encryption
1631
- print("\nYou are about to encrypt your seed phrase.")
1879
+ # Need to decrypt with old password first
1880
+ old_password = getpass.getpass("Enter your current password to decrypt: ")
1881
+ decrypted_seed_phrase = decrypt_seed_phrase(old_password, account_name)
1882
+
1883
+ if not decrypted_seed_phrase:
1884
+ error("Unable to decrypt the seed phrase. Incorrect password?")
1885
+ return 1
1886
+
1887
+ # Now we have the decrypted seed phrase
1888
+ seed_phrase = decrypted_seed_phrase
1889
+
1890
+ # Get new password for encryption
1891
+ info("\nYou are about to encrypt your seed phrase.")
1632
1892
  password = getpass.getpass("Enter a password for encryption: ")
1633
1893
  confirm = getpass.getpass("Confirm password: ")
1634
1894
 
1635
1895
  if password != confirm:
1636
- print("Error: Passwords do not match")
1896
+ error("Passwords do not match")
1637
1897
  return 1
1638
1898
 
1639
1899
  if not password:
1640
- print("Error: Password cannot be empty for encryption")
1900
+ error("Password cannot be empty for encryption")
1641
1901
  return 1
1642
1902
 
1643
- # Encrypt the seed phrase
1644
- encrypt_seed_phrase(password, account_name)
1903
+ # Now encrypt the seed phrase - key fix here passing correct parameters
1904
+ success = encrypt_seed_phrase(seed_phrase, password, account_name)
1645
1905
 
1646
- print("Seed phrase encrypted successfully")
1647
- print("You will need to provide this password when using the account")
1906
+ # Security: Clear the plaintext seed phrase from memory
1907
+ # This is a best-effort approach, as Python's garbage collection may still keep copies
1908
+ seed_phrase = None
1909
+
1910
+ if success:
1911
+ # Create success panel with encryption information
1912
+ encryption_info = [
1913
+ f"Account: [bold]{account_name}[/bold]",
1914
+ "[bold green]Seed phrase encrypted successfully[/bold green]",
1915
+ "",
1916
+ "You will need to provide this password when using the account for:",
1917
+ " - Pinning files to IPFS",
1918
+ " - Erasure coding with publishing",
1919
+ " - Any other blockchain operations",
1920
+ "",
1921
+ "[bold yellow underline]Security note:[/bold yellow underline] The original unencrypted seed phrase is NOT stored in the config.",
1922
+ ]
1923
+
1924
+ # Try to get the address for display
1925
+ try:
1926
+ address = get_account_address(account_name)
1927
+ if address:
1928
+ encryption_info.append("")
1929
+ encryption_info.append(
1930
+ f"Account address: [bold cyan]{address}[/bold cyan]"
1931
+ )
1932
+ except Exception:
1933
+ pass
1934
+
1935
+ print_panel("\n".join(encryption_info), title="Encryption Successful")
1936
+ return 0
1937
+ else:
1938
+ error("Failed to encrypt seed phrase")
1939
+ return 1
1648
1940
 
1649
- return 0
1650
1941
  except Exception as e:
1651
- print(f"Error encrypting seed phrase: {e}")
1942
+ error(f"Error encrypting seed phrase: {e}")
1652
1943
  return 1
1653
1944
 
1654
1945
 
1655
1946
  def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
1656
- """Handle the seed decode command"""
1947
+ """Handle the seed decode command - temporarily decrypts and displays the seed phrase"""
1657
1948
  try:
1658
1949
  # Check if seed phrase exists and is encrypted
1659
1950
  config = load_config()
@@ -1661,19 +1952,29 @@ def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
1661
1952
 
1662
1953
  if account_name:
1663
1954
  account = accounts.get(account_name, {})
1664
- is_encrypted = account.get("encrypted", False)
1955
+ is_encrypted = account.get("seed_phrase_encoded", False)
1665
1956
  else:
1666
- is_encrypted = config.get("substrate", {}).get("encrypted", False)
1957
+ # Get active account
1958
+ active_account = config.get("substrate", {}).get("active_account")
1959
+ if active_account and active_account in accounts:
1960
+ is_encrypted = accounts[active_account].get(
1961
+ "seed_phrase_encoded", False
1962
+ )
1963
+ else:
1964
+ # Legacy mode
1965
+ is_encrypted = config.get("substrate", {}).get(
1966
+ "seed_phrase_encoded", False
1967
+ )
1667
1968
 
1668
1969
  if not is_encrypted:
1669
- print("Seed phrase is not encrypted")
1970
+ info("Seed phrase is not encrypted")
1670
1971
  return 0
1671
1972
 
1672
1973
  # Get password for decryption
1673
1974
  password = getpass.getpass("Enter your password to decrypt the seed phrase: ")
1674
1975
 
1675
1976
  if not password:
1676
- print("Error: Password cannot be empty")
1977
+ error("Password cannot be empty")
1677
1978
  return 1
1678
1979
 
1679
1980
  # Try to decrypt the seed phrase
@@ -1681,30 +1982,40 @@ def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
1681
1982
  seed_phrase = decrypt_seed_phrase(password, account_name)
1682
1983
 
1683
1984
  if seed_phrase:
1684
- print("\nDecrypted seed phrase:")
1685
- print(seed_phrase)
1985
+ # Create info panel for the decrypted seed phrase
1986
+ seed_info = [
1987
+ f"Decrypted seed phrase: [bold yellow]{seed_phrase}[/bold yellow]",
1988
+ "",
1989
+ "[bold green underline]NOTE: This is a temporary decryption only. Your seed phrase remains encrypted in the config.[/bold green underline]",
1990
+ "",
1991
+ "[bold red underline]SECURITY WARNING:[/bold red underline]",
1992
+ "- Your seed phrase gives full access to your account funds",
1993
+ "- Never share it with anyone or store it in an insecure location",
1994
+ "- Be aware that displaying it on screen could expose it to screen capture",
1995
+ "- Consider clearing your terminal history after this operation",
1996
+ ]
1686
1997
 
1687
- # Security warning
1688
- print(
1689
- "\nWARNING: Your seed phrase gives full access to your account funds."
1690
- )
1691
- print("Never share it with anyone or store it in an insecure location.")
1998
+ print_panel("\n".join(seed_info), title="Seed Phrase Decoded")
1999
+
2000
+ # Security: Clear the plaintext seed phrase from memory
2001
+ # This is a best-effort approach, as Python's garbage collection may still keep copies
2002
+ seed_phrase = None
1692
2003
 
1693
2004
  return 0
1694
2005
  else:
1695
- print("Error: Failed to decrypt seed phrase")
2006
+ error("Failed to decrypt seed phrase")
1696
2007
  return 1
1697
2008
 
1698
2009
  except Exception as e:
1699
- print(f"Error decrypting seed phrase: {e}")
2010
+ error(f"Error decrypting seed phrase: {e}")
1700
2011
 
1701
2012
  if "decryption failed" in str(e).lower():
1702
- print("Incorrect password")
2013
+ warning("Incorrect password")
1703
2014
 
1704
2015
  return 1
1705
2016
 
1706
2017
  except Exception as e:
1707
- print(f"Error: {e}")
2018
+ error(f"{e}")
1708
2019
  return 1
1709
2020
 
1710
2021
 
@@ -1725,7 +2036,7 @@ def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
1725
2036
 
1726
2037
  account = accounts[account_name]
1727
2038
  has_seed = "seed_phrase" in account
1728
- is_encrypted = account.get("encrypted", False)
2039
+ is_encrypted = account.get("seed_phrase_encoded", False)
1729
2040
  is_active = account_name == get_active_account()
1730
2041
 
1731
2042
  print("\nAccount Status:")
@@ -1746,31 +2057,39 @@ def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
1746
2057
  print(f" Address: Unable to derive (Error: {e})")
1747
2058
 
1748
2059
  else:
1749
- print("Checking default seed phrase status")
1750
-
1751
- # Check default seed phrase
1752
- has_seed = "seed_phrase" in config.get("substrate", {})
1753
- is_encrypted = config.get("substrate", {}).get("encrypted", False)
1754
-
1755
- print("\nDefault Seed Phrase Status:")
1756
- print(f" Has Seed Phrase: {'Yes' if has_seed else 'No'}")
1757
- print(f" Encrypted: {'Yes' if is_encrypted else 'No'}")
1758
-
1759
- if has_seed:
1760
- try:
1761
- # Try to get the address (will use cached if available)
1762
- address = get_account_address()
1763
- print(f" Address: {address}")
1764
- except Exception as e:
1765
- if is_encrypted:
1766
- print(" Address: Encrypted (password required to view)")
1767
- else:
1768
- print(f" Address: Unable to derive (Error: {e})")
2060
+ print("Checking active account seed phrase status")
1769
2061
 
1770
- # Show active account
2062
+ # Get the active account
1771
2063
  active_account = get_active_account()
1772
2064
  if active_account:
1773
- print(f"\nActive Account: {active_account}")
2065
+ accounts = config.get("substrate", {}).get("accounts", {})
2066
+ if active_account in accounts:
2067
+ account = accounts[active_account]
2068
+ has_seed = "seed_phrase" in account
2069
+ is_encrypted = account.get("seed_phrase_encoded", False)
2070
+
2071
+ print(f"\nActive Account: {active_account}")
2072
+ print(f" Has Seed Phrase: {'Yes' if has_seed else 'No'}")
2073
+ print(f" Encrypted: {'Yes' if is_encrypted else 'No'}")
2074
+
2075
+ if has_seed:
2076
+ try:
2077
+ # Try to get the address (will use cached if available)
2078
+ address = get_account_address(active_account)
2079
+ print(f" Address: {address}")
2080
+ except Exception as e:
2081
+ if is_encrypted:
2082
+ print(
2083
+ " Address: Encrypted (password required to view)"
2084
+ )
2085
+ else:
2086
+ print(f" Address: Unable to derive (Error: {e})")
2087
+ else:
2088
+ print(
2089
+ f"\nActive account '{active_account}' not found in configuration"
2090
+ )
2091
+ else:
2092
+ print("\nNo active account set")
1774
2093
 
1775
2094
  return 0
1776
2095
 
@@ -1784,6 +2103,88 @@ def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
1784
2103
  #
1785
2104
 
1786
2105
 
2106
+ def handle_account_info(account_name: Optional[str] = None) -> int:
2107
+ """Handle the account info command - displays detailed information about an account"""
2108
+ try:
2109
+ # Load configuration
2110
+ config = load_config()
2111
+
2112
+ # If account name not specified, use active account
2113
+ if not account_name:
2114
+ account_name = config.get("substrate", {}).get("active_account")
2115
+ if not account_name:
2116
+ error("No account specified and no active account")
2117
+ return 1
2118
+
2119
+ # Check if account exists
2120
+ accounts = config.get("substrate", {}).get("accounts", {})
2121
+ if account_name not in accounts:
2122
+ error(f"Account '{account_name}' not found")
2123
+ return 1
2124
+
2125
+ # Get account details
2126
+ account = accounts[account_name]
2127
+ has_seed = "seed_phrase" in account
2128
+ is_encrypted = account.get("seed_phrase_encoded", False)
2129
+ is_active = account_name == get_active_account()
2130
+ ss58_address = account.get("ss58_address", "")
2131
+
2132
+ # Account information panel with rich formatting
2133
+ account_info = [
2134
+ f"Account Name: [bold]{account_name}[/bold]",
2135
+ f"Active: [bold cyan]{'Yes' if is_active else 'No'}[/bold cyan]",
2136
+ f"Has Seed Phrase: [bold]{'Yes' if has_seed else 'No'}[/bold]",
2137
+ f"Encryption: [bold {'green' if is_encrypted else 'yellow'}]{'Encrypted' if is_encrypted else 'Unencrypted'}[/bold {'green' if is_encrypted else 'yellow'}]",
2138
+ ]
2139
+
2140
+ if ss58_address:
2141
+ account_info.append(f"SS58 Address: [bold cyan]{ss58_address}[/bold cyan]")
2142
+ elif has_seed:
2143
+ if is_encrypted:
2144
+ account_info.append(
2145
+ "[dim]Address: Encrypted (password required to view)[/dim]"
2146
+ )
2147
+ else:
2148
+ try:
2149
+ # Try to get the address
2150
+ address = get_account_address(account_name)
2151
+ account_info.append(
2152
+ f"SS58 Address: [bold cyan]{address}[/bold cyan]"
2153
+ )
2154
+ except Exception as e:
2155
+ account_info.append(
2156
+ f"[yellow]Unable to derive address: {e}[/yellow]"
2157
+ )
2158
+
2159
+ # Add suggestions based on account status
2160
+ account_info.append("")
2161
+ if is_active:
2162
+ account_info.append("[bold green]This is your active account[/bold green]")
2163
+ else:
2164
+ account_info.append(
2165
+ f"[dim]To use this account: [bold green underline]hippius account switch {account_name}[/bold green underline][/dim]"
2166
+ )
2167
+
2168
+ if has_seed and not is_encrypted:
2169
+ account_info.append(
2170
+ f"[bold yellow underline]WARNING:[/bold yellow underline] Seed phrase is not encrypted"
2171
+ )
2172
+ account_info.append(
2173
+ f"[dim]To encrypt: [bold green underline]hippius account encode --name {account_name}[/bold green underline][/dim]"
2174
+ )
2175
+
2176
+ # Print the panel with rich formatting
2177
+ print_panel(
2178
+ "\n".join(account_info), title=f"Account Information: {account_name}"
2179
+ )
2180
+
2181
+ return 0
2182
+
2183
+ except Exception as e:
2184
+ error(f"Error getting account info: {e}")
2185
+ return 1
2186
+
2187
+
1787
2188
  def handle_account_create(
1788
2189
  client: HippiusClient, name: str, encrypt: bool = False
1789
2190
  ) -> int:
@@ -1797,6 +2198,9 @@ def handle_account_create(
1797
2198
 
1798
2199
  print(f"Creating new account: {name}")
1799
2200
 
2201
+ # Import Keypair at the beginning to ensure it's available
2202
+ from substrateinterface import Keypair
2203
+
1800
2204
  # Generate a new keypair (seed phrase)
1801
2205
  seed_phrase = client.substrate_client.generate_seed_phrase()
1802
2206
 
@@ -1820,56 +2224,70 @@ def handle_account_create(
1820
2224
  return 1
1821
2225
 
1822
2226
  # Set the seed phrase for the new account
1823
- set_seed_phrase(seed_phrase, password, name)
2227
+ # First load the config to directly edit it
2228
+ config = load_config()
1824
2229
 
1825
- # Set as active account
1826
- set_active_account(name)
2230
+ # Ensure accounts structure exists
2231
+ if "accounts" not in config["substrate"]:
2232
+ config["substrate"]["accounts"] = {}
1827
2233
 
1828
- # Get account address
1829
- try:
1830
- if password:
1831
- # Create a temporary client with the password to get the address
1832
- temp_client = HippiusClient(
1833
- substrate_seed_phrase=seed_phrase,
1834
- seed_phrase_password=password,
1835
- account_name=name,
1836
- )
1837
- address = temp_client.substrate_client.get_account_address()
1838
- else:
1839
- address = get_account_address(name)
2234
+ # Create keypair directly from seed phrase
2235
+ keypair = Keypair.create_from_mnemonic(seed_phrase)
2236
+ address = keypair.ss58_address
1840
2237
 
1841
- print(f"\nCreated new account: {name}")
1842
- print(f"Address: {address}")
1843
- print(f"Seed phrase: {seed_phrase}")
1844
- print(
1845
- "\nIMPORTANT: Keep your seed phrase safe. It's the only way to recover your account!"
1846
- )
2238
+ # Add the new account
2239
+ config["substrate"]["accounts"][name] = {
2240
+ "seed_phrase": seed_phrase,
2241
+ "seed_phrase_encoded": False,
2242
+ "seed_phrase_salt": None,
2243
+ "ss58_address": address,
2244
+ }
1847
2245
 
1848
- # Encryption status
1849
- if encrypt:
1850
- print("\nYour seed phrase is encrypted.")
1851
- print(
1852
- "You'll need to provide the password whenever using this account."
1853
- )
1854
- else:
1855
- print("\nWARNING: Your seed phrase is stored unencrypted.")
1856
- print(
1857
- "Consider encrypting it with: hippius account encode --name", name
1858
- )
2246
+ # Set as active account
2247
+ config["substrate"]["active_account"] = name
1859
2248
 
1860
- print("\nThis account is now active. Use it with: hippius <command>")
2249
+ # Save the config
2250
+ save_config(config)
1861
2251
 
1862
- return 0
2252
+ # Print account information using rich formatting
2253
+ account_info = [
2254
+ f"Account: [bold]{name}[/bold]",
2255
+ f"Address: [bold cyan]{address}[/bold cyan]",
2256
+ f"Seed phrase: [bold yellow]{seed_phrase}[/bold yellow]",
2257
+ "",
2258
+ "[bold red underline]IMPORTANT:[/bold red underline] Keep your seed phrase safe. It's the only way to recover your account!",
2259
+ ]
1863
2260
 
1864
- except Exception as e:
1865
- print(f"Account created but failed to get address: {e}")
1866
- print(
1867
- f"The seed phrase has been saved but you may need to fix the configuration."
2261
+ # Add encryption status
2262
+ if encrypt:
2263
+ account_info.append("")
2264
+ account_info.append(
2265
+ "[bold green]Your seed phrase is encrypted.[/bold green]"
1868
2266
  )
1869
- return 1
2267
+ account_info.append(
2268
+ "You'll need to provide the password whenever using this account."
2269
+ )
2270
+ else:
2271
+ account_info.append("")
2272
+ account_info.append(
2273
+ "[bold yellow underline]WARNING:[/bold yellow underline] Your seed phrase is stored unencrypted."
2274
+ )
2275
+ account_info.append(
2276
+ f"[bold green underline]Consider encrypting it with: hippius account encode --name {name}[/bold green underline]"
2277
+ )
2278
+
2279
+ account_info.append("")
2280
+ account_info.append(
2281
+ "This account is now active. Use it with: [bold]hippius <command>[/bold]"
2282
+ )
2283
+
2284
+ # Print the panel with rich formatting
2285
+ print_panel("\n".join(account_info), title="Account Created Successfully")
2286
+
2287
+ return 0
1870
2288
 
1871
2289
  except Exception as e:
1872
- print(f"Error creating account: {e}")
2290
+ error(f"Error creating account: {e}")
1873
2291
  return 1
1874
2292
 
1875
2293
 
@@ -2105,7 +2523,7 @@ def handle_account_list() -> int:
2105
2523
 
2106
2524
  is_active = account_name == active_account
2107
2525
  has_seed = "seed_phrase" in account_data
2108
- is_encrypted = account_data.get("encrypted", False)
2526
+ is_encrypted = account_data.get("seed_phrase_encoded", False)
2109
2527
 
2110
2528
  # Get address
2111
2529
  address = account_data.get("ss58_address", "")
@@ -2136,8 +2554,8 @@ def handle_account_list() -> int:
2136
2554
 
2137
2555
  # Instructions
2138
2556
  help_text = [
2139
- "To switch accounts: [bold]hippius account switch <account_name>[/bold]",
2140
- "To create a new account: [bold]hippius account create --name <account_name>[/bold]",
2557
+ "To switch accounts: [bold green underline]hippius account switch <account_name>[/bold green underline]",
2558
+ "To create a new account: [bold green underline]hippius account create --name <account_name>[/bold green underline]",
2141
2559
  ]
2142
2560
  print_panel("\n".join(help_text), title="Account Management")
2143
2561
 
@@ -2188,6 +2606,199 @@ def handle_account_switch(account_name: str) -> int:
2188
2606
  return 1
2189
2607
 
2190
2608
 
2609
+ def handle_account_login() -> int:
2610
+ """Handle the account login command - prompts for account details and creates an account"""
2611
+ try:
2612
+ # Display the login banner
2613
+ from hippius_sdk.cli_assets import LOGIN_ASSET
2614
+
2615
+ console.print(LOGIN_ASSET, style="bold cyan")
2616
+ console.print(
2617
+ "\n[bold blue]Welcome to Hippius![/bold blue] Let's set up your account.\n"
2618
+ )
2619
+
2620
+ # Create a style for prompts
2621
+ prompt_style = "bold green"
2622
+ input_style = "bold cyan"
2623
+
2624
+ # Prompt for account name with nice formatting
2625
+ console.print(
2626
+ "[bold]Step 1:[/bold] Choose a name for your account", style=prompt_style
2627
+ )
2628
+ console.print(
2629
+ "This name will be used to identify your account in the Hippius system.",
2630
+ style="dim",
2631
+ )
2632
+ console.print("Account name:", style=input_style, end=" ")
2633
+ name = input().strip()
2634
+
2635
+ if not name:
2636
+ error("[bold red]Account name cannot be empty[/bold red]")
2637
+ return 1
2638
+
2639
+ # Check if account already exists
2640
+ accounts = list_accounts()
2641
+ if name in accounts:
2642
+ warning(f"Account '[bold]{name}[/bold]' already exists")
2643
+ console.print(
2644
+ "Do you want to overwrite it? (y/n):", style=input_style, end=" "
2645
+ )
2646
+ confirm = input().strip().lower()
2647
+ if confirm != "y":
2648
+ info("Login cancelled")
2649
+ return 0
2650
+
2651
+ # Prompt for seed phrase with detailed explanation
2652
+ console.print(
2653
+ "\n[bold]Step 2:[/bold] Enter your seed phrase", style=prompt_style
2654
+ )
2655
+ console.print(
2656
+ "Your seed phrase gives access to your blockchain account and funds.",
2657
+ style="dim",
2658
+ )
2659
+ console.print(
2660
+ "[yellow]Important:[/yellow] Must be 12 or 24 words separated by spaces.",
2661
+ style="dim",
2662
+ )
2663
+ console.print("Seed phrase:", style=input_style, end=" ")
2664
+ seed_phrase = input().strip()
2665
+
2666
+ # Validate the seed phrase
2667
+ if not seed_phrase or len(seed_phrase.split()) not in [12, 24]:
2668
+ error(
2669
+ "[bold red]Invalid seed phrase[/bold red] - must be 12 or 24 words separated by spaces"
2670
+ )
2671
+ return 1
2672
+
2673
+ # Prompt for encryption with security explanation
2674
+ console.print("\n[bold]Step 3:[/bold] Secure your account", style=prompt_style)
2675
+ console.print(
2676
+ "Encrypting your seed phrase adds an extra layer of security.", style="dim"
2677
+ )
2678
+ console.print(
2679
+ "[bold yellow]Strongly recommended[/bold yellow] to protect your account.",
2680
+ style="dim",
2681
+ )
2682
+ console.print(
2683
+ "Encrypt seed phrase? [bold green](Y/n)[/bold green]:",
2684
+ style=input_style,
2685
+ end=" ",
2686
+ )
2687
+ encrypt_input = input().strip().lower()
2688
+ encrypt = encrypt_input == "y" or encrypt_input == "" or encrypt_input == "yes"
2689
+
2690
+ # Set up encryption if requested
2691
+ password = None
2692
+ if encrypt:
2693
+ console.print(
2694
+ "\n[bold]Step 4:[/bold] Set encryption password", style=prompt_style
2695
+ )
2696
+ console.print(
2697
+ "This password will be required whenever you use your account for blockchain operations.",
2698
+ style="dim",
2699
+ )
2700
+
2701
+ password = getpass.getpass("Enter a password: ")
2702
+ confirm = getpass.getpass("Confirm password: ")
2703
+
2704
+ if password != confirm:
2705
+ error("[bold red]Passwords do not match[/bold red]")
2706
+ return 1
2707
+
2708
+ if not password:
2709
+ error("[bold red]Password cannot be empty for encryption[/bold red]")
2710
+ return 1
2711
+
2712
+ # Initialize address variable
2713
+ address = None
2714
+
2715
+ # Create and store the account
2716
+ with console.status("[cyan]Setting up your account...[/cyan]", spinner="dots"):
2717
+ # First, directly modify the config to ensure account is created
2718
+ config = load_config()
2719
+
2720
+ # Ensure accounts structure exists
2721
+ if "substrate" not in config:
2722
+ config["substrate"] = {}
2723
+ if "accounts" not in config["substrate"]:
2724
+ config["substrate"]["accounts"] = {}
2725
+
2726
+ # Create keypair and get address from seed phrase
2727
+ from substrateinterface import Keypair
2728
+
2729
+ keypair = Keypair.create_from_mnemonic(seed_phrase)
2730
+ address = keypair.ss58_address
2731
+
2732
+ # Add the new account
2733
+ config["substrate"]["accounts"][name] = {
2734
+ "seed_phrase": seed_phrase,
2735
+ "seed_phrase_encoded": False,
2736
+ "seed_phrase_salt": None,
2737
+ "ss58_address": address,
2738
+ }
2739
+
2740
+ # Set as active account
2741
+ config["substrate"]["active_account"] = name
2742
+
2743
+ # Save the config first
2744
+ save_config(config)
2745
+
2746
+ # Now encrypt if requested
2747
+ if encrypt:
2748
+ encrypt_seed_phrase(seed_phrase, password, name)
2749
+
2750
+ time.sleep(0.5) # Small delay for visual feedback
2751
+
2752
+ # Success panel with account information
2753
+ account_info = [
2754
+ f"[bold]Account Name:[/bold] [bold magenta]{name}[/bold magenta]",
2755
+ f"[bold]Blockchain Address:[/bold] [bold cyan]{address}[/bold cyan]",
2756
+ "",
2757
+ "[bold green]✓ Login successful![/bold green]",
2758
+ "[bold green]✓ Account set as active[/bold green]",
2759
+ ]
2760
+
2761
+ if encrypt:
2762
+ account_info.append("[bold green]✓ Seed phrase encrypted[/bold green]")
2763
+ account_info.append("")
2764
+ account_info.append(
2765
+ "[dim]You'll need your password when using this account for blockchain operations.[/dim]"
2766
+ )
2767
+ else:
2768
+ account_info.append(
2769
+ "[bold yellow]⚠ Seed phrase not encrypted[/bold yellow]"
2770
+ )
2771
+ account_info.append("")
2772
+ account_info.append(
2773
+ "[dim]For better security, consider encrypting your seed phrase:[/dim]"
2774
+ )
2775
+ account_info.append(
2776
+ f"[dim] [bold green underline]hippius account encode --name {name}[/bold green underline][/dim]"
2777
+ )
2778
+
2779
+ # Add next steps
2780
+ account_info.append("")
2781
+ account_info.append("[bold blue]Next steps:[/bold blue]")
2782
+ account_info.append(
2783
+ "• [bold green underline]hippius credits[/bold green underline] - Check your account balance"
2784
+ )
2785
+ account_info.append(
2786
+ "• [bold green underline]hippius files[/bold green underline] - View your stored files"
2787
+ )
2788
+ account_info.append(
2789
+ "• [bold green underline]hippius store <file>[/bold green underline] - Upload a file to IPFS"
2790
+ )
2791
+
2792
+ print_panel(
2793
+ "\n".join(account_info), title="[bold green]Account Ready[/bold green]"
2794
+ )
2795
+ return 0
2796
+
2797
+ except Exception as e:
2798
+ error(f"[bold red]Error logging in:[/bold red] {e}")
2799
+ return 1
2800
+
2801
+
2191
2802
  def handle_account_delete(account_name: str) -> int:
2192
2803
  """Handle the account delete command"""
2193
2804
  try: