hippius 0.1.6__py3-none-any.whl → 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hippius_sdk/cli.py CHANGED
@@ -11,11 +11,35 @@ import argparse
11
11
  import os
12
12
  import sys
13
13
  import time
14
+ import json
14
15
  from typing import Optional, List
16
+ import getpass
17
+ import concurrent.futures
18
+ import threading
15
19
 
16
20
  # Import SDK components
17
21
  from hippius_sdk import HippiusClient
18
22
  from hippius_sdk.substrate import FileInput
23
+ from hippius_sdk import (
24
+ get_config_value,
25
+ set_config_value,
26
+ get_encryption_key,
27
+ set_encryption_key,
28
+ load_config,
29
+ save_config,
30
+ get_all_config,
31
+ reset_config,
32
+ initialize_from_env,
33
+ get_seed_phrase,
34
+ set_seed_phrase,
35
+ encrypt_seed_phrase,
36
+ decrypt_seed_phrase,
37
+ get_active_account,
38
+ set_active_account,
39
+ list_accounts,
40
+ delete_account,
41
+ get_account_address,
42
+ )
19
43
  from dotenv import load_dotenv
20
44
 
21
45
  try:
@@ -29,6 +53,9 @@ else:
29
53
  # Load environment variables
30
54
  load_dotenv()
31
55
 
56
+ # Initialize configuration from environment variables
57
+ initialize_from_env()
58
+
32
59
 
33
60
  def generate_key():
34
61
  """Generate a random encryption key for NaCl secretbox."""
@@ -103,22 +130,46 @@ def create_client(args):
103
130
  if hasattr(args, "encryption_key") and args.encryption_key:
104
131
  try:
105
132
  encryption_key = base64.b64decode(args.encryption_key)
106
- if args.verbose:
133
+ if hasattr(args, "verbose") and args.verbose:
107
134
  print(f"Using provided encryption key")
108
135
  except Exception as e:
109
136
  print(f"Warning: Could not decode encryption key: {e}")
110
- print(f"Using default encryption key from .env if available")
111
-
112
- # Initialize client with provided gateway and API URL
137
+ print(f"Using default encryption key from configuration if available")
138
+
139
+ # Get API URL based on local_ipfs flag if the flag exists
140
+ api_url = None
141
+ if hasattr(args, "local_ipfs") and args.local_ipfs:
142
+ api_url = "http://localhost:5001"
143
+ elif hasattr(args, "api_url"):
144
+ api_url = args.api_url
145
+ elif hasattr(args, "ipfs_api"):
146
+ api_url = args.ipfs_api
147
+
148
+ # Get gateway URL
149
+ gateway = None
150
+ if hasattr(args, "gateway"):
151
+ gateway = args.gateway
152
+ elif hasattr(args, "ipfs_gateway"):
153
+ gateway = args.ipfs_gateway
154
+
155
+ # Get substrate URL
156
+ substrate_url = args.substrate_url if hasattr(args, "substrate_url") else None
157
+
158
+ # Initialize client with provided parameters
113
159
  client = HippiusClient(
114
- ipfs_gateway=args.gateway,
115
- ipfs_api_url="http://localhost:5001" if args.local_ipfs else args.api_url,
116
- substrate_url=args.substrate_url,
160
+ ipfs_gateway=gateway,
161
+ ipfs_api_url=api_url,
162
+ substrate_url=substrate_url,
163
+ substrate_seed_phrase=args.seed_phrase
164
+ if hasattr(args, "seed_phrase")
165
+ else None,
166
+ seed_phrase_password=args.password if hasattr(args, "password") else None,
167
+ account_name=args.account if hasattr(args, "account") else None,
117
168
  encrypt_by_default=encrypt,
118
169
  encryption_key=encryption_key,
119
170
  )
120
171
 
121
- return client, encrypt, decrypt
172
+ return client
122
173
 
123
174
 
124
175
  def handle_download(client, cid, output_path, decrypt=None):
@@ -344,15 +395,35 @@ def handle_credits(client, account_address):
344
395
  """Handle the credits command"""
345
396
  print("Checking free credits for the account...")
346
397
  try:
398
+ # Get the account address we're querying
399
+ if account_address is None:
400
+ # If no address provided, first try to get from keypair (if available)
401
+ if (
402
+ hasattr(client.substrate_client, "_keypair")
403
+ and client.substrate_client._keypair is not None
404
+ ):
405
+ account_address = client.substrate_client._keypair.ss58_address
406
+ else:
407
+ # Try to get the default address
408
+ default_address = get_default_address()
409
+ if default_address:
410
+ account_address = default_address
411
+ else:
412
+ print(
413
+ "Error: No account address provided, and client has no keypair."
414
+ )
415
+ print(
416
+ "Please provide an account address with '--account_address' or set a default with 'hippius address set-default'"
417
+ )
418
+ return 1
419
+
347
420
  credits = client.substrate_client.get_free_credits(account_address)
348
421
  print(f"\nFree credits: {credits:.6f}")
349
422
  raw_value = int(
350
423
  credits * 1_000_000_000_000_000_000
351
424
  ) # Convert back to raw for display
352
425
  print(f"Raw value: {raw_value:,}")
353
- print(
354
- f"Account address: {account_address or client.substrate_client._keypair.ss58_address}"
355
- )
426
+ print(f"Account address: {account_address}")
356
427
  except Exception as e:
357
428
  print(f"Error checking credits: {e}")
358
429
  return 1
@@ -364,6 +435,28 @@ def handle_files(client, account_address, debug=False, show_all_miners=False):
364
435
  """Handle the files command"""
365
436
  print("Retrieving file information...")
366
437
  try:
438
+ # Get the account address we're querying
439
+ if account_address is None:
440
+ # If no address provided, first try to get from keypair (if available)
441
+ if (
442
+ hasattr(client.substrate_client, "_keypair")
443
+ and client.substrate_client._keypair is not None
444
+ ):
445
+ account_address = client.substrate_client._keypair.ss58_address
446
+ else:
447
+ # Try to get the default address
448
+ default_address = get_default_address()
449
+ if default_address:
450
+ account_address = default_address
451
+ else:
452
+ print(
453
+ "Error: No account address provided, and client has no keypair."
454
+ )
455
+ print(
456
+ "Please provide an account address with '--account_address' or set a default with 'hippius address set-default'"
457
+ )
458
+ return 1
459
+
367
460
  if debug:
368
461
  print("DEBUG MODE: Will show details about CID decoding")
369
462
 
@@ -376,9 +469,7 @@ def handle_files(client, account_address, debug=False, show_all_miners=False):
376
469
  )
377
470
 
378
471
  if files:
379
- print(
380
- f"\nFound {len(files)} files for account: {account_address or client.substrate_client._keypair.ss58_address}"
381
- )
472
+ print(f"\nFound {len(files)} files for account: {account_address}")
382
473
  print("\n" + "-" * 80)
383
474
 
384
475
  for i, file in enumerate(files, 1):
@@ -435,6 +526,306 @@ def handle_files(client, account_address, debug=False, show_all_miners=False):
435
526
  return 0
436
527
 
437
528
 
