hippius 0.2.3__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.
@@ -10,11 +10,9 @@ import base64
10
10
  import getpass
11
11
  import json
12
12
  import os
13
- import pprint
14
- import sys
15
13
  import tempfile
16
14
  import time
17
- from typing import Any, Dict, List, Optional, Tuple, Union
15
+ from typing import Any, List, Optional
18
16
 
19
17
  from hippius_sdk import (
20
18
  HippiusClient,
@@ -26,8 +24,6 @@ from hippius_sdk import (
26
24
  get_active_account,
27
25
  get_all_config,
28
26
  get_config_value,
29
- get_seed_phrase,
30
- initialize_from_env,
31
27
  list_accounts,
32
28
  load_config,
33
29
  reset_config,
@@ -98,6 +94,21 @@ def create_client(args: Any) -> HippiusClient:
98
94
  # Get substrate URL
99
95
  substrate_url = args.substrate_url if hasattr(args, "substrate_url") else None
100
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
+
101
112
  # Initialize client with provided parameters
102
113
  client = HippiusClient(
103
114
  ipfs_gateway=gateway,
@@ -106,7 +117,7 @@ def create_client(args: Any) -> HippiusClient:
106
117
  substrate_seed_phrase=(
107
118
  args.seed_phrase if hasattr(args, "seed_phrase") else None
108
119
  ),
109
- seed_phrase_password=args.password if hasattr(args, "password") else None,
120
+ seed_phrase_password=password,
110
121
  account_name=args.account if hasattr(args, "account") else None,
111
122
  encrypt_by_default=encrypt,
112
123
  encryption_key=encryption_key,
@@ -160,7 +171,7 @@ async def handle_exists(client: HippiusClient, cid: str) -> int:
160
171
  log(f"Gateway URL: [link]{result['gateway_url']}[/link]")
161
172
 
162
173
  # Display download command in a panel
163
- command = f"hippius download {formatted_cid} <output_path>"
174
+ command = f"[bold green underline]hippius download {formatted_cid} <output_path>[/bold green underline]"
164
175
  print_panel(command, title="Download Command")
165
176
  else:
166
177
  error(f"CID [bold cyan]{formatted_cid}[/bold cyan] does not exist on IPFS")
@@ -331,7 +342,7 @@ async def handle_store(
331
342
  )
332
343
 
333
344
  # Display download command in a panel
334
- command = f"hippius download {result['cid']} <output_path>"
345
+ command = f"[bold green underline]hippius download {result['cid']} <output_path>[/bold green underline]"
335
346
  print_panel(command, title="Download Command")
336
347
 
337
348
  return 0
@@ -498,8 +509,7 @@ async def handle_credits(
498
509
  "Please provide an account address with '--account_address' or set a default with:"
499
510
  )
500
511
  log(
501
- " hippius address set-default <your_account_address>",
502
- style="bold blue",
512
+ " [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
503
513
  )
504
514
 
505
515
  return 1
@@ -555,7 +565,9 @@ async def handle_files(
555
565
  info(
556
566
  "Please provide an account address with '--account_address' or set a default with:"
557
567
  )
558
- log(" hippius address set-default <your_account_address>")
568
+ log(
569
+ " [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
570
+ )
559
571
  return 1
560
572
 
561
573
  # Get files from the marketplace
@@ -707,7 +719,9 @@ async def handle_ec_files(
707
719
  info(
708
720
  "Please provide an account address with '--account_address' or set a default with:"
709
721
  )
710
- log(" hippius address set-default <your_account_address>")
722
+ log(
723
+ " [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
724
+ )
711
725
  return 1
712
726
 
713
727
  info(f"Getting erasure-coded files for account: [bold]{account_address}[/bold]")
@@ -937,7 +951,7 @@ async def handle_ec_files(
937
951
  # Include a real example with the first metadata CID
938
952
  example_cid = metadata_cids[0] if metadata_cids else "<METADATA_CID>"
939
953
  print_panel(
940
- f"hippius reconstruct <METADATA_CID> <OUTPUT_FILENAME>\n\nExample:\nhippius reconstruct {example_cid} reconstructed_file.bin",
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]",
941
955
  title="Reconstruction Command",
942
956
  )
943
957
 
@@ -969,8 +983,12 @@ async def handle_erasure_code(
969
983
  import zfec
970
984
  except ImportError:
971
985
  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]")
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
+ )
974
992
  return 1
975
993
 
976
994
  # Get file size
@@ -989,11 +1007,14 @@ async def handle_erasure_code(
989
1007
  # Calculate new chunk size to get exactly k chunks
990
1008
  new_chunk_size = file_size / k
991
1009
 
1010
+ new_chunk_size = int(new_chunk_size)
1011
+ new_chunk_size = max(1, new_chunk_size)
1012
+
992
1013
  # Create a panel with parameter adjustment information
993
1014
  adjustment_info = [
994
- f"Original parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold], chunk size=[bold]{chunk_size/1024/1024:.2f} MB[/bold]",
1015
+ f"Original parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold], chunk size=[bold]{chunk_size / 1024 / 1024:.2f} MB[/bold]",
995
1016
  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",
1017
+ f"Automatically adjusting chunk size to [bold green]{new_chunk_size / 1024 / 1024:.6f} MB[/bold green] to create at least {k} chunks",
997
1018
  ]
998
1019
  print_panel("\n".join(adjustment_info), title="Parameter Adjustment")
999
1020
 
@@ -1001,9 +1022,9 @@ async def handle_erasure_code(
1001
1022
 
1002
1023
  # Create parameter information panel
1003
1024
  param_info = [
1004
- f"File: [bold]{file_name}[/bold] ([bold cyan]{file_size/1024/1024:.2f} MB[/bold cyan])",
1025
+ f"File: [bold]{file_name}[/bold] ([bold cyan]{file_size / 1024 / 1024:.2f} MB[/bold cyan])",
1005
1026
  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]",
1027
+ f"Chunk size: [bold cyan]{chunk_size / 1024 / 1024:.6f} MB[/bold cyan]",
1007
1028
  ]
1008
1029
 
1009
1030
  # Add encryption status
@@ -1012,6 +1033,16 @@ async def handle_erasure_code(
1012
1033
  else:
1013
1034
  param_info.append("[bold yellow]Encryption: Disabled[/bold yellow]")
1014
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
+
1015
1046
  # Parse miner IDs if provided
1016
1047
  miner_id_list = None
1017
1048
  if miner_ids:
@@ -1028,7 +1059,9 @@ async def handle_erasure_code(
1028
1059
  # Create progress for the erasure coding operation
1029
1060
  with create_progress() as progress:
1030
1061
  # Add tasks for the different stages
1031
- processing_task = progress.add_task("[cyan]Processing file...", total=100)
1062
+ processing_task = progress.add_task(
1063
+ "[cyan]Processing file...", total=100, visible=False
1064
+ )
1032
1065
  encoding_task = progress.add_task(
1033
1066
  "[green]Encoding chunks...", total=100, visible=False
1034
1067
  )
@@ -1087,6 +1120,7 @@ async def handle_erasure_code(
1087
1120
  max_retries=3,
1088
1121
  verbose=verbose,
1089
1122
  progress_callback=update_progress_bar,
1123
+ publish=publish,
1090
1124
  )
1091
1125
 
1092
1126
  # Complete all progress tasks
@@ -1145,7 +1179,7 @@ async def handle_erasure_code(
1145
1179
  # Use direct values from input parameters when metadata is not available
1146
1180
  summary_lines.extend(
1147
1181
  [
1148
- f"Original file: [bold]{file_name}[/bold] ([bold cyan]{file_size/1024/1024:.2f} MB[/bold cyan])",
1182
+ f"Original file: [bold]{file_name}[/bold] ([bold cyan]{file_size / 1024 / 1024:.2f} MB[/bold cyan])",
1149
1183
  f"Parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold]",
1150
1184
  f"Total files stored in marketplace: [bold]{total_files_stored}[/bold]",
1151
1185
  f"Metadata CID: [bold cyan]{metadata_cid}[/bold cyan]",
@@ -1156,14 +1190,14 @@ async def handle_erasure_code(
1156
1190
  if publish:
1157
1191
  summary_lines.extend(
1158
1192
  [
1159
- f"Published to global IPFS: [bold green]Yes[/bold green]",
1193
+ "Published to global IPFS: [bold green]Yes[/bold green]",
1160
1194
  f"Global access URL: [link]{client.ipfs_client.gateway}/ipfs/{metadata_cid}[/link]",
1161
1195
  ]
1162
1196
  )
1163
1197
  else:
1164
1198
  summary_lines.extend(
1165
1199
  [
1166
- f"Original file: [bold]{original_file.get('name')}[/bold] ([bold cyan]{original_file.get('size', 0)/1024/1024:.2f} MB[/bold cyan])",
1200
+ f"Original file: [bold]{original_file.get('name')}[/bold] ([bold cyan]{original_file.get('size', 0) / 1024 / 1024:.2f} MB[/bold cyan])",
1167
1201
  f"File ID: [bold]{erasure_coding.get('file_id')}[/bold]",
1168
1202
  f"Parameters: k=[bold]{erasure_coding.get('k')}[/bold], m=[bold]{erasure_coding.get('m')}[/bold]",
1169
1203
  f"Total chunks: [bold]{len(metadata.get('chunks', []))}[/bold]",
@@ -1176,7 +1210,7 @@ async def handle_erasure_code(
1176
1210
  if publish:
1177
1211
  summary_lines.extend(
1178
1212
  [
1179
- f"Published to global IPFS: [bold green]Yes[/bold green]",
1213
+ "Published to global IPFS: [bold green]Yes[/bold green]",
1180
1214
  f"Global access URL: [link]{client.ipfs_client.gateway}/ipfs/{metadata_cid}[/link]",
1181
1215
  ]
1182
1216
  )
@@ -1202,7 +1236,7 @@ async def handle_erasure_code(
1202
1236
  f" 2. Access to at least [bold]{k}[/bold] chunks for each original chunk",
1203
1237
  "",
1204
1238
  "Reconstruction command:",
1205
- f"[bold]hippius reconstruct {metadata_cid} reconstructed_{output_filename}[/bold]",
1239
+ f"[bold green underline]hippius reconstruct {metadata_cid} reconstructed_{output_filename}[/bold green underline]",
1206
1240
  ]
1207
1241
 
1208
1242
  print_panel(
@@ -1593,67 +1627,116 @@ def handle_seed_phrase_set(
1593
1627
  def handle_seed_phrase_encode(account_name: Optional[str] = None) -> int:
1594
1628
  """Handle the seed encode command"""
1595
1629
  try:
1596
- # Check if seed phrase exists
1597
- seed_phrase = get_seed_phrase(account_name)
1630
+ # Check if account exists and get its encryption status
1631
+ config = load_config()
1632
+ accounts = config.get("substrate", {}).get("accounts", {})
1598
1633
 
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>")
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")
1607
1644
  return 1
1608
1645
 
1609
- # Check if already encrypted
1610
- config = load_config()
1611
- accounts = config.get("substrate", {}).get("accounts", {})
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")
1612
1650
 
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)
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
1618
1658
 
1659
+ # Check if the seed phrase is already encrypted
1619
1660
  if is_encrypted:
1620
- print("Seed phrase is already encrypted")
1661
+ info("Seed phrase is already encrypted")
1621
1662
  confirm = (
1622
1663
  input("Do you want to re-encrypt it with a new password? (y/n): ")
1623
1664
  .strip()
1624
1665
  .lower()
1625
1666
  )
1626
1667
  if confirm != "y":
1627
- print("Encryption cancelled")
1668
+ info("Encryption cancelled")
1628
1669
  return 0
1629
1670
 
1630
- # Get password for encryption
1631
- print("\nYou are about to encrypt your seed phrase.")
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.")
1632
1684
  password = getpass.getpass("Enter a password for encryption: ")
1633
1685
  confirm = getpass.getpass("Confirm password: ")
1634
1686
 
1635
1687
  if password != confirm:
1636
- print("Error: Passwords do not match")
1688
+ error("Passwords do not match")
1637
1689
  return 1
1638
1690
 
1639
1691
  if not password:
1640
- print("Error: Password cannot be empty for encryption")
1692
+ error("Password cannot be empty for encryption")
1641
1693
  return 1
1642
1694
 
1643
- # Encrypt the seed phrase
1644
- encrypt_seed_phrase(password, account_name)
1695
+ # Now encrypt the seed phrase - key fix here passing correct parameters
1696
+ success = encrypt_seed_phrase(seed_phrase, password, account_name)
1645
1697
 
1646
- print("Seed phrase encrypted successfully")
1647
- print("You will need to provide this password when using the account")
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
1648
1732
 
1649
- return 0
1650
1733
  except Exception as e:
1651
- print(f"Error encrypting seed phrase: {e}")
1734
+ error(f"Error encrypting seed phrase: {e}")
1652
1735
  return 1
1653
1736
 
1654
1737
 
1655
1738
  def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
1656
- """Handle the seed decode command"""
1739
+ """Handle the seed decode command - temporarily decrypts and displays the seed phrase"""
1657
1740
  try:
1658
1741
  # Check if seed phrase exists and is encrypted
1659
1742
  config = load_config()
@@ -1661,19 +1744,29 @@ def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
1661
1744
 
1662
1745
  if account_name:
1663
1746
  account = accounts.get(account_name, {})
1664
- is_encrypted = account.get("encrypted", False)
1747
+ is_encrypted = account.get("seed_phrase_encoded", False)
1665
1748
  else:
1666
- is_encrypted = config.get("substrate", {}).get("encrypted", False)
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
+ )
1667
1760
 
1668
1761
  if not is_encrypted:
1669
- print("Seed phrase is not encrypted")
1762
+ info("Seed phrase is not encrypted")
1670
1763
  return 0
1671
1764
 
1672
1765
  # Get password for decryption
1673
1766
  password = getpass.getpass("Enter your password to decrypt the seed phrase: ")
1674
1767
 
1675
1768
  if not password:
1676
- print("Error: Password cannot be empty")
1769
+ error("Password cannot be empty")
1677
1770
  return 1
1678
1771
 
1679
1772
  # Try to decrypt the seed phrase
@@ -1681,30 +1774,40 @@ def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
1681
1774
  seed_phrase = decrypt_seed_phrase(password, account_name)
1682
1775
 
1683
1776
  if seed_phrase:
1684
- print("\nDecrypted seed phrase:")
1685
- print(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
+ ]
1686
1789
 
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.")
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
1692
1795
 
1693
1796
  return 0
1694
1797
  else:
1695
- print("Error: Failed to decrypt seed phrase")
1798
+ error("Failed to decrypt seed phrase")
1696
1799
  return 1
1697
1800
 
1698
1801
  except Exception as e:
1699
- print(f"Error decrypting seed phrase: {e}")
1802
+ error(f"Error decrypting seed phrase: {e}")
1700
1803
 
1701
1804
  if "decryption failed" in str(e).lower():
1702
- print("Incorrect password")
1805
+ warning("Incorrect password")
1703
1806
 
1704
1807
  return 1
1705
1808
 
1706
1809
  except Exception as e:
1707
- print(f"Error: {e}")
1810
+ error(f"{e}")
1708
1811
  return 1
1709
1812
 
1710
1813
 
@@ -1725,7 +1828,7 @@ def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
1725
1828
 
1726
1829
  account = accounts[account_name]
1727
1830
  has_seed = "seed_phrase" in account
1728
- is_encrypted = account.get("encrypted", False)
1831
+ is_encrypted = account.get("seed_phrase_encoded", False)
1729
1832
  is_active = account_name == get_active_account()
1730
1833
 
1731
1834
  print("\nAccount Status:")
@@ -1746,31 +1849,39 @@ def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
1746
1849
  print(f" Address: Unable to derive (Error: {e})")
1747
1850
 
1748
1851
  else:
1749
- print("Checking default seed phrase status")
1852
+ print("Checking active account seed phrase status")
1750
1853
 
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})")
1769
-
1770
- # Show active account
1854
+ # Get the active account
1771
1855
  active_account = get_active_account()
1772
1856
  if active_account:
1773
- print(f"\nActive Account: {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")
1774
1885
 
1775
1886
  return 0
1776
1887
 
@@ -1784,6 +1895,88 @@ def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
1784
1895
  #
1785
1896
 
1786
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
+
1787
1980
  def handle_account_create(
1788
1981
  client: HippiusClient, name: str, encrypt: bool = False
1789
1982
  ) -> int:
@@ -1797,6 +1990,9 @@ def handle_account_create(
1797
1990
 
1798
1991
  print(f"Creating new account: {name}")
1799
1992
 
1993
+ # Import Keypair at the beginning to ensure it's available
1994
+ from substrateinterface import Keypair
1995
+
1800
1996
  # Generate a new keypair (seed phrase)
1801
1997
  seed_phrase = client.substrate_client.generate_seed_phrase()
1802
1998
 
@@ -1820,56 +2016,70 @@ def handle_account_create(
1820
2016
  return 1
1821
2017
 
1822
2018
  # Set the seed phrase for the new account
1823
- set_seed_phrase(seed_phrase, password, name)
2019
+ # First load the config to directly edit it
2020
+ config = load_config()
1824
2021
 
1825
- # Set as active account
1826
- set_active_account(name)
2022
+ # Ensure accounts structure exists
2023
+ if "accounts" not in config["substrate"]:
2024
+ config["substrate"]["accounts"] = {}
1827
2025
 
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)
2026
+ # Create keypair directly from seed phrase
2027
+ keypair = Keypair.create_from_mnemonic(seed_phrase)
2028
+ address = keypair.ss58_address
1840
2029
 
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
- )
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
+ }
1847
2037
 
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
- )
2038
+ # Set as active account
2039
+ config["substrate"]["active_account"] = name
1859
2040
 
1860
- print("\nThis account is now active. Use it with: hippius <command>")
2041
+ # Save the config
2042
+ save_config(config)
1861
2043
 
1862
- return 0
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
+ ]
1863
2052
 
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."
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]"
1868
2058
  )
1869
- return 1
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
1870
2080
 
1871
2081
  except Exception as e:
1872
- print(f"Error creating account: {e}")
2082
+ error(f"Error creating account: {e}")
1873
2083
  return 1
1874
2084
 
1875
2085
 
@@ -2105,7 +2315,7 @@ def handle_account_list() -> int:
2105
2315
 
2106
2316
  is_active = account_name == active_account
2107
2317
  has_seed = "seed_phrase" in account_data
2108
- is_encrypted = account_data.get("encrypted", False)
2318
+ is_encrypted = account_data.get("seed_phrase_encoded", False)
2109
2319
 
2110
2320
  # Get address
2111
2321
  address = account_data.get("ss58_address", "")
@@ -2136,8 +2346,8 @@ def handle_account_list() -> int:
2136
2346
 
2137
2347
  # Instructions
2138
2348
  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]",
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]",
2141
2351
  ]
2142
2352
  print_panel("\n".join(help_text), title="Account Management")
2143
2353
 
@@ -2188,6 +2398,199 @@ def handle_account_switch(account_name: str) -> int:
2188
2398
  return 1
2189
2399
 
2190
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
+
2191
2594
  def handle_account_delete(account_name: str) -> int:
2192
2595
  """Handle the account delete command"""
2193
2596
  try: