hippius 0.1.0__py3-none-any.whl → 0.1.7__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,55 +526,1112 @@ 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
+
829
+ def handle_erasure_code(
830
+ client, file_path, k, m, chunk_size, miner_ids, encrypt=None, verbose=True
831
+ ):
832
+ """Handle the erasure-code command"""
833
+ if not os.path.exists(file_path):
834
+ print(f"Error: File {file_path} not found")
835
+ return 1
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
+
879
+ # Check if zfec is installed
880
+ try:
881
+ import zfec
882
+ except ImportError:
883
+ print(
884
+ "Error: zfec is required for erasure coding. Install it with: pip install zfec"
885
+ )
886
+ print("Then update your environment: poetry add zfec")
887
+ return 1
888
+
889
+ # Parse miner IDs if provided
890
+ miner_id_list = None
891
+ if miner_ids:
892
+ miner_id_list = [m.strip() for m in miner_ids.split(",") if m.strip()]
893
+ if verbose:
894
+ print(f"Targeting {len(miner_id_list)} miners: {', '.join(miner_id_list)}")
895
+
896
+ # Get the file size and adjust parameters if needed
897
+ file_size = os.path.getsize(file_path)
898
+ file_size_mb = file_size / (1024 * 1024)
899
+
900
+ print(f"Processing {file_path} ({file_size_mb:.2f} MB) with erasure coding...")
901
+
902
+ # Check if the file is too small for the current chunk size and k value
903
+ original_k = k
904
+ original_m = m
905
+ original_chunk_size = chunk_size
906
+
907
+ # Calculate how many chunks we would get with current settings
908
+ potential_chunks = max(1, file_size // chunk_size)
909
+
910
+ # If we can't get at least k chunks, adjust the chunk size
911
+ if potential_chunks < k:
912
+ # Calculate a new chunk size that would give us exactly k chunks
913
+ new_chunk_size = max(1024, file_size // k) # Ensure at least 1KB chunks
914
+
915
+ print(f"Warning: File is too small for the requested parameters.")
916
+ print(
917
+ f"Original parameters: k={k}, m={m}, chunk size={chunk_size/1024/1024:.2f} MB"
918
+ )
919
+ print(f"Would create only {potential_chunks} chunks, which is less than k={k}")
920
+ print(
921
+ f"Automatically adjusting chunk size to {new_chunk_size/1024/1024:.6f} MB to create at least {k} chunks"
922
+ )
923
+
924
+ chunk_size = new_chunk_size
925
+
926
+ print(f"Final parameters: k={k}, m={m} (need {k} of {m} chunks to reconstruct)")
927
+ print(f"Chunk size: {chunk_size/1024/1024:.6f} MB")
928
+
929
+ if encrypt:
930
+ print("Encryption: Enabled")
931
+
932
+ start_time = time.time()
933
+
934
+ try:
935
+ # Use the store_erasure_coded_file method directly from HippiusClient
936
+ result = client.store_erasure_coded_file(
937
+ file_path=file_path,
938
+ k=k,
939
+ m=m,
940
+ chunk_size=chunk_size,
941
+ encrypt=encrypt,
942
+ miner_ids=miner_id_list,
943
+ max_retries=3,
944
+ verbose=verbose,
945
+ )
946
+
947
+ elapsed_time = time.time() - start_time
948
+
949
+ print(f"\nErasure coding and storage completed in {elapsed_time:.2f} seconds!")
950
+
951
+ # Display metadata
952
+ metadata = result.get("metadata", {})
953
+ metadata_cid = result.get("metadata_cid", "unknown")
954
+ total_files_stored = result.get("total_files_stored", 0)
955
+
956
+ original_file = metadata.get("original_file", {})
957
+ erasure_coding = metadata.get("erasure_coding", {})
958
+
959
+ print("\nErasure Coding Summary:")
960
+ print(
961
+ f" Original file: {original_file.get('name')} ({original_file.get('size', 0)/1024/1024:.2f} MB)"
962
+ )
963
+ print(f" File ID: {erasure_coding.get('file_id')}")
964
+ print(f" Parameters: k={erasure_coding.get('k')}, m={erasure_coding.get('m')}")
965
+ print(f" Total chunks: {len(metadata.get('chunks', []))}")
966
+ print(f" Total files stored in marketplace: {total_files_stored}")
967
+ print(f" Metadata CID: {metadata_cid}")
968
+
969
+ # If we stored in the marketplace
970
+ if "transaction_hash" in result:
971
+ print(
972
+ f"\nStored in marketplace. Transaction hash: {result['transaction_hash']}"
973
+ )
974
+
975
+ # Instructions for reconstruction
976
+ print("\nTo reconstruct this file, you will need:")
977
+ print(f" 1. The metadata CID: {metadata_cid}")
978
+ print(" 2. Access to at least k chunks for each original chunk")
979
+ print("\nReconstruction command:")
980
+ print(
981
+ f" hippius reconstruct {metadata_cid} reconstructed_{original_file.get('name')}"
982
+ )
983
+
984
+ return 0
985
+
986
+ except Exception as e:
987
+ print(f"Error during erasure coding: {e}")
988
+
989
+ # Provide helpful advice based on the error
990
+ if "Wrong length" in str(e) and "input blocks" in str(e):
991
+ print("\nThis error typically occurs with very small files.")
992
+ print("Suggestions:")
993
+ print(" 1. Try using a smaller chunk size: --chunk-size 4096")
994
+ print(" 2. Try using a smaller k value: --k 2")
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")
1018
+ return 1
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
+
1149
+
1150
+ def handle_reconstruct(client, metadata_cid, output_file, verbose=True):
1151
+ """Handle the reconstruct command for erasure-coded files"""
1152
+ # Check if zfec is installed
1153
+ try:
1154
+ import zfec
1155
+ except ImportError:
1156
+ print(
1157
+ "Error: zfec is required for erasure coding. Install it with: pip install zfec"
1158
+ )
1159
+ print("Then update your environment: poetry add zfec")
1160
+ return 1
1161
+
1162
+ print(f"Reconstructing file from metadata CID: {metadata_cid}")
1163
+ print(f"Output file: {output_file}")
1164
+
1165
+ start_time = time.time()
1166
+
1167
+ try:
1168
+ # Use the reconstruct_from_erasure_code method
1169
+ result = client.reconstruct_from_erasure_code(
1170
+ metadata_cid=metadata_cid, output_file=output_file, verbose=verbose
1171
+ )
1172
+
1173
+ elapsed_time = time.time() - start_time
1174
+ print(f"\nFile reconstruction completed in {elapsed_time:.2f} seconds!")
1175
+ print(f"Reconstructed file saved to: {result}")
1176
+
1177
+ return 0
1178
+
1179
+ except Exception as e:
1180
+ print(f"Error during file reconstruction: {e}")
1181
+ return 1
1182
+
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
+
438
1563
  def main():
439
- """Main entry point for the Hippius CLI."""
1564
+ """Main CLI entry point for hippius command."""
1565
+ # Set up the argument parser
440
1566
  parser = argparse.ArgumentParser(
441
1567
  description="Hippius SDK Command Line Interface",
442
1568
  formatter_class=argparse.RawDescriptionHelpFormatter,
443
1569
  epilog="""
444
- Examples:
445
- hippius download QmCID123 downloaded_file.txt
446
- hippius exists QmCID123
447
- hippius cat QmCID123
448
- hippius store test_file.txt
449
- hippius store-dir ./test_directory
1570
+ examples:
1571
+ # Store a file
1572
+ hippius store example.txt
1573
+
1574
+ # Store a directory
1575
+ hippius store-dir ./my_directory
1576
+
1577
+ # Download a file
1578
+ hippius download QmHash output.txt
1579
+
1580
+ # Check if a CID exists
1581
+ hippius exists QmHash
1582
+
1583
+ # View the content of a CID
1584
+ hippius cat QmHash
1585
+
1586
+ # View your available credits
450
1587
  hippius credits
451
- hippius credits 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
1588
+
1589
+ # View your stored files
452
1590
  hippius files
453
- hippius files 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
1591
+
1592
+ # View all miners for stored files
454
1593
  hippius files --all-miners
455
- hippius keygen
456
- hippius keygen --copy
1594
+
1595
+ # Erasure code a file (Reed-Solomon)
1596
+ hippius erasure-code large_file.mp4 --k 3 --m 5
1597
+
1598
+ # Reconstruct an erasure-coded file
1599
+ hippius reconstruct QmMetadataHash reconstructed_file.mp4
457
1600
  """,
458
1601
  )
459
1602
 
460
1603
  # Optional arguments for all commands
461
1604
  parser.add_argument(
462
1605
  "--gateway",
463
- default=os.getenv("IPFS_GATEWAY", "https://ipfs.io"),
464
- 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)",
465
1608
  )
466
1609
  parser.add_argument(
467
1610
  "--api-url",
468
- default=os.getenv("IPFS_API_URL", "https://relay-fr.hippius.network"),
469
- help="IPFS API URL for uploads (default: from env or https://relay-fr.hippius.network)",
1611
+ default=get_config_value("ipfs", "api_url", "https://relay-fr.hippius.network"),
1612
+ help="IPFS API URL for uploads (default: from config or https://relay-fr.hippius.network)",
470
1613
  )
471
1614
  parser.add_argument(
472
1615
  "--local-ipfs",
473
1616
  action="store_true",
1617
+ default=get_config_value("ipfs", "local_ipfs", False),
474
1618
  help="Use local IPFS node (http://localhost:5001) instead of remote API",
475
1619
  )
476
1620
  parser.add_argument(
477
1621
  "--substrate-url",
478
- default=os.getenv("SUBSTRATE_URL", "wss://rpc.hippius.network"),
479
- 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)",
480
1624
  )