529
+ def handle_ec_files(client, account_address, show_all_miners=False, show_chunks=False):
530
+ """
531
+ Display erasure-coded files stored by a user.
532
+
533
+ This command only reads data and doesn't require seed phrase decryption.
534
+ """
535
+ # For progress reporting
536
+ processed_files = 0
537
+ total_files = 0
538
+ lock = threading.Lock()
539
+
540
+ # Store results from worker threads
541
+ results = {
542
+ "ec_files": [],
543
+ "binary_files": 0,
544
+ "json_decode_errors": 0,
545
+ "not_ec_files": 0,
546
+ "skipped_files": 0,
547
+ }
548
+
549
+ # For quick identification of potential EC files - common naming patterns
550
+ EC_FILENAME_PATTERNS = ["metadata", "ec-", "erasure"]
551
+
552
+ # Debug print function that can be enabled/disabled
553
+ verbose = get_config_value("cli", "verbose", False)
554
+
555
+ def debug_print(msg):
556
+ if verbose:
557
+ print(msg)
558
+
559
+ try:
560
+ # Get the account address we're querying
561
+ if account_address is None:
562
+ # If no address provided, first try to get from keypair (if available)
563
+ if (
564
+ hasattr(client.substrate_client, "_keypair")
565
+ and client.substrate_client._keypair is not None
566
+ ):
567
+ account_address = client.substrate_client._keypair.ss58_address
568
+ else:
569
+ # Try to get the default address
570
+ default_address = get_default_address()
571
+ if default_address:
572
+ account_address = default_address
573
+ else:
574
+ print(
575
+ "Error: No account address provided, and client has no keypair."
576
+ )
577
+ print(
578
+ "Please provide an account address with '--account_address' or set a default with 'hippius address set-default'"
579
+ )
580
+ return 1
581
+
582
+ # Get all files for the account
583
+ print(f"Fetching files for account: {account_address}")
584
+ files = client.substrate_client.get_user_files(
585
+ account_address=account_address,
586
+ truncate_miners=not show_all_miners,
587
+ max_miners=0 if show_all_miners else 3,
588
+ )
589
+
590
+ if not files:
591
+ print(f"No files found for account: {account_address}")
592
+ return
593
+
594
+ total_files = len(files)
595
+ print(f"Found {total_files} files, analyzing for erasure-coded metadata...")
596
+
597
+ # First, do a quick initial filter to identify potential EC files based on name patterns
598
+ potential_ec_files = []
599
+ for file in files:
600
+ cid = file.get("file_hash")
601
+ if not cid:
602
+ results["skipped_files"] += 1
603
+ continue
604
+
605
+ # Check if filename contains any of our EC patterns
606
+ name = file.get("file_name", "").lower()
607
+ if any(pattern in name for pattern in EC_FILENAME_PATTERNS):
608
+ # Higher chance this is an EC file
609
+ debug_print(f" - Potential EC file based on name: {name}")
610
+ potential_ec_files.append((0, file)) # Priority 0 = high
611
+ else:
612
+ # Still check it, but with lower priority
613
+ potential_ec_files.append((1, file)) # Priority 1 = lower
614
+
615
+ # Sort by priority (check likely EC files first)
616
+ potential_ec_files.sort(key=lambda x: x[0])
617
+ priority_files = [f for _, f in potential_ec_files]
618
+
619
+ # Progress update function
620
+ def update_progress():
621
+ nonlocal processed_files
622
+ processed_files += 1
623
+ if processed_files % 5 == 0 or processed_files == total_files:
624
+ print(
625
+ f" Progress: {processed_files}/{total_files} files analyzed ({(processed_files/total_files)*100:.1f}%)",
626
+ end="\r",
627
+ )
628
+
629
+ # Function to process a single file - for parallel execution
630
+ def process_file(file):
631
+ try:
632
+ cid = file.get("file_hash")
633
+ name = file.get("file_name", "")
634
+
635
+ debug_print(f" - Processing: {cid} ({name})")
636
+
637
+ # Try to fetch metadata to see if it's an erasure-coded file
638
+ metadata = client.ipfs_client.cat(cid)
639
+ if not metadata or not metadata.get("content"):
640
+ with lock:
641
+ results["skipped_files"] += 1
642
+ update_progress()
643
+ return None
644
+
645
+ content = metadata.get("content")
646
+ if isinstance(content, bytes):
647
+ try:
648
+ # Try to decode the content as UTF-8 text - might fail for binary files
649
+ metadata_text = content.decode("utf-8", errors="strict")
650
+
651
+ try:
652
+ metadata_obj = json.loads(metadata_text)
653
+
654
+ # Check if this is an erasure-coded file metadata - look for either format
655
+ is_ec_file = False
656
+
657
+ # Check primary format
658
+ if (
659
+ isinstance(metadata_obj, dict)
660
+ and metadata_obj.get("chunks")
661
+ and metadata_obj.get("original_name")
662
+ ):
663
+ is_ec_file = True
664
+
665
+ # Check alternative format - different structure used in some versions
666
+ elif (
667
+ isinstance(metadata_obj, dict)
668
+ and metadata_obj.get("erasure_coding")
669
+ and metadata_obj.get("original_file")
670
+ ):
671
+ # This is the newer format with a different structure
672
+ metadata_obj = {
673
+ "original_name": metadata_obj.get(
674
+ "original_file", {}
675
+ ).get("name", "unknown"),
676
+ "k": metadata_obj.get("erasure_coding", {}).get(
677
+ "k"
678
+ ),
679
+ "m": metadata_obj.get("erasure_coding", {}).get(
680
+ "m"
681
+ ),
682
+ "original_size": metadata_obj.get(
683
+ "original_file", {}
684
+ ).get("size", 0),
685
+ "encrypted": metadata_obj.get("encrypted", False),
686
+ "chunks": metadata_obj.get("chunks", []),
687
+ }
688
+ is_ec_file = True
689
+
690
+ if is_ec_file:
691
+ # Found an erasure-coded file!
692
+ debug_print(
693
+ f" ✓ Found erasure-coded file: {metadata_obj.get('original_name')}"
694
+ )
695
+
696
+ ec_file = {
697
+ "metadata_cid": cid,
698
+ "original_name": metadata_obj.get(
699
+ "original_name", "unknown"
700
+ ),
701
+ "k": metadata_obj.get("k"),
702
+ "m": metadata_obj.get("m"),
703
+ "total_chunks": len(metadata_obj.get("chunks", [])),
704
+ "original_size_bytes": metadata_obj.get(
705
+ "original_size"
706
+ ),
707
+ "size_formatted": client.format_size(
708
+ metadata_obj.get("original_size", 0)
709
+ ),
710
+ "encrypted": metadata_obj.get("encrypted", False),
711
+ "miner_ids": file.get("miner_ids", []),
712
+ "miner_count": file.get("miner_count", 0),
713
+ "chunks": metadata_obj.get("chunks", []),
714
+ }
715
+ with lock:
716
+ results["ec_files"].append(ec_file)
717
+ update_progress()
718
+ return ec_file
719
+ else:
720
+ with lock:
721
+ results["not_ec_files"] += 1
722
+ update_progress()
723
+ return None
724
+
725
+ except json.JSONDecodeError:
726
+ # Not a JSON file, so not metadata
727
+ with lock:
728
+ results["json_decode_errors"] += 1
729
+ update_progress()
730
+ return None
731
+ except UnicodeDecodeError:
732
+ # This is a binary file, not UTF-8 text, so not erasure-coded metadata
733
+ with lock:
734
+ results["binary_files"] += 1
735
+ update_progress()
736
+ return None
737
+ else:
738
+ with lock:
739
+ results["skipped_files"] += 1
740
+ update_progress()
741
+ return None
742
+ except Exception as e:
743
+ # For other unexpected errors
744
+ debug_print(f" ✗ Error processing {name}: {str(e)}")
745
+ with lock:
746
+ results["skipped_files"] += 1
747
+ update_progress()
748
+ return None
749
+
750
+ # Process files in parallel - significantly speeds up execution
751
+ max_workers = min(10, len(files)) # Don't create too many threads
752
+ print(f"Processing files using {max_workers} parallel workers...")
753
+
754
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
755
+ # Start processing files - higher priority files will be submitted first
756
+ futures = []
757
+ # First process files that look like metadata files
758
+ for file in priority_files:
759
+ futures.append(executor.submit(process_file, file))
760
+
761
+ # Wait for all processing to complete
762
+ concurrent.futures.wait(futures)
763
+
764
+ print(
765
+ "\nAnalysis complete! "
766
+ ) # Extra spaces to clear progress line
767
+
768
+ # Print statistics
769
+ print(f"\nResults:")
770
+ print(f" Total files analyzed: {total_files}")
771
+ print(f" Erasure-coded files found: {len(results['ec_files'])}")
772
+ if verbose:
773
+ print(f" Binary files (skipped): {results['binary_files']}")
774
+ print(f" JSON parse errors: {results['json_decode_errors']}")
775
+ print(f" Not erasure-coded: {results['not_ec_files']}")
776
+ print(f" Other skipped: {results['skipped_files']}")
777
+
778
+ # Print results
779
+ ec_files = results["ec_files"]
780
+ if not ec_files:
781
+ print(f"\nNo erasure-coded files found for this account.")
782
+ return
783
+
784
+ print(f"\nFound {len(ec_files)} erasure-coded files:\n")
785
+
786
+ for i, file in enumerate(ec_files):
787
+ print(f"{i+1}. {file['original_name']} ({file['size_formatted']})")
788
+ print(f" Metadata CID: {file['metadata_cid']}")
789
+ print(
790
+ f" Erasure coding: {file['k']}/{file['m']} scheme ({file['total_chunks']} chunks)"
791
+ )
792
+ print(f" Encrypted: {'Yes' if file['encrypted'] else 'No'}")
793
+ print(f" Stored by {file['miner_count']} miners")
794
+
795
+ if file.get("miner_ids") and show_all_miners:
796
+ print("\n Miners:")
797
+ for miner in file["miner_ids"]:
798
+ miner_id = (
799
+ miner.get("id", miner) if isinstance(miner, dict) else miner
800
+ )
801
+ formatted = (
802
+ miner.get("formatted", miner_id)
803
+ if isinstance(miner, dict)
804
+ else miner_id
805
+ )
806
+ print(f" - {formatted}")
807
+
808
+ if show_chunks and file.get("chunks"):
809
+ print("\n Chunks:")
810
+ for j, chunk in enumerate(file["chunks"]):
811
+ if isinstance(chunk, dict):
812
+ print(f" {j+1}. CID: {chunk.get('cid')}")
813
+ else:
814
+ print(f" {j+1}. CID: {chunk}")
815
+
816
+ print("") # Empty line between files
817
+
818
+ print("\nTo reconstruct a file:")
819
+ print("hippius reconstruct <metadata_cid> <output_file>")
820
+
821
+ except Exception as e:
822
+ print(f"Error: {e}")
823
+ if verbose:
824
+ import traceback
825
+
826
+ traceback.print_exc()
827
+
828
+
438
829
  def handle_erasure_code(
439
830
  client, file_path, k, m, chunk_size, miner_ids, encrypt=None, verbose=True
440
831
  ):
