hippius 0.1.6__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-0.1.6.dist-info → hippius-0.1.7.dist-info}/METADATA +274 -4
- hippius-0.1.7.dist-info/RECORD +10 -0
- hippius_sdk/__init__.py +45 -1
- hippius_sdk/cli.py +1269 -36
- hippius_sdk/client.py +53 -12
- hippius_sdk/config.py +744 -0
- hippius_sdk/ipfs.py +178 -87
- hippius_sdk/substrate.py +130 -68
- hippius-0.1.6.dist-info/RECORD +0 -9
- {hippius-0.1.6.dist-info → hippius-0.1.7.dist-info}/WHEEL +0 -0
- {hippius-0.1.6.dist-info → hippius-0.1.7.dist-info}/entry_points.txt +0 -0
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
|
111
|
-
|
112
|
-
#
|
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=
|
115
|
-
ipfs_api_url=
|
116
|
-
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
|
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(
|
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(
|
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(
|
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=
|
641
|
-
help="IPFS gateway URL for downloads (default: from
|
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=
|
646
|
-
help="IPFS API URL for uploads (default: from
|
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)",
|
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=
|
656
|
-
help="Substrate node WebSocket URL (default: from
|
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
|
1627
|
+
help="Comma-separated list of miner IDs for storage (default: from config)",
|
661
1628
|
)
|
662
1629
|
parser.add_argument(
|
663
|
-
"--verbose",
|
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
|
-
#
|
843
|
-
|
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
|