hippius 0.2.2__py3-none-any.whl → 0.2.3__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
@@ -6,2555 +6,84 @@ This module provides CLI tools for working with the Hippius SDK, including
6
6
  utilities for encryption key generation, file operations, and marketplace interactions.
7
7
  """
8
8
 
9
- import argparse
10
9
  import asyncio
11
- import base64
12
- import concurrent.futures
13
- import getpass
14
10
  import inspect
15
- import json
16
11
  import os
17
12
  import sys
18
- import time
13
+ from typing import Callable
19
14
 
20
15
  from dotenv import load_dotenv
21
16
 
22
- # Import SDK components
23
- from hippius_sdk import (
24
- HippiusClient,
25
- decrypt_seed_phrase,
26
- delete_account,
27
- encrypt_seed_phrase,
28
- get_account_address,
29
- get_active_account,
30
- get_all_config,
31
- get_config_value,
32
- get_seed_phrase,
33
- initialize_from_env,
34
- list_accounts,
35
- load_config,
36
- reset_config,
37
- save_config,
38
- set_active_account,
39
- set_config_value,
40
- set_seed_phrase,
41
- )
42
-
43
- try:
44
- import nacl.secret
45
- import nacl.utils
46
- except ImportError:
47
- ENCRYPTION_AVAILABLE = False
48
- else:
49
- ENCRYPTION_AVAILABLE = True
50
-
51
- load_dotenv()
52
- initialize_from_env()
53
-
54
-
55
- def get_default_address():
56
- """Get the default address for read-only operations"""
57
- config = load_config()
58
- return config["substrate"].get("default_address")
59
-
60
-
61
- def generate_key():
62
- """Generate a random encryption key for NaCl secretbox."""
63
- if not ENCRYPTION_AVAILABLE:
64
- print(
65
- "Error: PyNaCl is required for encryption. Install it with: pip install pynacl"
66
- )
67
- sys.exit(1)
68
-
69
- # Generate a random key
70
- key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
71
-
72
- # Encode to base64 for .env file
73
- encoded_key = base64.b64encode(key).decode()
74
-
75
- return encoded_key
76
-
77
-
78
- def key_generation_cli():
79
- """CLI entry point for encryption key generation."""
80
- parser = argparse.ArgumentParser(
81
- description="Generate a secure encryption key for Hippius SDK"
82
- )
83
- parser.add_argument("--copy", action="store_true", help="Copy the key to clipboard")
84
- args = parser.parse_args()
85
-
86
- # Generate the key
87
- encoded_key = generate_key()
88
-
89
- # Copy to clipboard if requested
90
- if args.copy:
91
- try:
92
- import pyperclip
93
-
94
- pyperclip.copy(encoded_key)
95
- print("Key copied to clipboard!")
96
- except ImportError:
97
- print(
98
- "Warning: Could not copy to clipboard. Install pyperclip with: pip install pyperclip"
99
- )
100
-
101
- # Print instructions
102
- print("\nGenerated a new encryption key for Hippius SDK")
103
- print(f"Key: {encoded_key}")
104
- print("\nAdd this to your .env file:")
105
- print(f"HIPPIUS_ENCRYPTION_KEY={encoded_key}")
106
- print("\nOr configure it in your code:")
107
- print("import base64")
108
- print(f'encryption_key = base64.b64decode("{encoded_key}")')
109
- print(
110
- "client = HippiusClient(encrypt_by_default=True, encryption_key=encryption_key)"
111
- )
112
-
113
-
114
- def create_client(args):
115
- """Create a HippiusClient instance from command line arguments."""
116
- # Process encryption flags
117
- encrypt = None
118
- if hasattr(args, "encrypt") and args.encrypt:
119
- encrypt = True
120
- elif hasattr(args, "no_encrypt") and args.no_encrypt:
121
- encrypt = False
122
-
123
- decrypt = None
124
- if hasattr(args, "decrypt") and args.decrypt:
125
- decrypt = True
126
- elif hasattr(args, "no_decrypt") and args.no_decrypt:
127
- decrypt = False
128
-
129
- # Process encryption key if provided
130
- encryption_key = None
131
- if hasattr(args, "encryption_key") and args.encryption_key:
132
- try:
133
- encryption_key = base64.b64decode(args.encryption_key)
134
- if hasattr(args, "verbose") and args.verbose:
135
- print(f"Using provided encryption key")
136
- except Exception as e:
137
- print(f"Warning: Could not decode encryption key: {e}")
138
- print(f"Using default encryption key from configuration if available")
139
-
140
- # Get API URL based on local_ipfs flag if the flag exists
141
- api_url = None
142
- if hasattr(args, "local_ipfs") and args.local_ipfs:
143
- api_url = "http://localhost:5001"
144
- elif hasattr(args, "api_url"):
145
- api_url = args.api_url
146
- elif hasattr(args, "ipfs_api"):
147
- api_url = args.ipfs_api
148
-
149
- # Get gateway URL
150
- gateway = None
151
- if hasattr(args, "gateway"):
152
- gateway = args.gateway
153
- elif hasattr(args, "ipfs_gateway"):
154
- gateway = args.ipfs_gateway
155
-
156
- # Get substrate URL
157
- substrate_url = args.substrate_url if hasattr(args, "substrate_url") else None
158
-
159
- # Initialize client with provided parameters
160
- client = HippiusClient(
161
- ipfs_gateway=gateway,
162
- ipfs_api_url=api_url,
163
- substrate_url=substrate_url,
164
- substrate_seed_phrase=(
165
- args.seed_phrase if hasattr(args, "seed_phrase") else None
166
- ),
167
- seed_phrase_password=args.password if hasattr(args, "password") else None,
168
- account_name=args.account if hasattr(args, "account") else None,
169
- encrypt_by_default=encrypt,
170
- encryption_key=encryption_key,
171
- )
172
-
173
- return client
174
-
175
-
176
- async def handle_download(client, cid, output_path, decrypt=None):
177
- """Handle the download command"""
178
- print(f"Downloading {cid} to {output_path}...")
179
-
180
- # Use the enhanced download method which returns formatted information
181
- result = await client.download_file(cid, output_path, decrypt=decrypt)
182
-
183
- print(f"Download successful in {result['elapsed_seconds']} seconds!")
184
- print(f"Saved to: {result['output_path']}")
185
- print(f"Size: {result['size_bytes']:,} bytes ({result['size_formatted']})")
186
-
187
- if result.get("decrypted"):
188
- print("File was decrypted during download")
189
-
190
- return 0
191
-
192
-
193
- async def handle_exists(client, cid):
194
- """Handle the exists command"""
195
- print(f"Checking if CID {cid} exists on IPFS...")
196
- result = await client.exists(cid)
197
-
198
- # Use the formatted CID from the result
199
- formatted_cid = result["formatted_cid"]
200
- exists = result["exists"]
201
-
202
- print(f"CID {formatted_cid} exists: {exists}")
203
-
204
- if exists and result.get("gateway_url"):
205
- print(f"Gateway URL: {result['gateway_url']}")
206
- print("\nTo download this file, you can run:")
207
- print(f" hippius download {formatted_cid} <output_path>")
208
-
209
- return 0
210
-
211
-
212
- async def handle_cat(client, cid, max_size, decrypt=None):
213
- """Handle the cat command"""
214
- print(f"Retrieving content of CID {cid}...")
215
- try:
216
- # Use the enhanced cat method with formatting
217
- result = await client.cat(cid, max_display_bytes=max_size, decrypt=decrypt)
218
-
219
- # Display file information
220
- print(
221
- f"Content size: {result['size_bytes']:,} bytes ({result['size_formatted']})"
222
- )
223
-
224
- if result.get("decrypted"):
225
- print("Content was decrypted")
226
-
227
- # Display content based on type
228
- if result["is_text"]:
229
- print("\nContent (text):")
230
- print(result["text_preview"])
231
- if result["size_bytes"] > max_size:
232
- print(
233
- f"\n... (showing first {max_size} bytes of {result['size_bytes']} total) ..."
234
- )
235
- else:
236
- print("\nBinary content (hex):")
237
- print(result["hex_preview"])
238
- if result["size_bytes"] > max_size:
239
- print(
240
- f"\n... (showing first {max_size} bytes of {result['size_bytes']} total) ..."
241
- )
242
-
243
- except Exception as e:
244
- print(f"Error retrieving content: {e}")
245
- return 1
246
-
247
- return 0
248
-
249
-
250
- async def handle_store(client, file_path, miner_ids, encrypt=None):
251
- """Handle the store command"""
252
- if not os.path.exists(file_path):
253
- print(f"Error: File {file_path} not found")
254
- return 1
255
-
256
- print(f"Uploading {file_path} to IPFS...")
257
- start_time = time.time()
258
-
259
- # Use the enhanced upload_file method that returns formatted information
260
- result = await client.upload_file(file_path, encrypt=encrypt)
261
-
262
- ipfs_elapsed_time = time.time() - start_time
263
-
264
- print(f"IPFS upload successful in {ipfs_elapsed_time:.2f} seconds!")
265
- print(f"CID: {result['cid']}")
266
- print(f"Filename: {result['filename']}")
267
- print(f"Size: {result['size_bytes']:,} bytes ({result['size_formatted']})")
268
-
269
- if result.get("encrypted"):
270
- print("File was encrypted before upload")
271
-
272
- # Store the file on Substrate
273
- print("\nStoring the file on Substrate...")
274
- start_time = time.time()
275
-
276
- try:
277
- # Check if we have credits
278
- try:
279
- if hasattr(client.substrate_client, "get_free_credits"):
280
- credits = client.substrate_client.get_free_credits()
281
- print(f"Account credits: {credits}")
282
- if credits <= 0:
283
- print(
284
- f"Warning: Account has no free credits (current: {credits}). Transaction may fail."
285
- )
286
- except Exception as e:
287
- print(f"Warning: Could not check free credits: {e}")
288
-
289
- # Create a file input object for the marketplace
290
- file_input = {"fileHash": result["cid"], "fileName": result["filename"]}
291
-
292
- # Store on Substrate - now it's an async call
293
- tx_hash = await client.substrate_client.storage_request([file_input], miner_ids)
294
-
295
- substrate_elapsed_time = time.time() - start_time
296
- print(
297
- f"Substrate storage request completed in {substrate_elapsed_time:.2f} seconds!"
298
- )
299
-
300
- # Suggestion to verify
301
- print("\nTo verify the IPFS upload, you can run:")
302
- print(f" hippius exists {result['cid']}")
303
- print(f" hippius cat {result['cid']}")
304
-
305
- except NotImplementedError as e:
306
- print(f"\nNote: {e}")
307
- except Exception as e:
308
- print(f"\nError storing file on Substrate: {e}")
309
- return 1
310
-
311
- return 0
312
-
313
-
314
- async def handle_store_dir(client, dir_path, miner_ids, encrypt=None):
315
- """Handle the store-dir command"""
316
- if not os.path.isdir(dir_path):
317
- print(f"Error: Directory {dir_path} not found")
318
- return 1
319
-
320
- print(f"Uploading directory {dir_path} to IPFS...")
321
- start_time = time.time()
322
-
323
- # We'll manually upload each file first to get individual CIDs
324
- all_files = []
325
- for root, _, files in os.walk(dir_path):
326
- for file in files:
327
- file_path = os.path.join(root, file)
328
- rel_path = os.path.relpath(file_path, dir_path)
329
- all_files.append((file_path, rel_path))
330
-
331
- print(f"Found {len(all_files)} files to upload")
332
-
333
- # Upload each file individually to get all CIDs
334
- individual_cids = []
335
- for file_path, rel_path in all_files:
336
- try:
337
- print(f" Uploading: {rel_path}")
338
- file_result = await client.upload_file(file_path, encrypt=encrypt)
339
- individual_cids.append(
340
- {
341
- "path": rel_path,
342
- "cid": file_result["cid"],
343
- "filename": file_result["filename"],
344
- "size_bytes": file_result["size_bytes"],
345
- "size_formatted": file_result.get("size_formatted", ""),
346
- "encrypted": file_result.get("encrypted", False),
347
- }
348
- )
349
- print(
350
- f" CID: {individual_cids[-1]['cid']} ({individual_cids[-1]['size_formatted']})"
351
- )
352
- if file_result.get("encrypted"):
353
- print(f" Encrypted: Yes")
354
- except Exception as e:
355
- print(f" Error uploading {rel_path}: {e}")
356
-
357
- # Now upload the entire directory
358
- result = await client.upload_directory(dir_path, encrypt=encrypt)
359
-
360
- ipfs_elapsed_time = time.time() - start_time
361
-
362
- print(f"\nIPFS directory upload successful in {ipfs_elapsed_time:.2f} seconds!")
363
- print(f"Directory CID: {result['cid']}")
364
- print(f"Directory name: {result['dirname']}")
365
- print(f"Total files: {result.get('file_count', len(individual_cids))}")
366
- print(f"Total size: {result.get('size_formatted', 'Unknown')}")
367
-
368
- if result.get("encrypted"):
369
- print("Files were encrypted before upload")
370
-
371
- # Print summary of all individual file CIDs
372
- print(f"\nAll individual file CIDs ({len(individual_cids)}):")
373
- for item in individual_cids:
374
- print(f" {item['path']}: {item['cid']} ({item['size_formatted']})")
375
-
376
- # Suggestion to verify
377
- print("\nTo verify the IPFS directory upload, you can run:")
378
- print(f" hippius exists {result['cid']}")
379
-
380
- # Store all files on Substrate
381
- print("\nStoring all files on Substrate...")
382
- start_time = time.time()
383
-
384
- try:
385
- # Create file input objects for the marketplace
386
- file_inputs = []
387
- for item in individual_cids:
388
- file_inputs.append({"fileHash": item["cid"], "fileName": item["filename"]})
389
-
390
- # Store all files in a single batch request
391
- tx_hash = await client.substrate_client.storage_request(file_inputs, miner_ids)
392
-
393
- substrate_elapsed_time = time.time() - start_time
394
- print(
395
- f"Substrate storage request completed in {substrate_elapsed_time:.2f} seconds!"
396
- )
397
-
398
- except NotImplementedError as e:
399
- print(f"\nNote: {e}")
400
- except Exception as e:
401
- print(f"\nError storing files on Substrate: {e}")
402
- return 1
403
-
404
- return 0
405
-
406
-
407
- async def handle_credits(client, account_address):
408
- """Handle the credits command"""
409
- print("Checking free credits for the account...")
410
- try:
411
- # Get the account address we're querying
412
- if account_address is None:
413
- # If no address provided, first try to get from keypair (if available)
414
- if (
415
- hasattr(client.substrate_client, "_keypair")
416
- and client.substrate_client._keypair is not None
417
- ):
418
- account_address = client.substrate_client._keypair.ss58_address
419
- else:
420
- # Try to get the default address
421
- default_address = get_default_address()
422
- if default_address:
423
- account_address = default_address
424
- else:
425
- has_default = get_default_address() is not None
426
-
427
- print(
428
- "Error: No account address provided, and client has no keypair."
429
- )
430
-
431
- if has_default:
432
- print(
433
- "Please provide an account address with '--account_address' or the default address may be invalid."
434
- )
435
- else:
436
- print(
437
- "Please provide an account address with '--account_address' or set a default with:"
438
- )
439
- print(" hippius address set-default <your_account_address>")
440
-
441
- return 1
442
-
443
- credits = await client.substrate_client.get_free_credits(account_address)
444
- print(f"\nFree credits: {credits:.6f}")
445
- raw_value = int(
446
- credits * 1_000_000_000_000_000_000
447
- ) # Convert back to raw for display
448
- print(f"Raw value: {raw_value:,}")
449
- print(f"Account address: {account_address}")
450
- except Exception as e:
451
- print(f"Error checking credits: {e}")
452
- return 1
453
-
454
- return 0
455
-
456
-
457
- async def handle_files(client, account_address, show_all_miners=False):
458
- """
459
- Display files stored by a user in a nice format.
460
-
461
- This command only reads data and doesn't require seed phrase decryption.
462
- """
463
- try:
464
- # Get the account address we're querying
465
- if account_address is None:
466
- # If no address provided, first try to get from keypair (if available)
467
- if (
468
- hasattr(client.substrate_client, "_keypair")
469
- and client.substrate_client._keypair is not None
470
- ):
471
- account_address = client.substrate_client._keypair.ss58_address
472
- else:
473
- # Try to get the default address
474
- default_address = get_default_address()
475
- if default_address:
476
- account_address = default_address
477
- else:
478
- has_default = get_default_address() is not None
479
-
480
- print(
481
- "Error: No account address provided, and client has no keypair."
482
- )
483
-
484
- if has_default:
485
- print(
486
- "Please provide an account address with '--account_address' or the default address may be invalid."
487
- )
488
- else:
489
- print(
490
- "Please provide an account address with '--account_address' or set a default with:"
491
- )
492
- print(" hippius address set-default <your_account_address>")
493
- return 1
494
-
495
- # Get files for the account using the new profile-based method
496
- print(f"Retrieving files for account: {account_address}")
497
- files = await client.substrate_client.get_user_files_from_profile(
498
- account_address
499
- )
500
-
501
- # Check if any files were found
502
- if not files:
503
- print(f"No files found for account: {account_address}")
504
- return 0
505
-
506
- print(f"\nFound {len(files)} files for account: {account_address}")
507
- print("-" * 80)
508
-
509
- for i, file in enumerate(files, 1):
510
- try:
511
- print(f"File {i}:")
512
-
513
- # Display file hash/CID
514
- file_hash = file.get("file_hash", "Unknown")
515
- if file_hash is not None:
516
- formatted_cid = client.format_cid(file_hash)
517
- print(f" CID: {formatted_cid}")
518
- else:
519
- print(f" CID: Unknown (None)")
520
-
521
- # Display file name
522
- file_name = file.get("file_name", "Unnamed")
523
- print(
524
- f" File name: {file_name if file_name is not None else 'Unnamed'}"
525
- )
526
-
527
- # Display file size
528
- if "size_formatted" in file and file["size_formatted"] is not None:
529
- size_formatted = file["size_formatted"]
530
- file_size = file.get("file_size", 0)
531
- if file_size is not None and file_size > 0:
532
- print(f" File size: {file_size:,} bytes ({size_formatted})")
533
- else:
534
- print(f" File size: {size_formatted}")
535
- else:
536
- print(f" File size: Unknown")
537
-
538
- # Display miners (if available)
539
- miner_ids = file.get("miner_ids", [])
540
- miner_count = file.get("miner_count", 0)
541
-
542
- if miner_ids and show_all_miners:
543
- print(f" Stored by {len(miner_ids)} miners:")
544
- for miner in miner_ids:
545
- miner_id = (
546
- miner.get("id", miner) if isinstance(miner, dict) else miner
547
- )
548
- formatted = (
549
- miner.get("formatted", miner_id)
550
- if isinstance(miner, dict)
551
- else miner_id
552
- )
553
- print(f" - {formatted}")
554
- elif miner_count:
555
- print(f" Stored by {miner_count} miners")
556
- else:
557
- print(f" Storage information not available")
558
-
559
- print("-" * 80)
560
- except Exception as e:
561
- print(f" Error displaying file {i}: {e}")
562
- print("-" * 80)
563
- continue
564
-
565
- # Add tip for downloading
566
- if files:
567
- print("\nTo download a file, use:")
568
- print(f" hippius download <CID> <output_filename>")
569
-
570
- except Exception as e:
571
- print(f"Error retrieving files: {e}")
572
- return 1
573
-
574
- return 0
575
-
576
-
577
- async def handle_ec_files(
578
- client, account_address, show_all_miners=False, show_chunks=False
579
- ):
580
- """Handle the ec-files command to show only erasure-coded files"""
581
- print("Looking for erasure-coded files...")
582
- try:
583
- # Get the account address we're querying
584
- if account_address is None:
585
- # If no address provided, first try to get from keypair (if available)
586
- if (
587
- hasattr(client.substrate_client, "_keypair")
588
- and client.substrate_client._keypair is not None
589
- ):
590
- account_address = client.substrate_client._keypair.ss58_address
591
- else:
592
- # Try to get the default address
593
- default_address = get_default_address()
594
- if default_address:
595
- account_address = default_address
596
- else:
597
- has_default = get_default_address() is not None
598
-
599
- print(
600
- "Error: No account address provided, and client has no keypair."
601
- )
602
-
603
- if has_default:
604
- print(
605
- "Please provide an account address with '--account_address' or the default address may be invalid."
606
- )
607
- else:
608
- print(
609
- "Please provide an account address with '--account_address' or set a default with:"
610
- )
611
- print(" hippius address set-default <your_account_address>")
612
- return 1
613
-
614
- # First, get all user files using the profile method
615
- files = await client.substrate_client.get_user_files_from_profile(
616
- account_address
617
- )
618
-
619
- # Filter for metadata files (ending with .ec_metadata)
620
- ec_metadata_files = []
621
- for file in files:
622
- file_name = file.get("file_name", "")
623
- if (
624
- file_name
625
- and isinstance(file_name, str)
626
- and file_name.endswith(".ec_metadata")
627
- ):
628
- ec_metadata_files.append(file)
629
-
630
- if not ec_metadata_files:
631
- print(f"No erasure-coded files found for account {account_address}")
632
- return 0
633
-
634
- print(f"\nFound {len(ec_metadata_files)} erasure-coded files:")
635
- print("-" * 80)
636
-
637
- for i, file in enumerate(ec_metadata_files, 1):
638
- try:
639
- print(f"EC File {i}:")
640
-
641
- # Get the metadata CID
642
- metadata_cid = file.get("file_hash", "Unknown")
643
- if metadata_cid is not None and metadata_cid != "Unknown":
644
- formatted_cid = client.format_cid(metadata_cid)
645
- print(f" Metadata CID: {formatted_cid}")
646
-
647
- # Fetch and parse the metadata to get original file info
648
- try:
649
- # Use the formatted CID, not the raw hex-encoded version
650
- metadata = await client.ipfs_client.cat(formatted_cid)
651
-
652
- # Check if we have text content
653
- if metadata.get("is_text", False):
654
- # Parse the metadata content as JSON
655
- import json
656
-
657
- metadata_json = json.loads(metadata.get("content", "{}"))
658
-
659
- # Extract original file info
660
- # Check both possible formats
661
- original_file = metadata_json.get("original_file", {})
662
-
663
- if original_file:
664
- # New format
665
- print(
666
- f" Original file name: {original_file.get('name', 'Unknown')}"
667
- )
668
-
669
- # Show file size
670
- original_size = original_file.get("size", 0)
671
- if original_size:
672
- size_formatted = client.format_size(original_size)
673
- print(
674
- f" Original file size: {original_size:,} bytes ({size_formatted})"
675
- )
676
- else:
677
- print(f" Original file size: Unknown")
678
-
679
- # Show hash/CID of original file if available
680
- original_hash = original_file.get("hash", "")
681
- if original_hash:
682
- print(f" Original file hash: {original_hash}")
683
-
684
- # Show extension if available
685
- extension = original_file.get("extension", "")
686
- if extension:
687
- print(f" File extension: {extension}")
688
- else:
689
- # Try older format
690
- original_name = metadata_json.get(
691
- "original_name", "Unknown"
692
- )
693
- print(f" Original file name: {original_name}")
694
-
695
- original_size = metadata_json.get("original_size", 0)
696
- if original_size:
697
- size_formatted = client.format_size(original_size)
698
- print(
699
- f" Original file size: {original_size:,} bytes ({size_formatted})"
700
- )
701
- else:
702
- print(f" Original file size: Unknown")
703
-
704
- # Show erasure coding parameters if available
705
- ec_params = metadata_json.get("erasure_coding", {})
706
- if ec_params:
707
- k = ec_params.get("k", 0)
708
- m = ec_params.get("m", 0)
709
- if k and m:
710
- print(
711
- f" Erasure coding: k={k}, m={m} (need {k} of {k+m} parts)"
712
- )
713
- else:
714
- # Check old format
715
- k = metadata_json.get("k", 0)
716
- m = metadata_json.get("m", 0)
717
- if k and m:
718
- print(
719
- f" Erasure coding: k={k}, m={m} (need {k} of {k+m} parts)"
720
- )
721
-
722
- # Show encryption status if available
723
- encrypted = metadata_json.get("encrypted", False)
724
- print(f" Encrypted: {'Yes' if encrypted else 'No'}")
725
-
726
- # Count chunks
727
- chunks = metadata_json.get("chunks", [])
728
- if chunks:
729
- print(f" Total chunks: {len(chunks)}")
730
-
731
- # Show chunk details if requested
732
- if show_chunks:
733
- print(f" Chunks:")
734
- for j, chunk in enumerate(chunks):
735
- chunk_cid = (
736
- chunk
737
- if isinstance(chunk, str)
738
- else chunk.get("cid", "Unknown")
739
- )
740
- print(f" Chunk {j+1}: {chunk_cid}")
741
- else:
742
- # Couldn't parse metadata as text
743
- print(f" Error: Metadata is not in text format")
744
- except Exception as e:
745
- print(f" Error fetching metadata: {e}")
746
- else:
747
- print(f" Metadata CID: Unknown (None)")
748
-
749
- # Display file name (metadata file name)
750
- file_name = file.get("file_name", "Unnamed")
751
- print(
752
- f" Metadata file name: {file_name if file_name is not None else 'Unnamed'}"
753
- )
754
-
755
- # Show reconstruction command
756
- if metadata_cid is not None and metadata_cid != "Unknown":
757
- print(f" Reconstruction command:")
758
- # Try to extract original name from metadata file name
759
- original_name = (
760
- file_name.replace(".ec_metadata", "") if file_name else "file"
761
- )
762
- print(
763
- f" hippius reconstruct {formatted_cid} reconstructed_{original_name}"
764
- )
765
- else:
766
- print(f" Reconstruction command not available (missing CID)")
767
-
768
- print("-" * 80)
769
- except Exception as e:
770
- print(f" Error displaying EC file {i}: {e}")
771
- print("-" * 80)
772
- continue
773
-
774
- # Add helpful tips
775
- print("\nTo reconstruct a file, use:")
776
- print(f" hippius reconstruct <Metadata_CID> <output_filename>")
777
-
778
- except Exception as e:
779
- print(f"Error retrieving erasure-coded files: {e}")
780
- return 1
781
-
782
- return 0
783
-
784
-
785
- async def handle_erasure_code(
786
- client,
787
- file_path,
788
- k,
789
- m,
790
- chunk_size,
791
- miner_ids,
792
- encrypt=None,
793
- publish=True,
794
- verbose=True,
795
- ):
796
- """Handle the erasure-code command"""
797
- if not os.path.exists(file_path):
798
- print(f"Error: File {file_path} not found")
799
- return 1
800
-
801
- # Check if the input is a directory
802
- if os.path.isdir(file_path):
803
- print(f"Error: {file_path} is a directory, not a file.")
804
- print("\nErasure coding requires a single file as input. You have two options:")
805
- print("\n1. Archive the directory first:")
806
- print(f" zip -r {file_path}.zip {file_path}/")
807
- print(f" hippius erasure-code {file_path}.zip --k {k} --m {m}")
808
- print("\n2. Apply erasure coding to each file individually:")
809
- print(" # To code each file in the directory:")
810
-
811
- # Count the files to give the user an idea of how many files would be processed
812
- file_count = 0
813
- for root, _, files in os.walk(file_path):
814
- file_count += len(files)
815
-
816
- if file_count > 0:
817
- print(
818
- f"\n Found {file_count} files in the directory. Example command for individual files:"
819
- )
820
- # Show example for one file if available
821
- for root, _, files in os.walk(file_path):
822
- if files:
823
- example_file = os.path.join(root, files[0])
824
- print(f' hippius erasure-code "{example_file}" --k {k} --m {m}')
825
- break
826
-
827
- # Ask if user wants to automatically apply to all files
828
- print(
829
- "\nWould you like to automatically apply erasure coding to each file in the directory? (y/N)"
830
- )
831
- choice = input("> ").strip().lower()
832
-
833
- if choice in ("y", "yes"):
834
- return await handle_erasure_code_directory(
835
- client,
836
- file_path,
837
- k,
838
- m,
839
- chunk_size,
840
- miner_ids,
841
- encrypt,
842
- publish,
843
- verbose,
844
- )
845
- else:
846
- print(f" No files found in directory {file_path}")
847
-
848
- return 1
849
-
850
- # Check if zfec is installed
851
- try:
852
- import zfec
853
- except ImportError:
854
- print(
855
- "Error: zfec is required for erasure coding. Install it with: pip install zfec"
856
- )
857
- print("Then update your environment: poetry add zfec")
858
- return 1
859
-
860
- # Parse miner IDs if provided
861
- miner_id_list = None
862
- if miner_ids:
863
- miner_id_list = [m.strip() for m in miner_ids.split(",") if m.strip()]
864
- if verbose:
865
- print(f"Targeting {len(miner_id_list)} miners: {', '.join(miner_id_list)}")
866
-
867
- # Get the file size and adjust parameters if needed
868
- file_size = os.path.getsize(file_path)
869
- file_size_mb = file_size / (1024 * 1024)
870
-
871
- print(f"Processing {file_path} ({file_size_mb:.2f} MB) with erasure coding...")
872
-
873
- # Calculate how many chunks we would get with current settings
874
- potential_chunks = max(1, file_size // chunk_size)
875
-
876
- # If we can't get at least k chunks, adjust the chunk size
877
- if potential_chunks < k:
878
- # Calculate a new chunk size that would give us exactly k chunks
879
- new_chunk_size = max(1024, file_size // k) # Ensure at least 1KB chunks
880
-
881
- print("Warning: File is too small for the requested parameters.")
882
- print(
883
- f"Original parameters: k={k}, m={m}, chunk size={chunk_size/1024/1024:.2f} MB"
884
- )
885
- print(f"Would create only {potential_chunks} chunks, which is less than k={k}")
886
- print(
887
- f"Automatically adjusting chunk size to {new_chunk_size/1024/1024:.6f} MB to create at least {k} chunks"
888
- )
889
-
890
- chunk_size = new_chunk_size
891
-
892
- print(f"Final parameters: k={k}, m={m} (need {k} of {m} chunks to reconstruct)")
893
- print(f"Chunk size: {chunk_size/1024/1024:.6f} MB")
894
-
895
- if encrypt:
896
- print("Encryption: Enabled")
897
-
898
- start_time = time.time()
899
-
900
- try:
901
- # Use the store_erasure_coded_file method directly from HippiusClient
902
- result = await client.store_erasure_coded_file(
903
- file_path=file_path,
904
- k=k,
905
- m=m,
906
- chunk_size=chunk_size,
907
- encrypt=encrypt,
908
- miner_ids=miner_id_list,
909
- max_retries=3,
910
- verbose=verbose,
911
- )
912
-
913
- # Store the original result before potentially overwriting it with publish result
914
- storage_result = result.copy()
915
- metadata_cid = storage_result.get("metadata_cid", "unknown")
916
-
917
- # If publish flag is set, publish to the global IPFS network
918
- if publish:
919
- if metadata_cid != "unknown":
920
- print("\nPublishing to global IPFS network...")
921
- try:
922
- # Publish the metadata to the global IPFS network
923
- publish_result = await client.ipfs_client.publish_global(
924
- metadata_cid
925
- )
926
- if publish_result.get("published", False):
927
- print("Successfully published to global IPFS network")
928
- print(f"Access URL: https://ipfs.io/ipfs/{metadata_cid}")
929
- else:
930
- print(
931
- f"Warning: {publish_result.get('message', 'Failed to publish to global network')}"
932
- )
933
- except Exception as e:
934
- print(f"Warning: Failed to publish to global IPFS network: {e}")
935
-
936
- elapsed_time = time.time() - start_time
937
-
938
- print(f"\nErasure coding and storage completed in {elapsed_time:.2f} seconds!")
939
-
940
- # Display metadata
941
- metadata = storage_result.get("metadata", {})
942
- total_files_stored = storage_result.get("total_files_stored", 0)
943
-
944
- original_file = metadata.get("original_file", {})
945
- erasure_coding = metadata.get("erasure_coding", {})
946
-
947
- # If metadata_cid is known but metadata is empty, try to get file info from result directly
948
- if metadata_cid != "unknown" and not original_file:
949
- file_name = os.path.basename(file_path)
950
- file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
951
-
952
- # Use direct values from input parameters when metadata is not available
953
- print("\nErasure Coding Summary:")
954
- print(f" Original file: {file_name} ({file_size/1024/1024:.2f} MB)")
955
- print(f" Parameters: k={k}, m={m}")
956
- print(f" Total files stored in marketplace: {total_files_stored}")
957
- print(f" Metadata CID: {metadata_cid}")
958
-
959
- # Add publish status if applicable
960
- if publish:
961
- print(f" Published to global IPFS: Yes")
962
- print(f" Global access URL: https://ipfs.io/ipfs/{metadata_cid}")
963
- else:
964
- print("\nErasure Coding Summary:")
965
- print(
966
- f" Original file: {original_file.get('name')} ({original_file.get('size', 0)/1024/1024:.2f} MB)"
967
- )
968
- print(f" File ID: {erasure_coding.get('file_id')}")
969
- print(
970
- f" Parameters: k={erasure_coding.get('k')}, m={erasure_coding.get('m')}"
971
- )
972
- print(f" Total chunks: {len(metadata.get('chunks', []))}")
973
- print(f" Total files stored in marketplace: {total_files_stored}")
974
- print(f" Metadata CID: {metadata_cid}")
975
-
976
- # Add publish status if applicable
977
- if publish:
978
- print(f" Published to global IPFS: Yes")
979
- print(f" Global access URL: https://ipfs.io/ipfs/{metadata_cid}")
980
-
981
- # If we stored in the marketplace
982
- if "transaction_hash" in result:
983
- print(
984
- f"\nStored in marketplace. Transaction hash: {result['transaction_hash']}"
985
- )
986
-
987
- # Instructions for reconstruction
988
- print("\nTo reconstruct this file, you will need:")
989
- print(f" 1. The metadata CID: {metadata_cid}")
990
- print(" 2. Access to at least k chunks for each original chunk")
991
- print("\nReconstruction command:")
992
-
993
- # Get file name, either from metadata or directly from file path
994
- output_filename = original_file.get("name")
995
- if not output_filename:
996
- output_filename = os.path.basename(file_path)
997
-
998
- print(f" hippius reconstruct {metadata_cid} reconstructed_{output_filename}")
999
-
1000
- return 0
1001
-
1002
- except Exception as e:
1003
- print(f"Error during erasure coding: {e}")
1004
-
1005
- # Provide helpful advice based on the error
1006
- if "Wrong length" in str(e) and "input blocks" in str(e):
1007
- print("\nThis error typically occurs with very small files.")
1008
- print("Suggestions:")
1009
- print(" 1. Try using a smaller chunk size: --chunk-size 4096")
1010
- print(" 2. Try using a smaller k value: --k 2")
1011
- print(
1012
- " 3. For very small files, consider using regular storage instead of erasure coding."
1013
- )
1014
-
1015
- return 1
1016
-
1017
-
1018
- async def handle_erasure_code_directory(
1019
- client,
1020
- dir_path,
1021
- k,
1022
- m,
1023
- chunk_size,
1024
- miner_ids,
1025
- encrypt=None,
1026
- publish=False,
1027
- verbose=True,
1028
- ):
1029
- """Apply erasure coding to each file in a directory individually"""
1030
- if not os.path.isdir(dir_path):
1031
- print(f"Error: {dir_path} is not a directory")
1032
- return 1
1033
-
1034
- # Check if zfec is installed
1035
- try:
1036
- import zfec
1037
- except ImportError:
1038
- print(
1039
- "Error: zfec is required for erasure coding. Install it with: pip install zfec"
1040
- )
1041
- print("Then update your environment: poetry add zfec")
1042
- return 1
1043
-
1044
- print(f"Applying erasure coding to all files in {dir_path}")
1045
- print(f"Parameters: k={k}, m={m}, chunk_size={chunk_size/1024/1024:.2f} MB")
1046
- if encrypt:
1047
- print("Encryption: Enabled")
1048
-
1049
- # Parse miner IDs if provided
1050
- miner_id_list = None
1051
- if miner_ids:
1052
- miner_id_list = [m.strip() for m in miner_ids.split(",") if m.strip()]
1053
- if verbose:
1054
- print(f"Targeting {len(miner_id_list)} miners: {', '.join(miner_id_list)}")
1055
-
1056
- # Find all files
1057
- total_files = 0
1058
- successful = 0
1059
- failed = 0
1060
- skipped = 0
1061
-
1062
- # Collect files first
1063
- all_files = []
1064
- for root, _, files in os.walk(dir_path):
1065
- for filename in files:
1066
- file_path = os.path.join(root, filename)
1067
- all_files.append(file_path)
1068
-
1069
- total_files = len(all_files)
1070
- print(f"Found {total_files} files to process")
1071
-
1072
- if total_files == 0:
1073
- print("No files to process.")
1074
- return 0
1075
-
1076
- # Process each file
1077
- results = []
1078
-
1079
- for i, file_path in enumerate(all_files, 1):
1080
- print(f"\n[{i}/{total_files}] Processing: {file_path}")
1081
-
1082
- # Skip directories (shouldn't happen but just in case)
1083
- if os.path.isdir(file_path):
1084
- print(f"Skipping directory: {file_path}")
1085
- skipped += 1
1086
- continue
1087
-
1088
- # Get file size for information purposes
1089
- file_size = os.path.getsize(file_path)
1090
- file_size_mb = file_size / (1024 * 1024)
1091
- print(f"File size: {file_size_mb:.4f} MB ({file_size} bytes)")
1092
-
1093
- # Calculate adjusted chunk size for this file if needed
1094
- current_chunk_size = chunk_size
1095
- potential_chunks = max(1, file_size // current_chunk_size)
1096
-
1097
- if potential_chunks < k:
1098
- # Calculate a new chunk size that would give us exactly k chunks
1099
- # For very small files, use a minimal chunk size to ensure proper erasure coding
1100
- min_chunk_size = max(1, file_size // k) # Ensure at least 1 byte per chunk
1101
- print(f"Adjusting chunk size to {min_chunk_size} bytes for this file")
1102
- current_chunk_size = min_chunk_size
1103
-
1104
- try:
1105
- # Use the store_erasure_coded_file method directly from HippiusClient
1106
- result = await client.store_erasure_coded_file(
1107
- file_path=file_path,
1108
- k=k,
1109
- m=m,
1110
- chunk_size=current_chunk_size,
1111
- encrypt=encrypt,
1112
- miner_ids=miner_id_list,
1113
- max_retries=3,
1114
- verbose=False, # Less verbose for batch processing
1115
- )
1116
-
1117
- metadata_cid = result.get("metadata_cid", "unknown")
1118
- publishing_status = "Not published"
1119
-
1120
- # If publish flag is set, publish to the global IPFS network
1121
- if publish and metadata_cid != "unknown":
1122
- try:
1123
- # Publish the metadata to the global IPFS network
1124
- publish_result = await client.ipfs_client.publish_global(
1125
- metadata_cid
1126
- )
1127
- if publish_result.get("published", False):
1128
- publishing_status = "Published to global IPFS"
1129
- else:
1130
- publishing_status = f"Failed to publish: {publish_result.get('message', 'Unknown error')}"
1131
- except Exception as e:
1132
- publishing_status = f"Failed to publish: {str(e)}"
1133
-
1134
- # Store basic result info with additional publish info
1135
- results.append(
1136
- {
1137
- "file_path": file_path,
1138
- "metadata_cid": metadata_cid,
1139
- "success": True,
1140
- "published": publish
1141
- and publishing_status == "Published to global IPFS",
1142
- }
1143
- )
1144
-
1145
- status_msg = f"Success! Metadata CID: {metadata_cid}"
1146
- if publish:
1147
- status_msg += f" ({publishing_status})"
1148
- print(status_msg)
1149
- successful += 1
1150
-
1151
- except Exception as e:
1152
- print(f"Error coding file: {e}")
1153
-
1154
- # Provide specific guidance for very small files that fail
1155
- if file_size < 1024 and "Wrong length" in str(e):
1156
- print(
1157
- "This file may be too small for erasure coding with the current parameters."
1158
- )
1159
- print(
1160
- "Consider using smaller k and m values for very small files, e.g., --k 2 --m 3"
1161
- )
1162
-
1163
- results.append(
1164
- {
1165
- "file_path": file_path,
1166
- "error": str(e),
1167
- "success": False,
1168
- }
1169
- )
1170
- failed += 1
1171
-
1172
- # Print summary
1173
- print(f"\n=== Erasure Coding Directory Summary ===")
1174
- print(f"Total files processed: {total_files}")
1175
- print(f"Successfully coded: {successful}")
1176
- print(f"Failed: {failed}")
1177
- print(f"Skipped: {skipped}")
1178
-
1179
- if successful > 0:
1180
- print("\nSuccessfully coded files:")
1181
- for result in results:
1182
- if result.get("success"):
1183
- print(f" {result['file_path']} -> {result['metadata_cid']}")
1184
-
1185
- if failed > 0:
1186
- print("\nFailed files:")
1187
- for result in results:
1188
- if not result.get("success"):
1189
- print(
1190
- f" {result['file_path']}: {result.get('error', 'Unknown error')}"
1191
- )
1192
-
1193
- return 0 if failed == 0 else 1
1194
-
1195
-
1196
- async def handle_reconstruct(client, metadata_cid, output_file, verbose=True):
1197
- """Handle the reconstruct command for erasure-coded files"""
1198
- # Check if zfec is installed
1199
- try:
1200
- import zfec
1201
- except ImportError:
1202
- print(
1203
- "Error: zfec is required for erasure coding. Install it with: pip install zfec"
1204
- )
1205
- print("Then update your environment: poetry add zfec")
1206
- return 1
1207
-
1208
- print(f"Reconstructing file from metadata CID: {metadata_cid}")
1209
- print(f"Output file: {output_file}")
1210
-
1211
- start_time = time.time()
1212
-
1213
- try:
1214
- # Use the reconstruct_from_erasure_code method
1215
- await client.reconstruct_from_erasure_code(
1216
- metadata_cid=metadata_cid, output_file=output_file, verbose=verbose
1217
- )
1218
-
1219
- elapsed_time = time.time() - start_time
1220
- print(f"\nFile reconstruction completed in {elapsed_time:.2f} seconds!")
1221
-
1222
- return 0
1223
-
1224
- except Exception as e:
1225
- print(f"Error during file reconstruction: {e}")
1226
- return 1
1227
-
1228
-
1229
- def handle_config_get(section, key):
1230
- """Handle getting a configuration value"""
1231
- value = get_config_value(section, key)
1232
- print(f"Configuration value for {section}.{key}: {value}")
1233
- return 0
1234
-
1235
-
1236
- def handle_config_set(section, key, value):
1237
- """Handle setting a configuration value"""
1238
- # Try to parse JSON value for objects, arrays, and literals
1239
- try:
1240
- parsed_value = json.loads(value)
1241
- value = parsed_value
1242
- except (json.JSONDecodeError, TypeError):
1243
- # If not valid JSON, keep the raw string
1244
- pass
1245
-
1246
- result = set_config_value(section, key, value)
1247
- if result:
1248
- print(f"Successfully set {section}.{key} to {value}")
1249
- else:
1250
- print(f"Failed to set {section}.{key}")
1251
- return 1
1252
- return 0
1253
-
1254
-
1255
- def handle_config_list():
1256
- """Handle listing all configuration values"""
1257
- config = get_all_config()
1258
- print("Current Hippius SDK Configuration:")
1259
- print(json.dumps(config, indent=2))
1260
- print(f"\nConfiguration file: {os.path.expanduser('~/.hippius/config.json')}")
1261
- return 0
1262
-
1263
-
1264
- def handle_config_reset():
1265
- """Handle resetting configuration to default values"""
1266
- if reset_config():
1267
- print("Successfully reset configuration to default values")
1268
- else:
1269
- print("Failed to reset configuration")
1270
- return 1
1271
- return 0
1272
-
1273
-
1274
- def handle_seed_phrase_set(seed_phrase, encode=False, account_name=None):
1275
- """Handle setting the seed phrase"""
1276
- if encode:
1277
- try:
1278
- password = getpass.getpass("Enter password to encrypt seed phrase: ")
1279
- password_confirm = getpass.getpass("Confirm password: ")
1280
-
1281
- if password != password_confirm:
1282
- print("Error: Passwords do not match")
1283
- return 1
1284
-
1285
- result = set_seed_phrase(
1286
- seed_phrase, encode=True, password=password, account_name=account_name
1287
- )
1288
- except KeyboardInterrupt:
1289
- print("\nOperation cancelled")
1290
- return 1
1291
- else:
1292
- result = set_seed_phrase(seed_phrase, encode=False, account_name=account_name)
1293
-
1294
- if result:
1295
- account_msg = f" for account '{account_name}'" if account_name else ""
1296
-
1297
- if encode:
1298
- print(
1299
- f"Successfully set and encrypted the seed phrase{account_msg} with password protection"
1300
- )
1301
- else:
1302
- print(
1303
- f"Successfully set the seed phrase{account_msg} (WARNING: stored in plain text)"
1304
- )
1305
-
1306
- if account_name:
1307
- address = get_account_address(account_name)
1308
- if address:
1309
- print(f"SS58 Address: {address}")
1310
-
1311
- return 0
1312
- else:
1313
- print(f"Failed to set the seed phrase")
1314
- return 1
1315
-
1316
-
1317
- def handle_seed_phrase_encode(account_name=None):
1318
- """Handle encoding the existing seed phrase"""
1319
- # Get the current seed phrase
1320
- seed_phrase = get_seed_phrase(account_name=account_name)
1321
- if not seed_phrase:
1322
- if account_name:
1323
- print(f"Error: No seed phrase available for account '{account_name}'")
1324
- else:
1325
- print("Error: No seed phrase available to encode")
1326
- return 1
1327
-
1328
- # Check if it's already 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 is_encoded:
1339
- if account_name:
1340
- print(f"Seed phrase for account '{account_name}' is already encoded")
1341
- else:
1342
- print("Seed phrase is already encoded")
1343
- return 0
1344
-
1345
- # Get a password
1346
- try:
1347
- password = getpass.getpass("Enter password to encrypt seed phrase: ")
1348
- password_confirm = getpass.getpass("Confirm password: ")
1349
-
1350
- if password != password_confirm:
1351
- print("Error: Passwords do not match")
1352
- return 1
1353
-
1354
- # Encode the seed phrase
1355
- result = encrypt_seed_phrase(seed_phrase, password, account_name)
1356
- except KeyboardInterrupt:
1357
- print("\nOperation cancelled")
1358
- return 1
1359
-
1360
- if result:
1361
- account_msg = f" for account '{account_name}'" if account_name else ""
1362
- print(
1363
- f"Successfully encoded the seed phrase{account_msg} with password protection"
1364
- )
1365
- return 0
1366
- else:
1367
- print("Failed to encode the seed phrase")
1368
- return 1
1369
-
1370
-
1371
- def handle_seed_phrase_decode(account_name=None):
1372
- """Handle checking or decoding the seed phrase"""
1373
- # Check if the seed phrase is encoded
1374
- config = load_config()
1375
- is_encoded = False
1376
-
1377
- if account_name:
1378
- account_data = config["substrate"].get("accounts", {}).get(account_name, {})
1379
- is_encoded = account_data.get("seed_phrase_encoded", False)
1380
- else:
1381
- is_encoded = config["substrate"].get("seed_phrase_encoded", False)
1382
-
1383
- if not is_encoded:
1384
- if account_name:
1385
- print(
1386
- f"Seed phrase for account '{account_name}' is not encoded - nothing to decode"
1387
- )
1388
- else:
1389
- print("Seed phrase is not encoded - nothing to decode")
1390
- return 0
1391
-
1392
- # Get the decrypted seed phrase
1393
- try:
1394
- password = getpass.getpass("Enter password to decrypt seed phrase: ")
1395
- seed_phrase = decrypt_seed_phrase(password, account_name)
1396
-
1397
- if seed_phrase:
1398
- account_msg = f" for account '{account_name}'" if account_name else ""
1399
- print(f"Decrypted seed phrase{account_msg}: {seed_phrase}")
1400
-
1401
- # Ask if the user wants to save it as plain text
1402
- response = input(
1403
- "Do you want to save the seed phrase as plain text? (y/N): "
1404
- )
1405
- if response.lower() in ("y", "yes"):
1406
- result = set_seed_phrase(
1407
- seed_phrase, encode=False, account_name=account_name
1408
- )
1409
- if result:
1410
- print("Seed phrase saved as plain text")
1411
- else:
1412
- print("Failed to save the seed phrase as plain text")
1413
-
1414
- return 0
1415
- else:
1416
- print("Failed to decode the seed phrase. Incorrect password?")
1417
- return 1
1418
- except KeyboardInterrupt:
1419
- print("\nOperation cancelled")
1420
- return 1
1421
-
1422
-
1423
- def handle_seed_phrase_status(account_name=None):
1424
- """Handle showing the status of the seed phrase"""
1425
- # Check if we have a seed phrase
1426
- config = load_config()
1427
-
1428
- if account_name:
1429
- if account_name not in config["substrate"].get("accounts", {}):
1430
- print(f"Error: Account '{account_name}' not found")
1431
- return 1
1432
-
1433
- account_data = config["substrate"].get("accounts", {}).get(account_name, {})
1434
- seed_phrase_exists = account_data.get("seed_phrase") is not None
1435
- is_encoded = account_data.get("seed_phrase_encoded", False)
1436
- ss58_address = account_data.get("ss58_address")
1437
- else:
1438
- seed_phrase_exists = config["substrate"].get("seed_phrase") is not None
1439
- is_encoded = config["substrate"].get("seed_phrase_encoded", False)
1440
- ss58_address = config["substrate"].get("ss58_address")
1441
-
1442
- if not seed_phrase_exists:
1443
- if account_name:
1444
- print(f"No seed phrase is configured for account '{account_name}'")
1445
- else:
1446
- print("No seed phrase is configured")
1447
- return 0
1448
-
1449
- account_msg = f" for account '{account_name}'" if account_name else ""
1450
-
1451
- if is_encoded:
1452
- print(f"Seed phrase{account_msg} is stored with password-based encryption")
1453
-
1454
- # Offer to verify the password works
1455
- print("You can verify your password by decoding the seed phrase")
1456
- try:
1457
- verify = input("Would you like to verify your password works? (y/N): ")
1458
- if verify.lower() in ("y", "yes"):
1459
- password = getpass.getpass("Enter password to decrypt seed phrase: ")
1460
- seed_phrase = decrypt_seed_phrase(password, account_name)
1461
- if seed_phrase:
1462
- print("Password verification successful!")
1463
- else:
1464
- print("Password verification failed")
1465
- except KeyboardInterrupt:
1466
- print("\nOperation cancelled")
1467
- else:
1468
- print(f"Seed phrase{account_msg} is stored in plain text (not encrypted)")
1469
-
1470
- # Get the value
1471
- seed_phrase = get_seed_phrase(account_name=account_name)
1472
- if seed_phrase:
1473
- # Show only the first and last few words for security
1474
- words = seed_phrase.split()
1475
- if len(words) >= 6:
1476
- masked = " ".join(words[:2] + ["..."] + words[-2:])
1477
- print(f"Seed phrase (masked): {masked}")
1478
- else:
1479
- print("Seed phrase is available")
1480
-
1481
- if ss58_address:
1482
- print(f"SS58 Address: {ss58_address}")
1483
-
1484
- return 0
1485
-
1486
-
1487
- def handle_account_create(client, name, encrypt=False):
1488
- """Handle creating a new account with a generated seed phrase"""
1489
- print(f"Creating new account '{name}'...")
1490
-
1491
- # Get password if encryption is requested
1492
- password = None
1493
- if encrypt:
1494
- try:
1495
- password = getpass.getpass("Enter password to encrypt seed phrase: ")
1496
- password_confirm = getpass.getpass("Confirm password: ")
1497
-
1498
- if password != password_confirm:
1499
- print("Error: Passwords do not match")
1500
- return 1
1501
- except KeyboardInterrupt:
1502
- print("\nOperation cancelled")
1503
- return 1
1504
-
1505
- try:
1506
- # Create the account
1507
- result = client.substrate_client.create_account(
1508
- name, encode=encrypt, password=password
1509
- )
1510
-
1511
- print(f"Account created successfully!")
1512
- print(f"Name: {result['name']}")
1513
- print(f"Address: {result['address']}")
1514
- print(f"Seed phrase: {result['mnemonic']}")
1515
- print()
1516
- print(
1517
- "IMPORTANT: Please write down your seed phrase and store it in a safe place."
1518
- )
1519
- print(
1520
- "It is the only way to recover your account if you lose access to this configuration."
1521
- )
1522
-
1523
- return 0
1524
- except Exception as e:
1525
- print(f"Error creating account: {e}")
1526
- return 1
1527
-
1528
-
1529
- def handle_account_export(client, name=None, file_path=None):
1530
- """Handle exporting an account to a file"""
1531
- try:
1532
- # Export the account
1533
- exported_file = client.substrate_client.export_account(
1534
- account_name=name, file_path=file_path
1535
- )
1536
-
1537
- print(f"Account exported successfully to: {exported_file}")
1538
- print("The exported file contains your seed phrase in plain text.")
1539
- print("Please keep this file secure and do not share it with anyone.")
1540
-
1541
- return 0
1542
- except Exception as e:
1543
- print(f"Error exporting account: {e}")
1544
- return 1
1545
-
1546
-
1547
- def handle_account_import(client, file_path, encrypt=False):
1548
- """Handle importing an account from a file"""
1549
- # Get password if encryption is requested
1550
- password = None
1551
- if encrypt:
1552
- try:
1553
- password = getpass.getpass("Enter password to encrypt seed phrase: ")
1554
- password_confirm = getpass.getpass("Confirm password: ")
1555
-
1556
- if password != password_confirm:
1557
- print("Error: Passwords do not match")
1558
- return 1
1559
- except KeyboardInterrupt:
1560
- print("\nOperation cancelled")
1561
- return 1
1562
-
1563
- try:
1564
- # Import the account
1565
- result = client.substrate_client.import_account(
1566
- file_path, password=password if encrypt else None
1567
- )
1568
-
1569
- print(f"Account imported successfully!")
1570
- print(f"Name: {result['name']}")
1571
- print(f"Address: {result['address']}")
1572
-
1573
- if (
1574
- result.get("original_name")
1575
- and result.get("original_name") != result["name"]
1576
- ):
1577
- print(
1578
- f"Note: Original name '{result['original_name']}' was already in use, renamed to '{result['name']}'"
1579
- )
1580
-
1581
- return 0
1582
- except Exception as e:
1583
- print(f"Error importing account: {e}")
1584
- return 1
1585
-
1586
-
1587
- async def handle_account_info(client, account_name=None, include_history=False):
1588
- """Handle showing account information"""
1589
- try:
1590
- # Get account info - properly await the async method
1591
- info = await client.substrate_client.get_account_info(
1592
- account_name, include_history=include_history
1593
- )
1594
-
1595
- active_marker = " (active)" if info.get("is_active", False) else ""
1596
- encoded_status = (
1597
- "encrypted" if info.get("seed_phrase_encrypted", False) else "plain text"
1598
- )
1599
-
1600
- print(f"Account: {info['name']}{active_marker}")
1601
- print(f"Address: {info['address']}")
1602
- print(f"Seed phrase: {encoded_status}")
1603
-
1604
- # Show storage statistics if available
1605
- if "storage_stats" in info:
1606
- stats = info["storage_stats"]
1607
- if "error" in stats:
1608
- print(f"Storage stats: Error - {stats['error']}")
1609
- else:
1610
- print(f"Files stored: {stats['files']}")
1611
- print(f"Total storage: {stats['size_formatted']}")
1612
-
1613
- # Show balance if available
1614
- if "balance" in info:
1615
- balance = info["balance"]
1616
- print("\nAccount Balance:")
1617
- print(f" Free: {balance['free']:.6f}")
1618
- print(f" Reserved: {balance['reserved']:.6f}")
1619
- print(f" Total: {balance['total']:.6f}")
1620
-
1621
- # Show free credits if available
1622
- if "free_credits" in info:
1623
- print(f"Free credits: {info['free_credits']:.6f}")
1624
-
1625
- # Show file list if requested and available
1626
- if include_history and "files" in info and info["files"]:
1627
- print(f"\nStored Files ({len(info['files'])}):")
1628
- for i, file in enumerate(info["files"], 1):
1629
- print(f" {i}. {file.get('file_name', 'Unnamed')}")
1630
- print(f" CID: {file.get('file_hash', 'Unknown')}")
1631
- print(f" Size: {file.get('size_formatted', 'Unknown')}")
1632
-
1633
- return 0
1634
- except Exception as e:
1635
- print(f"Error retrieving account info: {e}")
1636
- return 1
1637
-
1638
-
1639
- async def handle_account_balance(client, account_name=None, watch=False, interval=5):
1640
- """Handle checking or watching account balance"""
1641
- try:
1642
- # Get the account address
1643
- if account_name:
1644
- address = get_account_address(account_name)
1645
- if not address:
1646
- print(f"Error: Could not find address for account '{account_name}'")
1647
- return 1
1648
- else:
1649
- if client.substrate_client._account_address:
1650
- address = client.substrate_client._account_address
1651
- else:
1652
- print("Error: No account address available")
1653
- return 1
1654
-
1655
- if watch:
1656
- # Watch mode - continuous updates
1657
- # Note: watch_account_balance may need to be modified to be async-compatible
1658
- await client.substrate_client.watch_account_balance(address, interval)
1659
- else:
1660
- # One-time check
1661
- balance = await client.substrate_client.get_account_balance(address)
1662
-
1663
- print(f"Account Balance for: {address}")
1664
- print(f"Free: {balance['free']:.6f}")
1665
- print(f"Reserved: {balance['reserved']:.6f}")
1666
- print(f"Frozen: {balance['frozen']:.6f}")
1667
- print(f"Total: {balance['total']:.6f}")
1668
-
1669
- # Show raw values
1670
- print("\nRaw Values:")
1671
- print(f"Free: {balance['raw']['free']:,}")
1672
- print(f"Reserved: {balance['raw']['reserved']:,}")
1673
- print(f"Frozen: {balance['raw']['frozen']:,}")
1674
-
1675
- return 0
1676
- except Exception as e:
1677
- print(f"Error checking account balance: {e}")
1678
- return 1
1679
-
1680
-
1681
- def handle_account_list():
1682
- """Handle listing all accounts"""
1683
- accounts = list_accounts()
1684
-
1685
- if not accounts:
1686
- print("No accounts configured")
1687
- return 0
1688
-
1689
- print(f"Found {len(accounts)} accounts:")
1690
-
1691
- for name, data in accounts.items():
1692
- active_marker = " (active)" if data.get("is_active", False) else ""
1693
- encoded_status = (
1694
- "encrypted" if data.get("seed_phrase_encoded", False) else "plain text"
1695
- )
1696
- address = data.get("ss58_address", "unknown")
1697
-
1698
- print(f" {name}{active_marker}:")
1699
- print(f" SS58 Address: {address}")
1700
- print(f" Seed phrase: {encoded_status}")
1701
- print()
1702
-
1703
- return 0
1704
-
1705
-
1706
- def handle_account_switch(account_name):
1707
- """Handle switching the active account"""
1708
- if set_active_account(account_name):
1709
- print(f"Switched to account '{account_name}'")
1710
-
1711
- # Show address
1712
- address = get_account_address(account_name)
1713
- if address:
1714
- print(f"SS58 Address: {address}")
1715
-
1716
- return 0
1717
- else:
1718
- return 1
1719
-
1720
-
1721
- def handle_account_delete(account_name):
1722
- """Handle deleting an account"""
1723
- # Ask for confirmation
1724
- confirm = input(
1725
- f"Are you sure you want to delete account '{account_name}'? This cannot be undone. (y/N): "
1726
- )
1727
- if confirm.lower() not in ("y", "yes"):
1728
- print("Operation cancelled")
1729
- return 0
1730
-
1731
- if delete_account(account_name):
1732
- print(f"Account '{account_name}' deleted")
1733
-
1734
- # Show the new active account if any
1735
- active_account = get_active_account()
1736
- if active_account:
1737
- print(f"Active account is now '{active_account}'")
1738
- else:
1739
- print("No accounts remaining")
1740
-
1741
- return 0
1742
- else:
1743
- return 1
1744
-
1745
-
1746
- def handle_default_address_set(address):
1747
- """Handle setting the default address for read-only operations"""
1748
- # Validate SS58 address format (basic check)
1749
- if not address.startswith("5"):
1750
- print(
1751
- f"Warning: '{address}' doesn't look like a valid SS58 address. SS58 addresses typically start with '5'."
1752
- )
1753
- confirm = input("Do you want to continue anyway? (y/N): ")
1754
- if confirm.lower() not in ("y", "yes"):
1755
- print("Operation cancelled")
1756
- return 1
1757
-
1758
- config = load_config()
1759
- config["substrate"]["default_address"] = address
1760
- save_config(config)
1761
-
1762
- print(f"Default address for read-only operations set to: {address}")
1763
- print(
1764
- "This address will be used for commands like 'files' and 'ec-files' when no address is explicitly provided."
1765
- )
1766
- return 0
1767
-
1768
-
1769
- def handle_default_address_get():
1770
- """Handle getting the current default address for read-only operations"""
1771
- config = load_config()
1772
- address = config["substrate"].get("default_address")
1773
-
1774
- if address:
1775
- print(f"Current default address for read-only operations: {address}")
1776
- else:
1777
- print("No default address set for read-only operations")
1778
- print("You can set one with: hippius address set-default <ss58_address>")
1779
-
1780
- return 0
1781
-
1782
-
1783
- def handle_default_address_clear():
1784
- """Handle clearing the default address for read-only operations"""
1785
- config = load_config()
1786
- if "default_address" in config["substrate"]:
1787
- del config["substrate"]["default_address"]
1788
- save_config(config)
1789
- print("Default address for read-only operations has been cleared")
1790
- else:
1791
- print("No default address was set")
1792
-
1793
- return 0
1794
-
1795
-
1796
- async def handle_pinning_status(
1797
- client, account_address, verbose=False, show_contents=True
1798
- ):
1799
- """Handle the pinning-status command"""
1800
- print("Checking file pinning status...")
1801
- try:
1802
- # Get the account address we're querying
1803
- if account_address is None:
1804
- # If no address provided, first try to get from keypair (if available)
1805
- if (
1806
- hasattr(client.substrate_client, "_keypair")
1807
- and client.substrate_client._keypair is not None
1808
- ):
1809
- account_address = client.substrate_client._keypair.ss58_address
1810
- else:
1811
- # Try to get the default address
1812
- default_address = get_default_address()
1813
- if default_address:
1814
- account_address = default_address
1815
- else:
1816
- has_default = get_default_address() is not None
1817
- print(
1818
- "Error: No account address provided, and client has no keypair."
1819
- )
1820
- if has_default:
1821
- print(
1822
- "Please provide an account address with '--account_address' or the default address may be invalid."
1823
- )
1824
- else:
1825
- print(
1826
- "Please provide an account address with '--account_address' or set a default with:"
1827
- )
1828
- print(" hippius address set-default <your_account_address>")
1829
- return 1
1830
-
1831
- storage_requests = client.substrate_client.get_pinning_status(account_address)
1832
-
1833
- # Check if any storage requests were found
1834
- if not storage_requests:
1835
- print(f"No pinning requests found for account: {account_address}")
1836
- return 0
1837
-
1838
- print(
1839
- f"\nFound {len(storage_requests)} pinning requests for account: {account_address}"
1840
- )
1841
- print("-" * 80)
1842
-
1843
- # Format and display each storage request
1844
- for i, request in enumerate(storage_requests, 1):
1845
- try:
1846
- print(f"Request {i}:")
1847
-
1848
- # Display CID if available
1849
- cid = None
1850
- if "cid" in request:
1851
- cid = request.get("cid", "Unknown")
1852
- print(f" CID: {cid}")
1853
-
1854
- # Display file name if available
1855
- if "file_name" in request:
1856
- file_name = request.get("file_name", "Unknown")
1857
- print(f" File name: {file_name}")
1858
- elif "raw_value" in request and "file_name" in request["raw_value"]:
1859
- # Try to extract from raw value if it's available
1860
- try:
1861
- raw_value = request["raw_value"]
1862
- if isinstance(raw_value, str) and "{" in raw_value:
1863
- # It's a string representation of a dict, try to extract the file_name
1864
- if "'file_name': " in raw_value:
1865
- start_idx = raw_value.find("'file_name': '") + len(
1866
- "'file_name': '"
1867
- )
1868
- end_idx = raw_value.find("'", start_idx)
1869
- if start_idx > 0 and end_idx > start_idx:
1870
- file_name = raw_value[start_idx:end_idx]
1871
- print(f" File name: {file_name}")
1872
- except Exception:
1873
- pass
1874
-
1875
- # Display total replicas if available
1876
- if "total_replicas" in request:
1877
- total_replicas = request.get("total_replicas", 0)
1878
- print(f" Total replicas: {total_replicas}")
1879
-
1880
- # Display owner if available
1881
- if "owner" in request:
1882
- owner = request.get("owner", "Unknown")
1883
- print(f" Owner: {owner}")
1884
-
1885
- # Display timestamps if available
1886
- if "created_at" in request:
1887
- created_at = request.get("created_at", 0)
1888
- if created_at > 0:
1889
- print(f" Created at block: {created_at}")
1890
-
1891
- if "last_charged_at" in request:
1892
- last_charged_at = request.get("last_charged_at", 0)
1893
- if last_charged_at > 0:
1894
- print(f" Last charged at block: {last_charged_at}")
1895
-
1896
- # Display assignment status and progress info
1897
- status_text = "Awaiting validator"
1898
- if "is_assigned" in request:
1899
- is_assigned = request.get("is_assigned", False)
1900
- if is_assigned:
1901
- status_text = "Assigned to miners"
1902
-
1903
- # Enhanced status info
1904
- if "miner_ids" in request and "total_replicas" in request:
1905
- miner_ids = request.get("miner_ids", [])
1906
- total_replicas = request.get("total_replicas", 0)
1907
-
1908
- if len(miner_ids) > 0:
1909
- if len(miner_ids) == total_replicas:
1910
- status_text = "Fully pinned"
1911
- else:
1912
- status_text = "Partially pinned"
1913
-
1914
- print(f" Status: {status_text}")
1915
-
1916
- # Display validator if available
1917
- if "selected_validator" in request:
1918
- validator = request.get("selected_validator", "")
1919
- if validator:
1920
- print(f" Selected validator: {validator}")
1921
-
1922
- # Display miners if available
1923
- if "miner_ids" in request:
1924
- miner_ids = request.get("miner_ids", [])
1925
- if miner_ids:
1926
- print(f" Assigned miners: {len(miner_ids)}")
1927
- for miner in miner_ids[:3]: # Show first 3 miners
1928
- print(f" - {miner}")
1929
- if len(miner_ids) > 3:
1930
- print(f" ... and {len(miner_ids) - 3} more")
1931
- else:
1932
- print(f" Assigned miners: None")
1933
-
1934
- # Calculate pinning percentage if we have total_replicas
1935
- if "total_replicas" in request and request["total_replicas"] > 0:
1936
- total_replicas = request["total_replicas"]
1937
- pinning_pct = (len(miner_ids) / total_replicas) * 100
1938
- print(
1939
- f" Pinning progress: {pinning_pct:.1f}% ({len(miner_ids)}/{total_replicas} miners)"
1940
- )
1941
-
1942
- # Display raw data for debugging
1943
- if verbose:
1944
- print(" Raw data:")
1945
- if "raw_key" in request:
1946
- print(f" Key: {request['raw_key']}")
1947
- if "raw_value" in request:
1948
- print(f" Value: {request['raw_value']}")
1949
-
1950
- # Try to fetch the content and determine if it's a file list by inspecting its contents
1951
- if show_contents and cid:
1952
- try:
1953
- print("\n Fetching contents from IPFS...")
1954
- # Fetch the contents from IPFS
1955
- file_data = await client.ipfs_client.cat(cid)
1956
-
1957
- if file_data and file_data.get("is_text", False):
1958
- try:
1959
- # Try to parse as JSON
1960
- content_json = json.loads(
1961
- file_data.get("content", "{}")
1962
- )
1963
-
1964
- # Detect if this is a file list by checking if it's a list of file objects
1965
- is_file_list = False
1966
- if (
1967
- isinstance(content_json, list)
1968
- and len(content_json) > 0
1969
- ):
1970
- # Check if it looks like a file list
1971
- sample_item = content_json[0]
1972
- if isinstance(sample_item, dict) and (
1973
- "cid" in sample_item
1974
- or "fileHash" in sample_item
1975
- or "filename" in sample_item
1976
- or "fileName" in sample_item
1977
- ):
1978
- is_file_list = True
1979
-
1980
- if is_file_list:
1981
- # It's a file list - display the files
1982
- print(
1983
- f" Content is a file list with {len(content_json)} files:"
1984
- )
1985
- print(" " + "-" * 40)
1986
- for j, file_info in enumerate(content_json, 1):
1987
- filename = file_info.get(
1988
- "filename"
1989
- ) or file_info.get("fileName", "Unknown")
1990
- file_cid = file_info.get(
1991
- "cid"
1992
- ) or file_info.get("fileHash", "Unknown")
1993
- print(f" File {j}: {filename}")
1994
- print(f" CID: {file_cid}")
1995
-
1996
- # Show size if available
1997
- if "size" in file_info:
1998
- size = file_info["size"]
1999
- size_formatted = (
2000
- client.format_size(size)
2001
- if hasattr(client, "format_size")
2002
- else f"{size} bytes"
2003
- )
2004
- print(f" Size: {size_formatted}")
2005
-
2006
- print(" " + "-" * 40)
2007
- else:
2008
- # Not a file list, show a compact summary
2009
- content_type = type(content_json).__name__
2010
- preview = str(content_json)
2011
- if len(preview) > 100:
2012
- preview = preview[:100] + "..."
2013
- print(f" Content type: JSON {content_type}")
2014
- print(f" Content preview: {preview}")
2015
- except json.JSONDecodeError:
2016
- # Not JSON, just show text preview
2017
- content = file_data.get("content", "")
2018
- preview = (
2019
- content[:100] + "..."
2020
- if len(content) > 100
2021
- else content
2022
- )
2023
- print(f" Content type: Text")
2024
- print(f" Content preview: {preview}")
2025
- else:
2026
- # Binary data
2027
- content_size = len(file_data.get("content", b""))
2028
- size_formatted = (
2029
- client.format_size(content_size)
2030
- if hasattr(client, "format_size")
2031
- else f"{content_size} bytes"
2032
- )
2033
- print(f" Content type: Binary data")
2034
- print(f" Content size: {size_formatted}")
2035
- except Exception as e:
2036
- print(f" Error fetching file list contents: {e}")
2037
-
2038
- print("-" * 80)
2039
- except Exception as e:
2040
- print(f" Error displaying request {i}: {e}")
2041
- print("-" * 80)
2042
- continue
2043
-
2044
- except Exception as e:
2045
- print(f"Error retrieving pinning status: {e}")
2046
- return 1
2047
-
2048
- return 0
2049
-
2050
-
2051
- def main():
2052
- """Main CLI entry point for hippius command."""
2053
- # Set up the argument parser
2054
- parser = argparse.ArgumentParser(
2055
- description="Hippius SDK Command Line Interface",
2056
- formatter_class=argparse.RawDescriptionHelpFormatter,
2057
- epilog="""
2058
- examples:
2059
- # Store a file
2060
- hippius store example.txt
2061
-
2062
- # Store a directory
2063
- hippius store-dir ./my_directory
2064
-
2065
- # Download a file
2066
- hippius download QmHash output.txt
2067
-
2068
- # Check if a CID exists
2069
- hippius exists QmHash
2070
-
2071
- # View the content of a CID
2072
- hippius cat QmHash
2073
-
2074
- # View your available credits
2075
- hippius credits
2076
-
2077
- # View your stored files
2078
- hippius files
2079
-
2080
- # View all miners for stored files
2081
- hippius files --all-miners
2082
-
2083
- # Check file pinning status
2084
- hippius pinning-status
2085
-
2086
- # Erasure code a file (Reed-Solomon)
2087
- hippius erasure-code large_file.mp4 --k 3 --m 5
2088
-
2089
- # Erasure code without publishing to global IPFS network
2090
- hippius erasure-code large_file.avi --no-publish
2091
-
2092
- # Reconstruct an erasure-coded file
2093
- hippius reconstruct QmMetadataHash reconstructed_file.mp4
2094
- """,
2095
- )
2096
-
2097
- # Optional arguments for all commands
2098
- parser.add_argument(
2099
- "--gateway",
2100
- default=get_config_value("ipfs", "gateway", "https://ipfs.io"),
2101
- help="IPFS gateway URL for downloads (default: from config or https://ipfs.io)",
2102
- )
2103
- parser.add_argument(
2104
- "--api-url",
2105
- default=get_config_value("ipfs", "api_url", "https://store.hippius.network"),
2106
- help="IPFS API URL for uploads (default: from config or https://store.hippius.network)",
2107
- )
2108
- parser.add_argument(
2109
- "--local-ipfs",
2110
- action="store_true",
2111
- default=get_config_value("ipfs", "local_ipfs", False),
2112
- help="Use local IPFS node (http://localhost:5001) instead of remote API",
2113
- )
2114
- parser.add_argument(
2115
- "--substrate-url",
2116
- default=get_config_value("substrate", "url", "wss://rpc.hippius.network"),
2117
- help="Substrate node WebSocket URL (default: from config or wss://rpc.hippius.network)",
2118
- )
2119
- parser.add_argument(
2120
- "--miner-ids",
2121
- help="Comma-separated list of miner IDs for storage (default: from config)",
2122
- )
2123
- parser.add_argument(
2124
- "--verbose",
2125
- "-v",
2126
- action="store_true",
2127
- default=get_config_value("cli", "verbose", False),
2128
- help="Enable verbose debug output",
2129
- )
2130
- parser.add_argument(
2131
- "--encrypt",
2132
- action="store_true",
2133
- help="Encrypt files when uploading (overrides default)",
2134
- )
2135
- parser.add_argument(
2136
- "--no-encrypt",
2137
- action="store_true",
2138
- help="Do not encrypt files when uploading (overrides default)",
2139
- )
2140
- parser.add_argument(
2141
- "--decrypt",
2142
- action="store_true",
2143
- help="Decrypt files when downloading (overrides default)",
2144
- )
2145
- parser.add_argument(
2146
- "--no-decrypt",
2147
- action="store_true",
2148
- help="Do not decrypt files when downloading (overrides default)",
2149
- )
2150
- parser.add_argument(
2151
- "--encryption-key",
2152
- help="Base64-encoded encryption key (overrides HIPPIUS_ENCRYPTION_KEY in .env)",
2153
- )
2154
- parser.add_argument(
2155
- "--password",
2156
- help="Password to decrypt the seed phrase if needed (will prompt if required and not provided)",
2157
- )
2158
- parser.add_argument(
2159
- "--account",
2160
- help="Account name to use (uses active account if not specified)",
2161
- )
2162
-
2163
- # Subcommands
2164
- subparsers = parser.add_subparsers(dest="command", help="Commands")
2165
-
2166
- # Download command
2167
- download_parser = subparsers.add_parser(
2168
- "download", help="Download a file from IPFS"
2169
- )
2170
- download_parser.add_argument("cid", help="CID of file to download")
2171
- download_parser.add_argument("output_path", help="Path to save downloaded file")
2172
-
2173
- # Exists command
2174
- exists_parser = subparsers.add_parser(
2175
- "exists", help="Check if a CID exists on IPFS"
2176
- )
2177
- exists_parser.add_argument("cid", help="CID to check")
2178
-
2179
- # Cat command
2180
- cat_parser = subparsers.add_parser(
2181
- "cat", help="Display content of a file from IPFS"
2182
- )
2183
- cat_parser.add_argument("cid", help="CID of file to display")
2184
- cat_parser.add_argument(
2185
- "--max-size",
2186
- type=int,
2187
- default=1024,
2188
- help="Maximum number of bytes to display (default: 1024)",
2189
- )
2190
-
2191
- # Store command (upload to IPFS then store on Substrate)
2192
- store_parser = subparsers.add_parser(
2193
- "store", help="Upload a file to IPFS and store it on Substrate"
2194
- )
2195
- store_parser.add_argument("file_path", help="Path to file to upload")
2196
-
2197
- # Store directory command
2198
- store_dir_parser = subparsers.add_parser(
2199
- "store-dir", help="Upload a directory to IPFS and store all files on Substrate"
2200
- )
2201
- store_dir_parser.add_argument("dir_path", help="Path to directory to upload")
2202
-
2203
- # Credits command
2204
- credits_parser = subparsers.add_parser(
2205
- "credits", help="Check free credits for an account in the marketplace"
2206
- )
2207
- credits_parser.add_argument(
2208
- "account_address",
2209
- nargs="?",
2210
- default=None,
2211
- help="Substrate account address (uses keypair address if not specified)",
2212
- )
2213
-
2214
- # Files command
2215
- files_parser = subparsers.add_parser(
2216
- "files", help="View files stored by you or another account"
2217
- )
2218
- files_parser.add_argument(
2219
- "--account_address",
2220
- help="Substrate account to view files for (defaults to your keyfile account)",
2221
- )
2222
- files_parser.add_argument(
2223
- "--all-miners",
2224
- action="store_true",
2225
- help="Show all miners for each file",
2226
- )
2227
- files_parser.set_defaults(
2228
- func=lambda args, client: handle_files(
2229
- client,
2230
- args.account_address,
2231
- show_all_miners=args.all_miners if hasattr(args, "all_miners") else False,
2232
- )
2233
- )
2234
-
2235
- # Pinning status command
2236
- pinning_status_parser = subparsers.add_parser(
2237
- "pinning-status", help="Check the status of file pinning requests"
2238
- )
2239
- pinning_status_parser.add_argument(
2240
- "--account_address",
2241
- help="Substrate account to check pinning status for (defaults to your keyfile account)",
2242
- )
2243
- pinning_status_parser.add_argument(
2244
- "--verbose",
2245
- "-v",
2246
- action="store_true",
2247
- help="Show detailed debug information",
2248
- )
2249
- pinning_status_parser.add_argument(
2250
- "--show-contents",
2251
- action="store_true",
2252
- default=True,
2253
- help="Show the contents of file lists (defaults to true)",
2254
- )
2255
- pinning_status_parser.add_argument(
2256
- "--no-contents",
2257
- action="store_true",
2258
- help="Don't show the contents of file lists",
2259
- )
2260
-
2261
- # Erasure Coded Files command
2262
- ec_files_parser = subparsers.add_parser(
2263
- "ec-files", help="View erasure-coded files stored by you or another account"
2264
- )
2265
- ec_files_parser.add_argument(
2266
- "--account_address",
2267
- help="Substrate account to view erasure-coded files for (defaults to your keyfile account)",
2268
- )
2269
- ec_files_parser.add_argument(
2270
- "--all-miners",
2271
- action="store_true",
2272
- help="Show all miners for each file",
2273
- )
2274
- ec_files_parser.add_argument(
2275
- "--show-chunks",
2276
- action="store_true",
2277
- help="Show chunk details for each erasure-coded file",
2278
- )
2279
- ec_files_parser.set_defaults(
2280
- func=lambda args, client: handle_ec_files(
2281
- client,
2282
- args.account_address,
2283
- show_all_miners=args.all_miners if hasattr(args, "all_miners") else False,
2284
- show_chunks=args.show_chunks if hasattr(args, "show_chunks") else False,
2285
- )
2286
- )
2287
-
2288
- # Key generation command
2289
- keygen_parser = subparsers.add_parser(
2290
- "keygen", help="Generate an encryption key for secure file storage"
2291
- )
2292
- keygen_parser.add_argument(
2293
- "--copy", action="store_true", help="Copy the generated key to the clipboard"
2294
- )
2295
- keygen_parser.add_argument(
2296
- "--save", action="store_true", help="Save the key to the Hippius configuration"
2297
- )
2298
-
2299
- # Erasure code command
2300
- erasure_code_parser = subparsers.add_parser(
2301
- "erasure-code", help="Erasure code a file"
2302
- )
2303
- erasure_code_parser.add_argument("file_path", help="Path to file to erasure code")
2304
- erasure_code_parser.add_argument(
2305
- "--k",
2306
- type=int,
2307
- default=3,
2308
- help="Number of data chunks needed to reconstruct (default: 3)",
2309
- )
2310
- erasure_code_parser.add_argument(
2311
- "--m", type=int, default=5, help="Total number of chunks to create (default: 5)"
2312
- )
2313
- erasure_code_parser.add_argument(
2314
- "--chunk-size",
2315
- type=int,
2316
- default=1048576,
2317
- help="Chunk size in bytes (default: 1MB)",
2318
- )
2319
- erasure_code_parser.add_argument(
2320
- "--miner-ids", help="Comma-separated list of miner IDs"
2321
- )
2322
- erasure_code_parser.add_argument(
2323
- "--encrypt", action="store_true", help="Encrypt the file"
2324
- )
2325
- erasure_code_parser.add_argument(
2326
- "--no-encrypt", action="store_true", help="Do not encrypt the file"
2327
- )
2328
- erasure_code_parser.add_argument(
2329
- "--no-publish",
2330
- action="store_true",
2331
- help="Do not upload and publish the erasure-coded file to the global IPFS network",
2332
- )
2333
- erasure_code_parser.add_argument(
2334
- "--verbose", action="store_true", help="Enable verbose output", default=True
2335
- )
2336
-
2337
- # Reconstruct command
2338
- reconstruct_parser = subparsers.add_parser(
2339
- "reconstruct", help="Reconstruct an erasure-coded file"
2340
- )
2341
- reconstruct_parser.add_argument(
2342
- "metadata_cid", help="Metadata CID of the erasure-coded file"
2343
- )
2344
- reconstruct_parser.add_argument(
2345
- "output_file", help="Path to save reconstructed file"
2346
- )
2347
- reconstruct_parser.add_argument(
2348
- "--verbose", action="store_true", help="Enable verbose output", default=True
2349
- )
2350
-
2351
- # Configuration subcommand
2352
- config_parser = subparsers.add_parser(
2353
- "config", help="Manage Hippius SDK configuration"
2354
- )
2355
- config_subparsers = config_parser.add_subparsers(
2356
- dest="config_action", help="Configuration action"
2357
- )
2358
-
2359
- # Get configuration value
2360
- get_parser = config_subparsers.add_parser("get", help="Get a configuration value")
2361
- get_parser.add_argument(
2362
- "section",
2363
- help="Configuration section (ipfs, substrate, encryption, erasure_coding, cli)",
2364
- )
2365
- get_parser.add_argument("key", help="Configuration key")
2366
-
2367
- # Set configuration value
2368
- set_parser = config_subparsers.add_parser("set", help="Set a configuration value")
2369
- set_parser.add_argument(
2370
- "section",
2371
- help="Configuration section (ipfs, substrate, encryption, erasure_coding, cli)",
2372
- )
2373
- set_parser.add_argument("key", help="Configuration key")
2374
- set_parser.add_argument("value", help="Value to set (use JSON for complex values)")
2375
-
2376
- # List all configuration values
2377
- config_subparsers.add_parser("list", help="List all configuration values")
2378
-
2379
- # Reset configuration to defaults
2380
- config_subparsers.add_parser("reset", help="Reset configuration to default values")
2381
-
2382
- # Import config from .env
2383
- config_subparsers.add_parser(
2384
- "import-env", help="Import configuration from .env file"
2385
- )
2386
-
2387
- # Seed Phrase subcommand
2388
- seed_parser = subparsers.add_parser("seed", help="Manage substrate seed phrase")
2389
- seed_subparsers = seed_parser.add_subparsers(
2390
- dest="seed_action", help="Seed phrase action"
2391
- )
2392
-
2393
- # Set seed phrase
2394
- set_seed_parser = seed_subparsers.add_parser(
2395
- "set", help="Set the substrate seed phrase"
2396
- )
2397
- set_seed_parser.add_argument(
2398
- "seed_phrase", help="The mnemonic seed phrase (e.g., 'word1 word2 word3...')"
2399
- )
2400
- set_seed_parser.add_argument(
2401
- "--encode", action="store_true", help="Encrypt the seed phrase with a password"
2402
- )
2403
- set_seed_parser.add_argument(
2404
- "--account", help="Account name to associate with this seed phrase"
2405
- )
2406
-
2407
- # Encode existing seed phrase
2408
- encode_seed_parser = seed_subparsers.add_parser(
2409
- "encode", help="Encrypt the existing seed phrase"
2410
- )
2411
- encode_seed_parser.add_argument(
2412
- "--account", help="Account name to encode the seed phrase for"
2413
- )
2414
-
2415
- # Decode seed phrase
2416
- decode_seed_parser = seed_subparsers.add_parser(
2417
- "decode", help="Temporarily decrypt and display the seed phrase"
2418
- )
2419
- decode_seed_parser.add_argument(
2420
- "--account", help="Account name to decode the seed phrase for"
2421
- )
17
+ from hippius_sdk import cli_handlers, initialize_from_env
18
+ from hippius_sdk.cli_assets import HERO_TITLE
19
+ from hippius_sdk.cli_parser import create_parser, get_subparser, parse_arguments
20
+ from hippius_sdk.cli_rich import console, error
21
+ from hippius_sdk.utils import generate_key
2422
22
 
2423
- # Check seed phrase status
2424
- status_seed_parser = seed_subparsers.add_parser(
2425
- "status", help="Check the status of the configured seed phrase"
2426
- )
2427
- status_seed_parser.add_argument(
2428
- "--account", help="Account name to check the status for"
2429
- )
23
+ # Import SDK components
2430
24
 
2431
- # Account subcommand
2432
- account_parser = subparsers.add_parser("account", help="Manage substrate accounts")
2433
- account_subparsers = account_parser.add_subparsers(
2434
- dest="account_action", help="Account action"
2435
- )
25
+ load_dotenv()
26
+ initialize_from_env()
2436
27
 
2437
- # List accounts
2438
- account_subparsers.add_parser("list", help="List all accounts")
2439
28
 
2440
- # Create account
2441
- create_account_parser = account_subparsers.add_parser(
2442
- "create", help="Create a new account with a generated seed phrase"
2443
- )
2444
- create_account_parser.add_argument(
2445
- "--name", required=True, help="Name for the new account"
2446
- )
2447
- create_account_parser.add_argument(
2448
- "--encrypt", action="store_true", help="Encrypt the seed phrase with a password"
2449
- )
29
+ def generate_encryption_key(copy_to_clipboard=False):
30
+ """Generate an encryption key and display it to the user."""
31
+ # Generate the key
32
+ encoded_key = generate_key()
2450
33
 
2451
- # Export account
2452
- export_account_parser = account_subparsers.add_parser(
2453
- "export", help="Export an account to a file"
2454
- )
2455
- export_account_parser.add_argument(
2456
- "--name",
2457
- help="Name of the account to export (uses active account if not specified)",
2458
- )
2459
- export_account_parser.add_argument(
2460
- "--file",
2461
- help="Path to save the exported account file (auto-generated if not specified)",
2462
- )
34
+ # Copy to clipboard if requested
35
+ if copy_to_clipboard:
36
+ try:
37
+ import pyperclip
2463
38
 
2464
- # Import account
2465
- import_account_parser = account_subparsers.add_parser(
2466
- "import", help="Import an account from a file"
2467
- )
2468
- import_account_parser.add_argument(
2469
- "--file", required=True, help="Path to the account file to import"
2470
- )
2471
- import_account_parser.add_argument(
2472
- "--encrypt",
2473
- action="store_true",
2474
- help="Encrypt the imported seed phrase with a password",
2475
- )
39
+ pyperclip.copy(encoded_key)
40
+ console.print("[green]Key copied to clipboard![/green]")
41
+ except ImportError:
42
+ console.print(
43
+ "[yellow]Warning:[/yellow] Could not copy to clipboard. Install pyperclip with: [bold]pip install pyperclip[/bold]"
44
+ )
2476
45
 
2477
- # Account info
2478
- info_account_parser = account_subparsers.add_parser(
2479
- "info", help="Display detailed information about an account"
2480
- )
2481
- info_account_parser.add_argument(
2482
- "account_name",
2483
- nargs="?",
2484
- help="Name of the account to show (uses active account if not specified)",
2485
- )
2486
- info_account_parser.add_argument(
2487
- "--history", action="store_true", help="Include usage history in the output"
2488
- )
46
+ return encoded_key
2489
47
 
2490
- # Account balance
2491
- balance_account_parser = account_subparsers.add_parser(
2492
- "balance", help="Check account balance"
2493
- )
2494
- balance_account_parser.add_argument(
2495
- "account_name",
2496
- nargs="?",
2497
- help="Name of the account to check (uses active account if not specified)",
2498
- )
2499
- balance_account_parser.add_argument(
2500
- "--watch",
2501
- action="store_true",
2502
- help="Watch account balance in real-time until Ctrl+C is pressed",
2503
- )
2504
- balance_account_parser.add_argument(
2505
- "--interval",
2506
- type=int,
2507
- default=5,
2508
- help="Update interval in seconds for watch mode (default: 5)",
2509
- )
2510
48
 
2511
- # Switch active account
2512
- switch_account_parser = account_subparsers.add_parser(
2513
- "switch", help="Switch to a different account"
2514
- )
2515
- switch_account_parser.add_argument(
2516
- "account_name", help="Name of the account to switch to"
2517
- )
49
+ def key_generation_cli():
50
+ """Standalone CLI tool for encryption key generation with Rich formatting."""
51
+ # Display the Hippius logo banner with Rich formatting
52
+ console.print(HERO_TITLE, style="bold cyan")
53
+ console.print("[bold]Encryption Key Generator[/bold]", style="blue")
2518
54
 
2519
- # Delete account
2520
- delete_account_parser = account_subparsers.add_parser(
2521
- "delete", help="Delete an account"
2522
- )
2523
- delete_account_parser.add_argument(
2524
- "account_name", help="Name of the account to delete"
2525
- )
55
+ try:
56
+ # Generate the key
57
+ encoded_key = generate_encryption_key(copy_to_clipboard=True)
58
+
59
+ # Display the key with Rich formatting
60
+ console.print("\n[bold green]Your encryption key:[/bold green]")
61
+ console.print(f"[yellow]{encoded_key}[/yellow]")
62
+ console.print("\n[dim]This key has been copied to your clipboard.[/dim]")
63
+ console.print("[bold blue]Usage instructions:[/bold blue]")
64
+ console.print("1. Store this key securely")
65
+ console.print("2. Use it to encrypt/decrypt files with the Hippius SDK")
66
+ console.print("3. [yellow]Never share this key with others[/yellow]")
2526
67
 
2527
- # Address subcommand for read-only operations
2528
- address_parser = subparsers.add_parser(
2529
- "address", help="Manage default address for read-only operations"
2530
- )
2531
- address_subparsers = address_parser.add_subparsers(
2532
- dest="address_action", help="Address action"
2533
- )
68
+ return 0
69
+ except Exception as e:
70
+ console.print(f"[bold red]Error:[/bold red] {e}")
71
+ return 1
2534
72
 
2535
- # Set default address
2536
- set_default_parser = address_subparsers.add_parser(
2537
- "set-default", help="Set the default address for read-only operations"
2538
- )
2539
- set_default_parser.add_argument(
2540
- "address", help="The SS58 address to use as default"
2541
- )
2542
73
 
2543
- # Get current default address
2544
- address_subparsers.add_parser(
2545
- "get-default", help="Show the current default address for read-only operations"
2546
- )
74
+ def main():
75
+ """Main CLI entry point for hippius command."""
76
+ # Parse arguments
77
+ args = parse_arguments()
2547
78
 
2548
- # Clear default address
2549
- address_subparsers.add_parser(
2550
- "clear-default", help="Clear the default address for read-only operations"
2551
- )
79
+ if not args.command:
80
+ # Display the Hippius logo banner with Rich formatting
81
+ console.print(HERO_TITLE, style="bold cyan")
2552
82
 
2553
- args = parser.parse_args()
83
+ # Use Rich formatting for help text
84
+ from hippius_sdk.cli_rich import print_help_text
2554
85
 
2555
- if not args.command:
2556
- parser.print_help()
2557
- return 1
86
+ print_help_text(create_parser())
2558
87
 
2559
88
  try:
2560
89
  # Parse miner IDs if provided
@@ -2567,48 +96,11 @@ examples:
2567
96
  for miner in os.getenv("SUBSTRATE_DEFAULT_MINERS").split(",")
2568
97
  ]
2569
98
 
2570
- # Process encryption flags
2571
- encrypt = None
2572
- if hasattr(args, "encrypt") and args.encrypt:
2573
- encrypt = True
2574
- elif hasattr(args, "no_encrypt") and args.no_encrypt:
2575
- encrypt = False
2576
-
2577
- decrypt = None
2578
- if hasattr(args, "decrypt") and args.decrypt:
2579
- decrypt = True
2580
- elif hasattr(args, "no_decrypt") and args.no_decrypt:
2581
- decrypt = False
2582
-
2583
- # Process encryption key if provided
2584
- encryption_key = None
2585
- if hasattr(args, "encryption_key") and args.encryption_key:
2586
- try:
2587
- encryption_key = base64.b64decode(args.encryption_key)
2588
- if args.verbose:
2589
- print(f"Using provided encryption key")
2590
- except Exception as e:
2591
- print(f"Warning: Could not decode encryption key: {e}")
2592
- print(f"Using default encryption key from configuration if available")
2593
-
2594
- # Get API URL based on local_ipfs flag
2595
- api_url = "http://localhost:5001" if args.local_ipfs else args.api_url
2596
-
2597
- # Create client - using the updated client parameters
2598
- client = HippiusClient(
2599
- ipfs_gateway=args.gateway,
2600
- ipfs_api_url=api_url,
2601
- substrate_url=args.substrate_url,
2602
- substrate_seed_phrase=None, # Let it use config
2603
- seed_phrase_password=args.password if hasattr(args, "password") else None,
2604
- account_name=args.account if hasattr(args, "account") else None,
2605
- encrypt_by_default=encrypt,
2606
- encryption_key=encryption_key,
2607
- )
99
+ # Create client
100
+ client = cli_handlers.create_client(args)
2608
101
 
2609
- # Handle commands - separate async and sync handlers
2610
- # Create a helper function to handle async handlers
2611
- def run_async_handler(handler_func, *args, **kwargs):
102
+ # Helper function to handle async handlers
103
+ def run_async_handler(handler_func: Callable, *args, **kwargs) -> int:
2612
104
  # Check if the handler is async
2613
105
  if inspect.iscoroutinefunction(handler_func):
2614
106
  # Run the async handler in the event loop
@@ -2617,41 +109,64 @@ examples:
2617
109
  # Run the handler directly
2618
110
  return handler_func(*args, **kwargs)
2619
111
 
112
+ # Process encrypted flags for common parameters
113
+ encrypt = True if args.encrypt else (False if args.no_encrypt else None)
114
+ decrypt = True if args.decrypt else (False if args.no_decrypt else None)
115
+
2620
116
  # Handle commands with the helper function
2621
117
  if args.command == "download":
2622
118
  return run_async_handler(
2623
- handle_download, client, args.cid, args.output_path, decrypt=decrypt
119
+ cli_handlers.handle_download,
120
+ client,
121
+ args.cid,
122
+ args.output_path,
123
+ decrypt=decrypt,
2624
124
  )
2625
125
 
2626
126
  elif args.command == "exists":
2627
- return run_async_handler(handle_exists, client, args.cid)
127
+ return run_async_handler(cli_handlers.handle_exists, client, args.cid)
2628
128
 
2629
129
  elif args.command == "cat":
2630
130
  return run_async_handler(
2631
- handle_cat, client, args.cid, args.max_size, decrypt=decrypt
131
+ cli_handlers.handle_cat,
132
+ client,
133
+ args.cid,
134
+ args.max_size,
135
+ decrypt=decrypt,
2632
136
  )
2633
137
 
2634
138
  elif args.command == "store":
2635
139
  return run_async_handler(
2636
- handle_store, client, args.file_path, miner_ids, encrypt=encrypt
140
+ cli_handlers.handle_store,
141
+ client,
142
+ args.file_path,
143
+ miner_ids,
144
+ encrypt=encrypt,
2637
145
  )
2638
146
 
2639
147
  elif args.command == "store-dir":
2640
148
  return run_async_handler(
2641
- handle_store_dir, client, args.dir_path, miner_ids, encrypt=encrypt
149
+ cli_handlers.handle_store_dir,
150
+ client,
151
+ args.dir_path,
152
+ miner_ids,
153
+ encrypt=encrypt,
2642
154
  )
2643
155
 
2644
156
  elif args.command == "credits":
2645
- return run_async_handler(handle_credits, client, args.account_address)
157
+ return run_async_handler(
158
+ cli_handlers.handle_credits, client, args.account_address
159
+ )
2646
160
 
2647
161
  elif args.command == "files":
2648
162
  return run_async_handler(
2649
- handle_files,
163
+ cli_handlers.handle_files,
2650
164
  client,
2651
- args.account_address,
165
+ args.account_address if hasattr(args, "account_address") else None,
2652
166
  show_all_miners=(
2653
167
  args.all_miners if hasattr(args, "all_miners") else False
2654
168
  ),
169
+ file_cid=args.cid if hasattr(args, "cid") else None,
2655
170
  )
2656
171
 
2657
172
  elif args.command == "pinning-status":
@@ -2659,141 +174,275 @@ examples:
2659
174
  not args.no_contents if hasattr(args, "no_contents") else True
2660
175
  )
2661
176
  return run_async_handler(
2662
- handle_pinning_status,
177
+ cli_handlers.handle_pinning_status,
2663
178
  client,
2664
- args.account_address,
179
+ args.account_address if hasattr(args, "account_address") else None,
2665
180
  verbose=args.verbose,
2666
181
  show_contents=show_contents,
2667
182
  )
2668
183
 
2669
184
  elif args.command == "ec-files":
2670
185
  return run_async_handler(
2671
- handle_ec_files,
186
+ cli_handlers.handle_ec_files,
2672
187
  client,
2673
- args.account_address,
188
+ args.account_address if hasattr(args, "account_address") else None,
2674
189
  show_all_miners=(
2675
190
  args.all_miners if hasattr(args, "all_miners") else False
2676
191
  ),
2677
192
  show_chunks=args.show_chunks if hasattr(args, "show_chunks") else False,
193
+ filter_metadata_cid=args.cid if hasattr(args, "cid") else None,
2678
194
  )
2679
195
 
2680
196
  elif args.command == "erasure-code":
2681
197
  return run_async_handler(
2682
- handle_erasure_code,
198
+ cli_handlers.handle_erasure_code,
2683
199
  client,
2684
200
  args.file_path,
2685
201
  args.k,
2686
202
  args.m,
2687
203
  args.chunk_size,
2688
204
  miner_ids,
2689
- encrypt=args.encrypt,
2690
- publish=not args.no_publish,
205
+ encrypt=args.encrypt if hasattr(args, "encrypt") else None,
206
+ publish=not args.no_publish if hasattr(args, "no_publish") else True,
2691
207
  verbose=args.verbose,
2692
208
  )
2693
209
 
2694
210
  elif args.command == "reconstruct":
2695
211
  return run_async_handler(
2696
- handle_reconstruct,
212
+ cli_handlers.handle_reconstruct,
2697
213
  client,
2698
214
  args.metadata_cid,
2699
215
  args.output_file,
2700
216
  verbose=args.verbose,
2701
217
  )
2702
218
 
219
+ elif args.command == "delete":
220
+ return run_async_handler(
221
+ cli_handlers.handle_delete,
222
+ client,
223
+ args.cid,
224
+ force=args.force if hasattr(args, "force") else False,
225
+ )
226
+
227
+ elif args.command == "ec-delete":
228
+ return run_async_handler(
229
+ cli_handlers.handle_ec_delete,
230
+ client,
231
+ args.metadata_cid,
232
+ force=args.force if hasattr(args, "force") else False,
233
+ )
234
+
2703
235
  elif args.command == "keygen":
2704
236
  # Generate and save an encryption key
2705
- client = HippiusClient()
2706
- encryption_key = client.generate_encryption_key()
2707
- print(f"Generated encryption key: {encryption_key}")
237
+ copy_to_clipboard = args.copy if hasattr(args, "copy") else False
238
+ encryption_key = generate_encryption_key(
239
+ copy_to_clipboard=copy_to_clipboard
240
+ )
241
+
242
+ # Display the key with Rich formatting
243
+ console.print("\n[bold green]Your encryption key:[/bold green]")
244
+ console.print(f"[yellow]{encryption_key}[/yellow]")
2708
245
 
2709
- # Save to config if requested
2710
246
  if hasattr(args, "save") and args.save:
2711
- print("Saving encryption key to configuration...")
2712
- handle_config_set("encryption", "encryption_key", encryption_key)
2713
- print(
2714
- "Encryption key saved. Files will not be automatically encrypted unless you set encryption.encrypt_by_default to true"
247
+ console.print(
248
+ "\n[bold]Saving encryption key to configuration...[/bold]"
249
+ )
250
+ cli_handlers.handle_config_set(
251
+ "encryption", "encryption_key", encryption_key
252
+ )
253
+ console.print(
254
+ "[green]Encryption key saved.[/green] Files will not be automatically encrypted unless you set [cyan]encryption.encrypt_by_default[/cyan] to [cyan]true[/cyan]"
2715
255
  )
2716
256
  return 0
2717
257
 
2718
258
  elif args.command == "config":
2719
259
  if args.config_action == "get":
2720
- return handle_config_get(args.section, args.key)
260
+ return cli_handlers.handle_config_get(args.section, args.key)
2721
261
  elif args.config_action == "set":
2722
- return handle_config_set(args.section, args.key, args.value)
262
+ return cli_handlers.handle_config_set(
263
+ args.section, args.key, args.value
264
+ )
2723
265
  elif args.config_action == "list":
2724
- return handle_config_list()
266
+ return cli_handlers.handle_config_list()
2725
267
  elif args.config_action == "reset":
2726
- return handle_config_reset()
268
+ return cli_handlers.handle_config_reset()
2727
269
  elif args.config_action == "import-env":
2728
270
  initialize_from_env()
2729
271
  print("Successfully imported configuration from environment variables")
2730
272
  return 0
2731
273
  else:
2732
- config_parser.print_help()
274
+ # Display the Hippius logo banner with Rich formatting
275
+ console.print(HERO_TITLE, style="bold cyan")
276
+
277
+ config_parser = get_subparser("config")
278
+ from hippius_sdk.cli_rich import print_help_text
279
+
280
+ print_help_text(config_parser)
2733
281
  return 1
2734
282
 
2735
283
  elif args.command == "seed":
2736
284
  if args.seed_action == "set":
2737
- return handle_seed_phrase_set(
2738
- args.seed_phrase, args.encode, args.account
285
+ return cli_handlers.handle_seed_phrase_set(
286
+ args.seed_phrase,
287
+ args.encode if hasattr(args, "encode") else False,
288
+ args.account if hasattr(args, "account") else None,
2739
289
  )
2740
290
  elif args.seed_action == "encode":
2741
- return handle_seed_phrase_encode(args.account)
291
+ return cli_handlers.handle_seed_phrase_encode(
292
+ args.account if hasattr(args, "account") else None
293
+ )
2742
294
  elif args.seed_action == "decode":
2743
- return handle_seed_phrase_decode(args.account)
295
+ return cli_handlers.handle_seed_phrase_decode(
296
+ args.account if hasattr(args, "account") else None
297
+ )
2744
298
  elif args.seed_action == "status":
2745
- return handle_seed_phrase_status(args.account)
299
+ return cli_handlers.handle_seed_phrase_status(
300
+ args.account if hasattr(args, "account") else None
301
+ )
2746
302
  else:
2747
- seed_parser.print_help()
303
+ # Display the Hippius logo banner with Rich formatting
304
+ console.print(HERO_TITLE, style="bold cyan")
305
+
306
+ seed_parser = get_subparser("seed")
307
+ from hippius_sdk.cli_rich import print_help_text
308
+
309
+ print_help_text(seed_parser)
2748
310
  return 1
2749
311
 
2750
312
  # Handle the account commands
2751
313
  elif args.command == "account":
2752
314
  if args.account_action == "list":
2753
- return handle_account_list()
2754
- elif args.account_action == "create":
2755
- return handle_account_create(client, args.name, args.encrypt)
315
+ return cli_handlers.handle_account_list()
316
+ elif args.account_action == "create" and hasattr(args, "name"):
317
+ return cli_handlers.handle_account_create(
318
+ client,
319
+ args.name,
320
+ encrypt=args.encrypt if hasattr(args, "encrypt") else False,
321
+ )
2756
322
  elif args.account_action == "export":
2757
- return handle_account_export(client, args.name, args.file)
2758
- elif args.account_action == "import":
2759
- return handle_account_import(client, args.file, args.encrypt)
2760
- elif args.account_action == "info":
2761
- return run_async_handler(
2762
- handle_account_info, client, args.account_name, args.history
323
+ return cli_handlers.handle_account_export(
324
+ client,
325
+ args.name if hasattr(args, "name") else None,
326
+ args.file_path if hasattr(args, "file_path") else None,
327
+ )
328
+ elif args.account_action == "import" and hasattr(args, "file_path"):
329
+ return cli_handlers.handle_account_import(
330
+ client,
331
+ args.file_path,
332
+ encrypt=args.encrypt if hasattr(args, "encrypt") else False,
2763
333
  )
334
+ elif args.account_action == "switch" and hasattr(args, "account_name"):
335
+ return cli_handlers.handle_account_switch(args.account_name)
336
+ elif args.account_action == "delete" and hasattr(args, "account_name"):
337
+ return cli_handlers.handle_account_delete(args.account_name)
2764
338
  elif args.account_action == "balance":
339
+ # Get account address - prioritize direct address over account name
340
+ account_address = None
341
+ if hasattr(args, "address") and args.address:
342
+ # If address is directly provided, use it
343
+ account_address = args.address
344
+ elif hasattr(args, "name") and args.name:
345
+ # If name is provided, get the address from the account
346
+ try:
347
+ account_address = cli_handlers.get_account_address(args.name)
348
+ except Exception as e:
349
+ error(f"Error getting address for account '{args.name}': {e}")
350
+ return 1
351
+
2765
352
  return run_async_handler(
2766
- handle_account_balance,
353
+ cli_handlers.handle_account_balance,
2767
354
  client,
2768
- args.account_name,
2769
- args.watch,
2770
- args.interval,
355
+ account_address,
2771
356
  )
2772
- elif args.account_action == "switch":
2773
- return handle_account_switch(args.account_name)
2774
- elif args.account_action == "delete":
2775
- return handle_account_delete(args.account_name)
2776
357
  else:
2777
- account_parser.print_help()
358
+ # Display the Hippius logo banner with Rich formatting
359
+ console.print(HERO_TITLE, style="bold cyan")
360
+
361
+ account_parser = get_subparser("account")
362
+ from hippius_sdk.cli_rich import print_help_text
363
+
364
+ print_help_text(account_parser)
2778
365
  return 1
2779
366
 
2780
- # Handle the address commands
367
+ # Handle address commands
2781
368
  elif args.command == "address":
2782
- if args.address_action == "set-default":
2783
- return handle_default_address_set(args.address)
369
+ if args.address_action == "set-default" and hasattr(args, "address"):
370
+ return cli_handlers.handle_default_address_set(args.address)
2784
371
  elif args.address_action == "get-default":
2785
- return handle_default_address_get()
372
+ return cli_handlers.handle_default_address_get()
2786
373
  elif args.address_action == "clear-default":
2787
- return handle_default_address_clear()
374
+ return cli_handlers.handle_default_address_clear()
2788
375
  else:
2789
- address_parser.print_help()
376
+ # Display the Hippius logo banner with Rich formatting
377
+ console.print(HERO_TITLE, style="bold cyan")
378
+
379
+ address_parser = get_subparser("address")
380
+ from hippius_sdk.cli_rich import print_help_text
381
+
382
+ print_help_text(address_parser)
2790
383
  return 1
2791
384
 
385
+ else:
386
+ # Command not recognized
387
+ error(f"Unknown command: [bold]{args.command}[/bold]")
388
+ return 1
389
+
390
+ except KeyboardInterrupt:
391
+ error("\nOperation cancelled by user")
392
+ return 1
2792
393
  except Exception as e:
2793
- print(f"Error: {e}")
394
+ error(f"{str(e)}")
395
+ if args.verbose:
396
+ import traceback
397
+
398
+ console.print("\n[bold red]Traceback:[/bold red]")
399
+ traceback.print_exc()
2794
400
  return 1
2795
401
 
2796
- return 0
402
+
403
+ def key_generation_cli():
404
+ """Standalone CLI tool for generating encryption keys."""
405
+ # Check if help flag is present
406
+ if "--help" in sys.argv or "-h" in sys.argv:
407
+ # Display the logo and help text with nice formatting
408
+ console.print(HERO_TITLE, style="bold cyan")
409
+
410
+ # Parse arguments
411
+ import argparse
412
+
413
+ parser = argparse.ArgumentParser(
414
+ description="Generate an encryption key for Hippius SDK"
415
+ )
416
+ parser.add_argument(
417
+ "--clipboard",
418
+ "-c",
419
+ action="store_true",
420
+ help="Copy the key to clipboard",
421
+ )
422
+ parser.add_argument(
423
+ "--save", "-s", action="store_true", help="Save the key to configuration"
424
+ )
425
+
426
+ args = parser.parse_args()
427
+
428
+ # Display encryption key generator title
429
+ console.print("[bold blue]Encryption Key Generator[/bold blue]\n")
430
+
431
+ # Generate and display the key
432
+ key = generate_encryption_key(copy_to_clipboard=args.clipboard)
433
+
434
+ # Display the key in a panel with formatting
435
+ console.print("\n[bold]Your encryption key:[/bold]")
436
+ console.print(f"[yellow]{key}[/yellow]", highlight=False)
437
+
438
+ # Save to config if requested
439
+ if args.save:
440
+ from hippius_sdk import cli_handlers
441
+
442
+ cli_handlers.handle_config_set("encryption", "encryption_key", key)
443
+ console.print(
444
+ "\n[green]Encryption key saved to configuration.[/green] Files will not be automatically encrypted unless you set [bold]encryption.encrypt_by_default[/bold] to [bold]true[/bold]."
445
+ )
2797
446
 
2798
447
 
2799
448
  if __name__ == "__main__":