@@ -443,6 +834,48 @@ def handle_erasure_code(
443
834
  print(f"Error: File {file_path} not found")
444
835
  return 1
445
836
 
837
+ # Check if the input is a directory
838
+ if os.path.isdir(file_path):
839
+ print(f"Error: {file_path} is a directory, not a file.")
840
+ print("\nErasure coding requires a single file as input. You have two options:")
841
+ print("\n1. Archive the directory first:")
842
+ print(f" zip -r {file_path}.zip {file_path}/")
843
+ print(f" hippius erasure-code {file_path}.zip --k {k} --m {m}")
844
+ print("\n2. Apply erasure coding to each file individually:")
845
+ print(" # To code each file in the directory:")
846
+
847
+ # Count the files to give the user an idea of how many files would be processed
848
+ file_count = 0
849
+ for root, _, files in os.walk(file_path):
850
+ file_count += len(files)
851
+
852
+ if file_count > 0:
853
+ print(
854
+ f"\n Found {file_count} files in the directory. Example command for individual files:"
855
+ )
856
+ # Show example for one file if available
857
+ for root, _, files in os.walk(file_path):
858
+ if files:
859
+ example_file = os.path.join(root, files[0])
860
+ rel_path = os.path.relpath(example_file, os.path.dirname(file_path))
861
+ print(f' hippius erasure-code "{example_file}" --k {k} --m {m}')
862
+ break
863
+
864
+ # Ask if user wants to automatically apply to all files
865
+ print(
866
+ "\nWould you like to automatically apply erasure coding to each file in the directory? (y/N)"
867
+ )
868
+ choice = input("> ").strip().lower()
869
+
870
+ if choice in ("y", "yes"):
871
+ return handle_erasure_code_directory(
872
+ client, file_path, k, m, chunk_size, miner_ids, encrypt, verbose
873
+ )
874
+ else:
875
+ print(f" No files found in directory {file_path}")
876
+
877
+ return 1
878
+
446
879
  # Check if zfec is installed
447
880
  try:
448
881
  import zfec
@@ -463,29 +896,33 @@ def handle_erasure_code(
463
896
  # Get the file size and adjust parameters if needed
464
897
  file_size = os.path.getsize(file_path)
465
898
  file_size_mb = file_size / (1024 * 1024)
466
-
899
+
467
900
  print(f"Processing {file_path} ({file_size_mb:.2f} MB) with erasure coding...")
468
-
901
+
469
902
  # Check if the file is too small for the current chunk size and k value
470
903
  original_k = k
471
904
  original_m = m
472
905
  original_chunk_size = chunk_size
473
-
906
+
474
907
  # Calculate how many chunks we would get with current settings
475
908
  potential_chunks = max(1, file_size // chunk_size)
476
-
909
+
477
910
  # If we can't get at least k chunks, adjust the chunk size
478
911
  if potential_chunks < k:
479
912
  # Calculate a new chunk size that would give us exactly k chunks
480
913
  new_chunk_size = max(1024, file_size // k) # Ensure at least 1KB chunks
481
-
914
+
482
915
  print(f"Warning: File is too small for the requested parameters.")
483
- print(f"Original parameters: k={k}, m={m}, chunk size={chunk_size/1024/1024:.2f} MB")
916
+ print(
917
+ f"Original parameters: k={k}, m={m}, chunk size={chunk_size/1024/1024:.2f} MB"
918
+ )
484
919
  print(f"Would create only {potential_chunks} chunks, which is less than k={k}")
485
- print(f"Automatically adjusting chunk size to {new_chunk_size/1024/1024:.6f} MB to create at least {k} chunks")
486
-
920
+ print(
921
+ f"Automatically adjusting chunk size to {new_chunk_size/1024/1024:.6f} MB to create at least {k} chunks"
922
+ )
923
+
487
924
  chunk_size = new_chunk_size
488
-
925
+
489
926
  print(f"Final parameters: k={k}, m={m} (need {k} of {m} chunks to reconstruct)")
490
927
  print(f"Chunk size: {chunk_size/1024/1024:.6f} MB")
491
928
 
@@ -548,17 +985,167 @@ def handle_erasure_code(
548
985
 
549
986
  except Exception as e:
550
987
  print(f"Error during erasure coding: {e}")
551
-
988
+
552
989
  # Provide helpful advice based on the error
553
990
  if "Wrong length" in str(e) and "input blocks" in str(e):
554
991
  print("\nThis error typically occurs with very small files.")
555
992
  print("Suggestions:")
556
993
  print(" 1. Try using a smaller chunk size: --chunk-size 4096")
557
994
  print(" 2. Try using a smaller k value: --k 2")
558
- print(" 3. For very small files, consider using regular storage instead of erasure coding.")
559
-
995
+ print(
996
+ " 3. For very small files, consider using regular storage instead of erasure coding."
997
+ )
998
+
999
+ return 1
1000
+
1001
+
1002
+ def handle_erasure_code_directory(
1003
+ client, dir_path, k, m, chunk_size, miner_ids, encrypt=None, verbose=True
1004
+ ):
1005
+ """Apply erasure coding to each file in a directory individually"""
1006
+ if not os.path.isdir(dir_path):
1007
+ print(f"Error: {dir_path} is not a directory")
1008
+ return 1
1009
+
1010
+ # Check if zfec is installed
1011
+ try:
1012
+ import zfec
1013
+ except ImportError:
1014
+ print(
1015
+ "Error: zfec is required for erasure coding. Install it with: pip install zfec"
1016
+ )
1017
+ print("Then update your environment: poetry add zfec")
560
1018
  return 1
561
1019
 
1020
+ print(f"Applying erasure coding to all files in {dir_path}")
1021
+ print(f"Parameters: k={k}, m={m}, chunk_size={chunk_size/1024/1024:.2f} MB")
1022
+ if encrypt:
1023
+ print("Encryption: Enabled")
1024
+
1025
+ # Parse miner IDs if provided
1026
+ miner_id_list = None
1027
+ if miner_ids:
1028
+ miner_id_list = [m.strip() for m in miner_ids.split(",") if m.strip()]
1029
+ if verbose:
1030
+ print(f"Targeting {len(miner_id_list)} miners: {', '.join(miner_id_list)}")
1031
+
1032
+ # Find all files
1033
+ total_files = 0
1034
+ successful = 0
1035
+ failed = 0
1036
+ skipped = 0
1037
+
1038
+ # Collect files first
1039
+ all_files = []
1040
+ for root, _, files in os.walk(dir_path):
1041
+ for filename in files:
1042
+ file_path = os.path.join(root, filename)
1043
+ all_files.append(file_path)
1044
+
1045
+ total_files = len(all_files)
1046
+ print(f"Found {total_files} files to process")
1047
+
1048
+ if total_files == 0:
1049
+ print("No files to process.")
1050
+ return 0
1051
+
1052
+ # Process each file
1053
+ results = []
1054
+
1055
+ for i, file_path in enumerate(all_files, 1):
1056
+ print(f"\n[{i}/{total_files}] Processing: {file_path}")
1057
+
1058
+ # Skip directories (shouldn't happen but just in case)
1059
+ if os.path.isdir(file_path):
1060
+ print(f"Skipping directory: {file_path}")
1061
+ skipped += 1
1062
+ continue
1063
+
1064
+ # Get file size for information purposes
1065
+ file_size = os.path.getsize(file_path)
1066
+ file_size_mb = file_size / (1024 * 1024)
1067
+ print(f"File size: {file_size_mb:.4f} MB ({file_size} bytes)")
1068
+
1069
+ # Calculate adjusted chunk size for this file if needed
1070
+ current_chunk_size = chunk_size
1071
+ potential_chunks = max(1, file_size // current_chunk_size)
1072
+
1073
+ if potential_chunks < k:
1074
+ # Calculate a new chunk size that would give us exactly k chunks
1075
+ # For very small files, use a minimal chunk size to ensure proper erasure coding
1076
+ min_chunk_size = max(1, file_size // k) # Ensure at least 1 byte per chunk
1077
+ print(f"Adjusting chunk size to {min_chunk_size} bytes for this file")
1078
+ current_chunk_size = min_chunk_size
1079
+
1080
+ try:
1081
+ # Use the store_erasure_coded_file method directly from HippiusClient
1082
+ result = client.store_erasure_coded_file(
1083
+ file_path=file_path,
1084
+ k=k,
1085
+ m=m,
1086
+ chunk_size=current_chunk_size,
1087
+ encrypt=encrypt,
1088
+ miner_ids=miner_id_list,
1089
+ max_retries=3,
1090
+ verbose=False, # Less verbose for batch processing
1091
+ )
1092
+
1093
+ # Store basic result info
1094
+ results.append(
1095
+ {
1096
+ "file_path": file_path,
1097
+ "metadata_cid": result.get("metadata_cid", "unknown"),
1098
+ "success": True,
1099
+ }
1100
+ )
1101
+
1102
+ print(f"Success! Metadata CID: {result.get('metadata_cid', 'unknown')}")
1103
+ successful += 1
1104
+
1105
+ except Exception as e:
1106
+ print(f"Error coding file: {e}")
1107
+
1108
+ # Provide specific guidance for very small files that fail
1109
+ if file_size < 1024 and "Wrong length" in str(e):
1110
+ print(
1111
+ "This file may be too small for erasure coding with the current parameters."
1112
+ )
1113
+ print(
1114
+ "Consider using smaller k and m values for very small files, e.g., --k 2 --m 3"
1115
+ )
1116
+
1117
+ results.append(
1118
+ {
1119
+ "file_path": file_path,
1120
+ "error": str(e),
1121
+ "success": False,
1122
+ }
1123
+ )
1124
+ failed += 1
1125
+
1126
+ # Print summary
1127
+ print(f"\n=== Erasure Coding Directory Summary ===")
1128
+ print(f"Total files processed: {total_files}")
1129
+ print(f"Successfully coded: {successful}")
1130
+ print(f"Failed: {failed}")
1131
+ print(f"Skipped: {skipped}")
1132
+
1133
+ if successful > 0:
1134
+ print("\nSuccessfully coded files:")
1135
+ for result in results:
1136
+ if result.get("success"):
1137
+ print(f" {result['file_path']} -> {result['metadata_cid']}")
1138
+
1139
+ if failed > 0:
1140
+ print("\nFailed files:")
1141
+ for result in results:
1142
+ if not result.get("success"):
1143
+ print(
1144
+ f" {result['file_path']}: {result.get('error', 'Unknown error')}"
1145
+ )
1146
+
1147
+ return 0 if failed == 0 else 1
1148
+
562
1149
 
563
1150
  def handle_reconstruct(client, metadata_cid, output_file, verbose=True):
564
1151
  """Handle the reconstruct command for erasure-coded files"""
@@ -594,6 +1181,385 @@ def handle_reconstruct(client, metadata_cid, output_file, verbose=True):
594
1181
  return 1
595
1182
 
596
1183
 
1184
+ def handle_config_get(section, key):
1185
+ """Handle getting a configuration value"""
1186
+ value = get_config_value(section, key)
1187
+ print(f"Configuration value for {section}.{key}: {value}")
1188
+ return 0
1189
+
1190
+
1191
+ def handle_config_set(section, key, value):
1192
+ """Handle setting a configuration value"""
1193
+ # Try to parse JSON value for objects, arrays, and literals
1194
+ try:
1195
+ parsed_value = json.loads(value)
1196
+ value = parsed_value
1197
+ except (json.JSONDecodeError, TypeError):
1198
+ # If not valid JSON, keep the raw string
1199
+ pass
1200
+
1201
+ result = set_config_value(section, key, value)
1202
+ if result:
1203
+ print(f"Successfully set {section}.{key} to {value}")
1204
+ else:
1205
+ print(f"Failed to set {section}.{key}")
1206
+ return 1
1207
+ return 0
1208
+
1209
+
1210
+ def handle_config_list():
1211
+ """Handle listing all configuration values"""
1212
+ config = get_all_config()
1213
+ print("Current Hippius SDK Configuration:")
1214
+ print(json.dumps(config, indent=2))
1215
+ print(f"\nConfiguration file: {os.path.expanduser('~/.hippius/config.json')}")
1216
+ return 0
1217
+
1218
+
1219
+ def handle_config_reset():
1220
+ """Handle resetting configuration to default values"""
1221
+ if reset_config():
1222
+ print("Successfully reset configuration to default values")
1223
+ else:
1224
+ print("Failed to reset configuration")
1225
+ return 1
1226
+ return 0
1227
+
1228
+
1229
+ def handle_seed_phrase_set(seed_phrase, encode=False, account_name=None):
1230
+ """Handle setting the seed phrase"""
1231
+ if encode:
1232
+ try:
1233
+ password = getpass.getpass("Enter password to encrypt seed phrase: ")
1234
+ password_confirm = getpass.getpass("Confirm password: ")
1235
+
1236
+ if password != password_confirm:
1237
+ print("Error: Passwords do not match")
1238
+ return 1
1239
+
1240
+ result = set_seed_phrase(
1241
+ seed_phrase, encode=True, password=password, account_name=account_name
1242
+ )
1243
+ except KeyboardInterrupt:
1244
+ print("\nOperation cancelled")
1245
+ return 1
1246
+ else:
1247
+ result = set_seed_phrase(seed_phrase, encode=False, account_name=account_name)
1248
+
1249
+ if result:
1250
+ account_msg = f" for account '{account_name}'" if account_name else ""
1251
+
1252
+ if encode:
1253
+ print(
1254
+ f"Successfully set and encrypted the seed phrase{account_msg} with password protection"
1255
+ )
1256
+ else:
1257
+ print(
1258
+ f"Successfully set the seed phrase{account_msg} (WARNING: stored in plain text)"
1259
+ )
1260
+
1261
+ if account_name:
1262
+ address = get_account_address(account_name)
1263
+ if address:
1264
+ print(f"SS58 Address: {address}")
1265
+
1266
+ return 0
1267
+ else:
1268
+ print(f"Failed to set the seed phrase")
1269
+ return 1
1270
+
1271
+
1272
+ def handle_seed_phrase_encode(account_name=None):
1273
+ """Handle encoding the existing seed phrase"""
1274
+ # Get the current seed phrase
1275
+ seed_phrase = get_seed_phrase(account_name=account_name)
1276
+ if not seed_phrase:
1277
+ if account_name:
1278
+ print(f"Error: No seed phrase available for account '{account_name}'")
1279
+ else:
1280
+ print("Error: No seed phrase available to encode")
1281
+ return 1
1282
+
1283
+ # Check if it's already encoded
1284
+ config = load_config()
1285
+ is_encoded = False
1286
+
1287
+ if account_name:
1288
+ account_data = config["substrate"].get("accounts", {}).get(account_name, {})
1289
+ is_encoded = account_data.get("seed_phrase_encoded", False)
1290
+ else:
1291
+ is_encoded = config["substrate"].get("seed_phrase_encoded", False)
1292
+
1293
+ if is_encoded:
1294
+ if account_name:
1295
+ print(f"Seed phrase for account '{account_name}' is already encoded")
1296
+ else:
1297
+ print("Seed phrase is already encoded")
1298
+ return 0
1299
+
1300
+ # Get a password
1301
+ try:
1302
+ password = getpass.getpass("Enter password to encrypt seed phrase: ")
1303
+ password_confirm = getpass.getpass("Confirm password: ")
1304
+
1305
+ if password != password_confirm:
1306
+ print("Error: Passwords do not match")
1307
+ return 1
1308
+
1309
+ # Encode the seed phrase
1310
+ result = encrypt_seed_phrase(seed_phrase, password, account_name)
1311
+ except KeyboardInterrupt:
1312
+ print("\nOperation cancelled")
1313
+ return 1
1314
+
1315
+ if result:
1316
+ account_msg = f" for account '{account_name}'" if account_name else ""
1317
+ print(
1318
+ f"Successfully encoded the seed phrase{account_msg} with password protection"
1319
+ )
1320
+ return 0
1321
+ else:
1322
+ print("Failed to encode the seed phrase")
1323
+ return 1
1324
+
1325
+
1326
+ def handle_seed_phrase_decode(account_name=None):
1327
+ """Handle checking or decoding the seed phrase"""
1328
+ # Check if the seed phrase is encoded
1329
+ config = load_config()
1330
+ is_encoded = False
1331
+
1332
+ if account_name:
1333
+ account_data = config["substrate"].get("accounts", {}).get(account_name, {})
1334
+ is_encoded = account_data.get("seed_phrase_encoded", False)
1335
+ else:
1336
+ is_encoded = config["substrate"].get("seed_phrase_encoded", False)
1337
+
1338
+ if not is_encoded:
1339
+ if account_name:
1340
+ print(
1341
+ f"Seed phrase for account '{account_name}' is not encoded - nothing to decode"
1342
+ )
1343
+ else:
1344
+ print("Seed phrase is not encoded - nothing to decode")
1345
+ return 0
1346
+
1347
+ # Get the decrypted seed phrase
1348
+ try:
1349
+ password = getpass.getpass("Enter password to decrypt seed phrase: ")
1350
+ seed_phrase = decrypt_seed_phrase(password, account_name)
1351
+
1352
+ if seed_phrase:
1353
+ account_msg = f" for account '{account_name}'" if account_name else ""
1354
+ print(f"Decrypted seed phrase{account_msg}: {seed_phrase}")
1355
+
1356
+ # Ask if the user wants to save it as plain text
1357
+ response = input(
1358
+ "Do you want to save the seed phrase as plain text? (y/N): "
1359
+ )
1360
+ if response.lower() in ("y", "yes"):
1361
+ result = set_seed_phrase(
1362
+ seed_phrase, encode=False, account_name=account_name
1363
+ )
1364
+ if result:
1365
+ print("Seed phrase saved as plain text")
1366
+ else:
1367
+ print("Failed to save the seed phrase as plain text")
1368
+
1369
+ return 0
1370
+ else:
1371
+ print("Failed to decode the seed phrase. Incorrect password?")
1372
+ return 1
1373
+ except KeyboardInterrupt:
1374
+ print("\nOperation cancelled")
1375
+ return 1
1376
+
1377
+
1378
+ def handle_seed_phrase_status(account_name=None):
1379
+ """Handle showing the status of the seed phrase"""
1380
+ # Check if we have a seed phrase
1381
+ config = load_config()
1382
+
1383
+ if account_name:
1384
+ if account_name not in config["substrate"].get("accounts", {}):
1385
+ print(f"Error: Account '{account_name}' not found")
1386
+ return 1
1387
+
1388
+ account_data = config["substrate"].get("accounts", {}).get(account_name, {})
1389
+ seed_phrase_exists = account_data.get("seed_phrase") is not None
1390
+ is_encoded = account_data.get("seed_phrase_encoded", False)
1391
+ ss58_address = account_data.get("ss58_address")
1392
+ else:
1393
+ seed_phrase_exists = config["substrate"].get("seed_phrase") is not None
1394
+ is_encoded = config["substrate"].get("seed_phrase_encoded", False)
1395
+ ss58_address = config["substrate"].get("ss58_address")
1396
+
1397
+ if not seed_phrase_exists:
1398
+ if account_name:
1399
+ print(f"No seed phrase is configured for account '{account_name}'")
1400
+ else:
1401
+ print("No seed phrase is configured")
1402
+ return 0
1403
+
1404
+ account_msg = f" for account '{account_name}'" if account_name else ""
1405
+
1406
+ if is_encoded:
1407
+ print(f"Seed phrase{account_msg} is stored with password-based encryption")
1408
+
1409
+ # Offer to verify the password works
1410
+ print("You can verify your password by decoding the seed phrase")
1411
+ try:
1412
+ verify = input("Would you like to verify your password works? (y/N): ")
1413
+ if verify.lower() in ("y", "yes"):
1414
+ password = getpass.getpass("Enter password to decrypt seed phrase: ")
1415
+ seed_phrase = decrypt_seed_phrase(password, account_name)
1416
+ if seed_phrase:
1417
+ print("Password verification successful!")
1418
+ else:
1419
+ print("Password verification failed")
1420
+ except KeyboardInterrupt:
1421
+ print("\nOperation cancelled")
1422
+ else:
1423
+ print(f"Seed phrase{account_msg} is stored in plain text (not encrypted)")
1424
+
1425
+ # Get the value
1426
+ seed_phrase = get_seed_phrase(account_name=account_name)
1427
+ if seed_phrase:
1428
+ # Show only the first and last few words for security
1429
+ words = seed_phrase.split()
1430
+ if len(words) >= 6:
1431
+ masked = " ".join(words[:2] + ["..."] + words[-2:])
1432
+ print(f"Seed phrase (masked): {masked}")
1433
+ else:
1434
+ print("Seed phrase is available")
1435
+
1436
+ if ss58_address:
1437
+ print(f"SS58 Address: {ss58_address}")
1438
+
1439
+ return 0
1440
+
1441
+
1442
+ def handle_account_list():
1443
+ """Handle listing all accounts"""
1444
+ accounts = list_accounts()
1445
+
1446
+ if not accounts:
1447
+ print("No accounts configured")
1448
+ return 0
1449
+
1450
+ print(f"Found {len(accounts)} accounts:")
1451
+
1452
+ for name, data in accounts.items():
1453
+ active_marker = " (active)" if data.get("is_active", False) else ""
1454
+ encoded_status = (
1455
+ "encrypted" if data.get("seed_phrase_encoded", False) else "plain text"
1456
+ )
1457
+ address = data.get("ss58_address", "unknown")
1458
+
1459
+ print(f" {name}{active_marker}:")
1460
+ print(f" SS58 Address: {address}")
1461
+ print(f" Seed phrase: {encoded_status}")
1462
+ print()
1463
+
1464
+ return 0
1465
+
1466
+
1467
+ def handle_account_switch(account_name):
1468
+ """Handle switching the active account"""
1469
+ if set_active_account(account_name):
1470
+ print(f"Switched to account '{account_name}'")
1471
+
1472
+ # Show address
1473
+ address = get_account_address(account_name)
1474
+ if address:
1475
+ print(f"SS58 Address: {address}")
1476
+
1477
+ return 0
1478
+ else:
1479
+ return 1
1480
+
1481
+
1482
+ def handle_account_delete(account_name):
1483
+ """Handle deleting an account"""
1484
+ # Ask for confirmation
1485
+ confirm = input(
1486
+ f"Are you sure you want to delete account '{account_name}'? This cannot be undone. (y/N): "
1487
+ )
1488
+ if confirm.lower() not in ("y", "yes"):
1489
+ print("Operation cancelled")
1490
+ return 0
1491
+
1492
+ if delete_account(account_name):
1493
+ print(f"Account '{account_name}' deleted")
1494
+
1495
+ # Show the new active account if any
1496
+ active_account = get_active_account()
1497
+ if active_account:
1498
+ print(f"Active account is now '{active_account}'")
1499
+ else:
1500
+ print("No accounts remaining")
1501
+
1502
+ return 0
1503
+ else:
1504
+ return 1
1505
+
1506
+
1507
+ def handle_default_address_set(address):
1508
+ """Handle setting the default address for read-only operations"""
1509
+ # Validate SS58 address format (basic check)
1510
+ if not address.startswith("5"):
1511
+ print(
1512
+ f"Warning: '{address}' doesn't look like a valid SS58 address. SS58 addresses typically start with '5'."
1513
+ )
1514
+ confirm = input("Do you want to continue anyway? (y/N): ")
1515
+ if confirm.lower() not in ("y", "yes"):
1516
+ print("Operation cancelled")
1517
+ return 1
1518
+
1519
+ config = load_config()
1520
+ config["substrate"]["default_address"] = address
1521
+ save_config(config)
1522
+
1523
+ print(f"Default address for read-only operations set to: {address}")
1524
+ print(
1525
+ "This address will be used for commands like 'files' and 'ec-files' when no address is explicitly provided."
1526
+ )
1527
+ return 0
1528
+
1529
+
1530
+ def handle_default_address_get():
1531
+ """Handle getting the current default address for read-only operations"""
1532
+ config = load_config()
1533
+ address = config["substrate"].get("default_address")
1534
+
1535
+ if address:
1536
+ print(f"Current default address for read-only operations: {address}")
1537
+ else:
1538
+ print("No default address set for read-only operations")
1539
+ print("You can set one with: hippius address set-default <ss58_address>")
1540
+
1541
+ return 0
1542
+
1543
+
1544
+ def handle_default_address_clear():
1545
+ """Handle clearing the default address for read-only operations"""
1546
+ config = load_config()
1547
+ if "default_address" in config["substrate"]:
1548
+ del config["substrate"]["default_address"]
1549
+ save_config(config)
1550
+ print("Default address for read-only operations has been cleared")
1551
+ else:
1552
+ print("No default address was set")
1553
+
1554
+ return 0
1555
+
1556
+
1557
+ def get_default_address():
1558
+ """Get the default address for read-only operations"""
1559
+ config = load_config()
1560
+ return config["substrate"].get("default_address")
1561
+
1562
+
597
1563
  def main():
598
1564
  """Main CLI entry point for hippius command."""
599
1565
  # Set up the argument parser
@@ -637,30 +1603,35 @@ examples:
637
1603
  # Optional arguments for all commands
638
1604
  parser.add_argument(
639
1605
  "--gateway",
640
- default=os.getenv("IPFS_GATEWAY", "https://ipfs.io"),
641
- help="IPFS gateway URL for downloads (default: from env or https://ipfs.io)",
1606
+ default=get_config_value("ipfs", "gateway", "https://ipfs.io"),
1607
+ help="IPFS gateway URL for downloads (default: from config or https://ipfs.io)",
642
1608
  )
643
1609
  parser.add_argument(
644
1610
  "--api-url",
645
- default=os.getenv("IPFS_API_URL", "https://relay-fr.hippius.network"),
646
- help="IPFS API URL for uploads (default: from env or https://relay-fr.hippius.network)",
1611
+ default=get_config_value("ipfs", "api_url", "https://store.hippius.network"),
1612
+ help="IPFS API URL for uploads (default: from config or https://store.hippius.network)",
647
1613
  )
648
1614
  parser.add_argument(
649
1615
  "--local-ipfs",
650
1616
  action="store_true",
1617
+ default=get_config_value("ipfs", "local_ipfs", False),
651
1618
  help="Use local IPFS node (http://localhost:5001) instead of remote API",
652
1619
  )
653
1620
  parser.add_argument(
654
1621
  "--substrate-url",
655
- default=os.getenv("SUBSTRATE_URL", "wss://rpc.hippius.network"),
656
- help="Substrate node WebSocket URL (default: from env or wss://rpc.hippius.network)",
1622
+ default=get_config_value("substrate", "url", "wss://rpc.hippius.network"),
1623
+ help="Substrate node WebSocket URL (default: from config or wss://rpc.hippius.network)",
657
1624
  )
658
1625
  parser.add_argument(
659
1626
  "--miner-ids",
660
- help="Comma-separated list of miner IDs for storage (default: from env SUBSTRATE_DEFAULT_MINERS)",
1627
+ help="Comma-separated list of miner IDs for storage (default: from config)",
661
1628
  )
662
1629
  parser.add_argument(
663
- "--verbose", "-v", action="store_true", help="Enable verbose debug output"
1630
+ "--verbose",
1631
+ "-v",
1632
+ action="store_true",
1633
+ default=get_config_value("cli", "verbose", False),
1634
+ help="Enable verbose debug output",
664
1635
  )
665
1636
  parser.add_argument(
666
1637
  "--encrypt",
@@ -686,6 +1657,14 @@ examples:
686
1657
  "--encryption-key",
687
1658
  help="Base64-encoded encryption key (overrides HIPPIUS_ENCRYPTION_KEY in .env)",
688
1659
  )
1660
+ parser.add_argument(
1661
+ "--password",
1662
+ help="Password to decrypt the seed phrase if needed (will prompt if required and not provided)",
1663
+ )
1664
+ parser.add_argument(
1665
+ "--account",
1666
+ help="Account name to use (uses active account if not specified)",
1667
+ )
689
1668
 
690
1669
  # Subcommands
691
1670
  subparsers = parser.add_subparsers(dest="command", help="Commands")
@@ -757,6 +1736,27 @@ examples:
757
1736
  help="Show all miners for each file instead of only the first 3",
758
1737
  )
759
1738
 
1739
+ # Erasure Coded Files command
1740
+ ec_files_parser = subparsers.add_parser(
1741
+ "ec-files", help="List only erasure-coded files stored by a user"
1742
+ )
1743
+ ec_files_parser.add_argument(
1744
+ "account_address",
1745
+ nargs="?",
1746
+ default=None,
1747
+ help="Substrate account address (uses keypair address if not specified)",
1748
+ )
1749
+ ec_files_parser.add_argument(
1750
+ "--all-miners",
1751
+ action="store_true",
1752
+ help="Show all miners for each file instead of only the first 3",
1753
+ )
1754
+ ec_files_parser.add_argument(
1755
+ "--show-chunks",
1756
+ action="store_true",
1757
+ help="Show associated chunks for each erasure-coded file",
1758
+ )
1759
+
760
1760
  # Key generation command
761
1761
  keygen_parser = subparsers.add_parser(
762
1762
  "keygen", help="Generate an encryption key for secure file storage"
@@ -812,6 +1812,137 @@ examples:
812
1812
  "--verbose", action="store_true", help="Enable verbose output", default=True
813
1813
  )
814
1814
 
1815
+ # Configuration subcommand
1816
+ config_parser = subparsers.add_parser(
1817
+ "config", help="Manage Hippius SDK configuration"
1818
+ )
1819
+ config_subparsers = config_parser.add_subparsers(
1820
+ dest="config_action", help="Configuration action"
1821
+ )
1822
+
1823
+ # Get configuration value
1824
+ get_parser = config_subparsers.add_parser("get", help="Get a configuration value")
1825
+ get_parser.add_argument(
1826
+ "section",
1827
+ help="Configuration section (ipfs, substrate, encryption, erasure_coding, cli)",
1828
+ )
1829
+ get_parser.add_argument("key", help="Configuration key")
1830
+
1831
+ # Set configuration value
1832
+ set_parser = config_subparsers.add_parser("set", help="Set a configuration value")
1833
+ set_parser.add_argument(
1834
+ "section",
1835
+ help="Configuration section (ipfs, substrate, encryption, erasure_coding, cli)",
1836
+ )
1837
+ set_parser.add_argument("key", help="Configuration key")
1838
+ set_parser.add_argument("value", help="Value to set (use JSON for complex values)")
1839
+
1840
+ # List all configuration values
1841
+ config_subparsers.add_parser("list", help="List all configuration values")
1842
+
1843
+ # Reset configuration to defaults
1844
+ config_subparsers.add_parser("reset", help="Reset configuration to default values")
1845
+
1846
+ # Import config from .env
1847
+ config_subparsers.add_parser(
1848
+ "import-env", help="Import configuration from .env file"
1849
+ )
1850
+
1851
+ # Seed Phrase subcommand
1852
+ seed_parser = subparsers.add_parser("seed", help="Manage substrate seed phrase")
1853
+ seed_subparsers = seed_parser.add_subparsers(
1854
+ dest="seed_action", help="Seed phrase action"
1855
+ )
1856
+
1857
+ # Set seed phrase
1858
+ set_seed_parser = seed_subparsers.add_parser(
1859
+ "set", help="Set the substrate seed phrase"
1860
+ )
1861
+ set_seed_parser.add_argument(
1862
+ "seed_phrase", help="The mnemonic seed phrase (e.g., 'word1 word2 word3...')"
1863
+ )
1864
+ set_seed_parser.add_argument(
1865
+ "--encode", action="store_true", help="Encrypt the seed phrase with a password"
1866
+ )
1867
+ set_seed_parser.add_argument(
1868
+ "--account", help="Account name to associate with this seed phrase"
1869
+ )
1870
+
1871
+ # Encode existing seed phrase
1872
+ encode_seed_parser = seed_subparsers.add_parser(
1873
+ "encode", help="Encrypt the existing seed phrase"
1874
+ )
1875
+ encode_seed_parser.add_argument(
1876
+ "--account", help="Account name to encode the seed phrase for"
1877
+ )
1878
+
1879
+ # Decode seed phrase
1880
+ decode_seed_parser = seed_subparsers.add_parser(
1881
+ "decode", help="Temporarily decrypt and display the seed phrase"
1882
+ )
1883
+ decode_seed_parser.add_argument(
1884
+ "--account", help="Account name to decode the seed phrase for"
1885
+ )
1886
+
1887
+ # Check seed phrase status
1888
+ status_seed_parser = seed_subparsers.add_parser(
1889
+ "status", help="Check the status of the configured seed phrase"
1890
+ )
1891
+ status_seed_parser.add_argument(
1892
+ "--account", help="Account name to check the status for"
1893
+ )
1894
+
1895
+ # Account subcommand
1896
+ account_parser = subparsers.add_parser("account", help="Manage substrate accounts")
1897
+ account_subparsers = account_parser.add_subparsers(
1898
+ dest="account_action", help="Account action"
1899
+ )
1900
+
1901
+ # List accounts
1902
+ account_subparsers.add_parser("list", help="List all accounts")
1903
+
1904
+ # Switch active account
1905
+ switch_account_parser = account_subparsers.add_parser(
1906
+ "switch", help="Switch to a different account"
1907
+ )
1908
+ switch_account_parser.add_argument(
1909
+ "account_name", help="Name of the account to switch to"
1910
+ )
1911
+
1912
+ # Delete account
1913
+ delete_account_parser = account_subparsers.add_parser(
1914
+ "delete", help="Delete an account"
1915
+ )
1916
+ delete_account_parser.add_argument(
1917
+ "account_name", help="Name of the account to delete"
1918
+ )
1919
+
1920
+ # Address subcommand for read-only operations
1921
+ address_parser = subparsers.add_parser(
1922
+ "address", help="Manage default address for read-only operations"
1923
+ )
1924
+ address_subparsers = address_parser.add_subparsers(
1925
+ dest="address_action", help="Address action"
1926
+ )
1927
+
1928
+ # Set default address
1929
+ set_default_parser = address_subparsers.add_parser(
1930
+ "set-default", help="Set the default address for read-only operations"
1931
+ )
1932
+ set_default_parser.add_argument(
1933
+ "address", help="The SS58 address to use as default"
1934
+ )
1935
+
1936
+ # Get current default address
1937
+ address_subparsers.add_parser(
1938
+ "get-default", help="Show the current default address for read-only operations"
1939
+ )
1940
+
1941
+ # Clear default address
1942
+ address_subparsers.add_parser(
1943
+ "clear-default", help="Clear the default address for read-only operations"
1944
+ )
1945
+
815
1946
  args = parser.parse_args()
816
1947
 
817
1948
  if not args.command:
@@ -839,8 +1970,44 @@ examples:
839
1970
  for miner in os.getenv("SUBSTRATE_DEFAULT_MINERS").split(",")
840
1971
  ]
841
1972
 
842
- # Create client
843
- client, encrypt, decrypt = create_client(args)
1973
+ # Process encryption flags
1974
+ encrypt = None
1975
+ if hasattr(args, "encrypt") and args.encrypt:
1976
+ encrypt = True
1977
+ elif hasattr(args, "no_encrypt") and args.no_encrypt:
1978
+ encrypt = False
1979
+
1980
+ decrypt = None
1981
+ if hasattr(args, "decrypt") and args.decrypt:
1982
+ decrypt = True
1983
+ elif hasattr(args, "no_decrypt") and args.no_decrypt:
1984
+ decrypt = False
1985
+
1986
+ # Process encryption key if provided
1987
+ encryption_key = None
1988
+ if hasattr(args, "encryption_key") and args.encryption_key:
1989
+ try:
1990
+ encryption_key = base64.b64decode(args.encryption_key)
1991
+ if args.verbose:
1992
+ print(f"Using provided encryption key")
1993
+ except Exception as e:
1994
+ print(f"Warning: Could not decode encryption key: {e}")
1995
+ print(f"Using default encryption key from configuration if available")
1996
+
1997
+ # Get API URL based on local_ipfs flag
1998
+ api_url = "http://localhost:5001" if args.local_ipfs else args.api_url
1999
+
2000
+ # Create client - using the updated client parameters
2001
+ client = HippiusClient(
2002
+ ipfs_gateway=args.gateway,
2003
+ ipfs_api_url=api_url,
2004
+ substrate_url=args.substrate_url,
2005
+ substrate_seed_phrase=None, # Let it use config
2006
+ seed_phrase_password=args.password if hasattr(args, "password") else None,
2007
+ account_name=args.account if hasattr(args, "account") else None,
2008
+ encrypt_by_default=encrypt,
2009
+ encryption_key=encryption_key,
2010
+ )
844
2011
 
845
2012
  # Handle commands
846
2013
  if args.command == "download":
@@ -871,6 +2038,16 @@ examples:
871
2038
  else False,
872
2039
  )
873
2040
 
2041
+ elif args.command == "ec-files":
2042
+ return handle_ec_files(
2043
+ client,
2044
+ args.account_address,
2045
+ show_all_miners=args.all_miners
2046
+ if hasattr(args, "all_miners")
2047
+ else False,
2048
+ show_chunks=args.show_chunks if hasattr(args, "show_chunks") else False,
2049
+ )
2050
+
874
2051
  elif args.command == "erasure-code":
875
2052
  return handle_erasure_code(
876
2053
  client,
@@ -888,6 +2065,62 @@ examples:
888
2065
  client, args.metadata_cid, args.output_file, verbose=args.verbose
889
2066
  )
890
2067
 
2068
+ elif args.command == "config":
2069
+ if args.config_action == "get":
2070
+ return handle_config_get(args.section, args.key)
2071
+ elif args.config_action == "set":
2072
+ return handle_config_set(args.section, args.key, args.value)
2073
+ elif args.config_action == "list":
2074
+ return handle_config_list()
2075
+ elif args.config_action == "reset":
2076
+ return handle_config_reset()
2077
+ elif args.config_action == "import-env":
2078
+ initialize_from_env()
2079
+ print("Successfully imported configuration from environment variables")
2080
+ return 0
2081
+ else:
2082
+ config_parser.print_help()
2083
+ return 1
2084
+
2085
+ elif args.command == "seed":
2086
+ if args.seed_action == "set":
2087
+ return handle_seed_phrase_set(
2088
+ args.seed_phrase, args.encode, args.account
2089
+ )
2090
+ elif args.seed_action == "encode":
2091
+ return handle_seed_phrase_encode(args.account)
2092
+ elif args.seed_action == "decode":
2093
+ return handle_seed_phrase_decode(args.account)
2094
+ elif args.seed_action == "status":
2095
+ return handle_seed_phrase_status(args.account)
2096
+ else:
2097
+ seed_parser.print_help()
2098
+ return 1
2099
+
2100
+ # Handle the account commands
2101
+ elif args.command == "account":
2102
+ if args.account_action == "list":
2103
+ return handle_account_list()
2104
+ elif args.account_action == "switch":
2105
+ return handle_account_switch(args.account_name)
2106
+ elif args.account_action == "delete":
2107
+ return handle_account_delete(args.account_name)
2108
+ else:
2109
+ account_parser.print_help()
2110
+ return 1
2111
+
2112
+ # Handle the address commands
2113
+ elif args.command == "address":
2114
+ if args.address_action == "set-default":
2115
+ return handle_default_address_set(args.address)
2116
+ elif args.address_action == "get-default":
2117
+ return handle_default_address_get()
2118
+ elif args.address_action == "clear-default":
2119
+ return handle_default_address_clear()
2120
+ else:
2121
+ address_parser.print_help()
2122
+ return 1
2123
+
891
2124
  except Exception as e:
892
2125
  print(f"Error: {e}")
893
2126
  return 1