481
1625
  parser.add_argument(
482
1626
  "--miner-ids",
483
- 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)",
484
1628
  )
485
1629
  parser.add_argument(
486
- "--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",
487
1635
  )
488
1636
  parser.add_argument(
489
1637
  "--encrypt",
@@ -509,6 +1657,14 @@ Examples:
509
1657
  "--encryption-key",
510
1658
  help="Base64-encoded encryption key (overrides HIPPIUS_ENCRYPTION_KEY in .env)",
511
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
+ )
512
1668
 
513
1669
  # Subcommands
514
1670
  subparsers = parser.add_subparsers(dest="command", help="Commands")
@@ -580,6 +1736,27 @@ Examples:
580
1736
  help="Show all miners for each file instead of only the first 3",
581
1737
  )
582
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
+
583
1760
  # Key generation command
584
1761
  keygen_parser = subparsers.add_parser(
585
1762
  "keygen", help="Generate an encryption key for secure file storage"
@@ -588,6 +1765,184 @@ Examples:
588
1765
  "--copy", action="store_true", help="Copy the generated key to the clipboard"
589
1766
  )
590
1767
 
1768
+ # Erasure code command
1769
+ erasure_code_parser = subparsers.add_parser(
1770
+ "erasure-code", help="Erasure code a file"
1771
+ )
1772
+ erasure_code_parser.add_argument("file_path", help="Path to file to erasure code")
1773
+ erasure_code_parser.add_argument(
1774
+ "--k",
1775
+ type=int,
1776
+ default=3,
1777
+ help="Number of data chunks needed to reconstruct (default: 3)",
1778
+ )
1779
+ erasure_code_parser.add_argument(
1780
+ "--m", type=int, default=5, help="Total number of chunks to create (default: 5)"
1781
+ )
1782
+ erasure_code_parser.add_argument(
1783
+ "--chunk-size",
1784
+ type=int,
1785
+ default=1048576,
1786
+ help="Chunk size in bytes (default: 1MB)",
1787
+ )
1788
+ erasure_code_parser.add_argument(
1789
+ "--miner-ids", help="Comma-separated list of miner IDs"
1790
+ )
1791
+ erasure_code_parser.add_argument(
1792
+ "--encrypt", action="store_true", help="Encrypt the file"
1793
+ )
1794
+ erasure_code_parser.add_argument(
1795
+ "--no-encrypt", action="store_true", help="Do not encrypt the file"
1796
+ )
1797
+ erasure_code_parser.add_argument(
1798
+ "--verbose", action="store_true", help="Enable verbose output", default=True
1799
+ )
1800
+
1801
+ # Reconstruct command
1802
+ reconstruct_parser = subparsers.add_parser(
1803
+ "reconstruct", help="Reconstruct an erasure-coded file"
1804
+ )
1805
+ reconstruct_parser.add_argument(
1806
+ "metadata_cid", help="Metadata CID of the erasure-coded file"
1807
+ )
1808
+ reconstruct_parser.add_argument(
1809
+ "output_file", help="Path to save reconstructed file"
1810
+ )
1811
+ reconstruct_parser.add_argument(
1812
+ "--verbose", action="store_true", help="Enable verbose output", default=True
1813
+ )
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
+
591
1946
  args = parser.parse_args()
592
1947
 
593
1948
  if not args.command:
@@ -615,8 +1970,44 @@ Examples:
615
1970
  for miner in os.getenv("SUBSTRATE_DEFAULT_MINERS").split(",")
616
1971
  ]
617
1972
 
618
- # Create client
619
- 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
+ )
620
2011
 
621
2012
  # Handle commands
622
2013
  if args.command == "download":
@@ -647,6 +2038,89 @@ Examples:
647
2038
  else False,
648
2039
  )
649
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
+
2051
+ elif args.command == "erasure-code":
2052
+ return handle_erasure_code(
2053
+ client,
2054
+ args.file_path,
2055
+ args.k,
2056
+ args.m,
2057
+ args.chunk_size,
2058
+ miner_ids,
2059
+ encrypt=args.encrypt,
2060
+ verbose=args.verbose,
2061
+ )
2062
+
2063
+ elif args.command == "reconstruct":
2064
+ return handle_reconstruct(
2065
+ client, args.metadata_cid, args.output_file, verbose=args.verbose
2066
+ )
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
+
650
2124
  except Exception as e:
651
2125
  print(f"Error: {e}")
652
2126
  return 1