hippius 0.2.3__py3-none-any.whl → 0.2.5__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_parser.py CHANGED
@@ -197,12 +197,32 @@ def add_storage_commands(subparsers):
197
197
  "store", help="Upload a file to IPFS and store it on Substrate"
198
198
  )
199
199
  store_parser.add_argument("file_path", help="Path to file to upload")
200
+ store_parser.add_argument(
201
+ "--publish",
202
+ action="store_true",
203
+ help="Publish file to IPFS and store on the blockchain (default)",
204
+ )
205
+ store_parser.add_argument(
206
+ "--no-publish",
207
+ action="store_true",
208
+ help="Don't publish file to IPFS or store on the blockchain (local only)",
209
+ )
200
210
 
201
211
  # Store directory command
202
212
  store_dir_parser = subparsers.add_parser(
203
213
  "store-dir", help="Upload a directory to IPFS and store all files on Substrate"
204
214
  )
205
215
  store_dir_parser.add_argument("dir_path", help="Path to directory to upload")
216
+ store_dir_parser.add_argument(
217
+ "--publish",
218
+ action="store_true",
219
+ help="Publish all files to IPFS and store on the blockchain (default)",
220
+ )
221
+ store_dir_parser.add_argument(
222
+ "--no-publish",
223
+ action="store_true",
224
+ help="Don't publish files to IPFS or store on the blockchain (local only)",
225
+ )
206
226
 
207
227
  # Pinning status command
208
228
  pinning_status_parser = subparsers.add_parser(
@@ -520,6 +540,11 @@ def add_account_commands(subparsers):
520
540
  help="Account name to show info for (uses active account if not specified)",
521
541
  )
522
542
 
543
+ # Account login
544
+ login_account_parser = account_subparsers.add_parser(
545
+ "login", help="Login with an account address and seed phrase"
546
+ )
547
+
523
548
  # Account balance
524
549
  balance_account_parser = account_subparsers.add_parser(
525
550
  "balance", help="Check account balance"
hippius_sdk/client.py CHANGED
@@ -64,8 +64,8 @@ class HippiusClient:
64
64
  "substrate", "url", "wss://rpc.hippius.network"
65
65
  )
66
66
 
67
- if substrate_seed_phrase is None:
68
- substrate_seed_phrase = get_config_value("substrate", "seed_phrase")
67
+ # Don't try to get a seed phrase from the legacy location
68
+ # The substrate_client will handle getting it from the active account
69
69
 
70
70
  if encrypt_by_default is None:
71
71
  encrypt_by_default = get_config_value(
@@ -82,18 +82,13 @@ class HippiusClient:
82
82
  encrypt_by_default=encrypt_by_default,
83
83
  encryption_key=encryption_key,
84
84
  )
85
-
86
85
  # Initialize Substrate client
87
- try:
88
- self.substrate_client = SubstrateClient(
89
- url=substrate_url,
90
- seed_phrase=substrate_seed_phrase,
91
- password=seed_phrase_password,
92
- account_name=account_name,
93
- )
94
- except Exception as e:
95
- print(f"Warning: Could not initialize Substrate client: {e}")
96
- self.substrate_client = None
86
+ self.substrate_client = SubstrateClient(
87
+ url=substrate_url,
88
+ seed_phrase=substrate_seed_phrase,
89
+ password=seed_phrase_password,
90
+ account_name=account_name,
91
+ )
97
92
 
98
93
  async def upload_file(
99
94
  self, file_path: str, encrypt: Optional[bool] = None
@@ -153,10 +148,11 @@ class HippiusClient:
153
148
  ) -> Dict[str, Any]:
154
149
  """
155
150
  Download a file from IPFS with optional decryption.
151
+ Supports downloading directories - in that case, a directory structure will be created.
156
152
 
157
153
  Args:
158
154
  cid: Content Identifier (CID) of the file to download
159
- output_path: Path where the downloaded file will be saved
155
+ output_path: Path where the downloaded file/directory will be saved
160
156
  decrypt: Whether to decrypt the file (overrides default)
161
157
 
162
158
  Returns:
@@ -167,6 +163,7 @@ class HippiusClient:
167
163
  - size_formatted: Human-readable file size
168
164
  - elapsed_seconds: Time taken for the download
169
165
  - decrypted: Whether the file was decrypted
166
+ - is_directory: Whether the download was a directory
170
167
 
171
168
  Raises:
172
169
  requests.RequestException: If the download fails
@@ -375,6 +372,7 @@ class HippiusClient:
375
372
  max_retries: int = 3,
376
373
  verbose: bool = True,
377
374
  progress_callback: Optional[Callable[[str, int, int], None]] = None,
375
+ publish: bool = True,
378
376
  ) -> Dict[str, Any]:
379
377
  """
380
378
  Erasure code a file, upload the chunks to IPFS, and store in the Hippius marketplace.
@@ -392,9 +390,12 @@ class HippiusClient:
392
390
  verbose: Whether to print progress information
393
391
  progress_callback: Optional callback function for progress updates
394
392
  Function receives (stage_name, current, total)
393
+ publish: Whether to publish to the blockchain (True) or just perform local
394
+ erasure coding without publishing (False). When False, no password
395
+ is needed for seed phrase access.
395
396
 
396
397
  Returns:
397
- dict: Result including metadata CID and transaction hash
398
+ dict: Result including metadata CID and transaction hash (if published)
398
399
 
399
400
  Raises:
400
401
  ValueError: If parameters are invalid
@@ -411,6 +412,7 @@ class HippiusClient:
411
412
  max_retries=max_retries,
412
413
  verbose=verbose,
413
414
  progress_callback=progress_callback,
415
+ publish=publish,
414
416
  )
415
417
 
416
418
  async def delete_file(
@@ -436,7 +438,7 @@ class HippiusClient:
436
438
  metadata_cid: str,
437
439
  cancel_from_blockchain: bool = True,
438
440
  parallel_limit: int = 20,
439
- ) -> Dict[str, Any]:
441
+ ) -> bool:
440
442
  """
441
443
  Delete an erasure-coded file, including all its chunks in parallel.
442
444
 
@@ -446,7 +448,7 @@ class HippiusClient:
446
448
  parallel_limit: Maximum number of concurrent deletion operations
447
449
 
448
450
  Returns:
449
- Dict containing the result of the operation
451
+ True or false if failed.
450
452
 
451
453
  Raises:
452
454
  RuntimeError: If deletion fails completely
hippius_sdk/config.py CHANGED
@@ -278,34 +278,30 @@ def decrypt_with_password(encrypted_data: str, salt: str, password: str) -> str:
278
278
  Returns:
279
279
  str: Decrypted data
280
280
  """
281
- try:
282
- # Decode the encrypted data and salt
283
- encrypted_bytes = base64.b64decode(encrypted_data)
284
- salt_bytes = base64.b64decode(salt)
281
+ # Decode the encrypted data and salt
282
+ encrypted_bytes = base64.b64decode(encrypted_data)
283
+ salt_bytes = base64.b64decode(salt)
285
284
 
286
- # Derive the key from the password and salt
287
- key, _ = _derive_key_from_password(password, salt_bytes)
285
+ # Derive the key from the password and salt
286
+ key, _ = _derive_key_from_password(password, salt_bytes)
288
287
 
289
- # Verify NaCl is available (imported at the top)
290
- try:
291
- if not hasattr(nacl, "secret") or not hasattr(nacl, "utils"):
292
- raise ImportError("NaCl modules not available")
293
- except ImportError:
294
- raise ValueError(
295
- "PyNaCl is required for decryption. Install it with: pip install pynacl"
296
- )
297
-
298
- # Create a SecretBox with our derived key
299
- box = nacl.secret.SecretBox(key)
288
+ # Verify NaCl is available (imported at the top)
289
+ try:
290
+ if not hasattr(nacl, "secret") or not hasattr(nacl, "utils"):
291
+ raise ImportError("NaCl modules not available")
292
+ except ImportError:
293
+ raise ValueError(
294
+ "PyNaCl is required for decryption. Install it with: pip install pynacl"
295
+ )
300
296
 
301
- # Decrypt the data
302
- decrypted_data = box.decrypt(encrypted_bytes)
297
+ # Create a SecretBox with our derived key
298
+ box = nacl.secret.SecretBox(key)
303
299
 
304
- # Return the decrypted string
305
- return decrypted_data.decode("utf-8")
300
+ # Decrypt the data
301
+ decrypted_data = box.decrypt(encrypted_bytes)
306
302
 
307
- except Exception as e:
308
- raise ValueError(f"Error decrypting data with password: {e}")
303
+ # Return the decrypted string
304
+ return decrypted_data.decode("utf-8")
309
305
 
310
306
 
311
307
  def encrypt_seed_phrase(
@@ -317,7 +313,7 @@ def encrypt_seed_phrase(
317
313
  Args:
318
314
  seed_phrase: The plain text seed phrase to encrypt
319
315
  password: Optional password (if None, will prompt)
320
- account_name: Optional name for the account (if None, uses legacy mode or active account)
316
+ account_name: Optional name for the account (if None, uses active account)
321
317
 
322
318
  Returns:
323
319
  bool: True if encryption and saving was successful, False otherwise
@@ -344,30 +340,31 @@ def encrypt_seed_phrase(
344
340
 
345
341
  config = load_config()
346
342
 
347
- # Check if we're using the new multi-account system
348
- if account_name is not None:
349
- # Multi-account mode
350
- if "accounts" not in config["substrate"]:
351
- config["substrate"]["accounts"] = {}
352
-
353
- # Store the account data
354
- config["substrate"]["accounts"][account_name] = {
355
- "seed_phrase": encrypted_data,
356
- "seed_phrase_encoded": True,
357
- "seed_phrase_salt": salt,
358
- "ss58_address": ss58_address,
359
- }
360
-
361
- # Set as active account if no active account exists
362
- if not config["substrate"].get("active_account"):
363
- config["substrate"]["active_account"] = account_name
364
-
365
- else:
366
- # Legacy mode - single account
367
- config["substrate"]["seed_phrase"] = encrypted_data
368
- config["substrate"]["seed_phrase_encoded"] = True
369
- config["substrate"]["seed_phrase_salt"] = salt
370
- config["substrate"]["ss58_address"] = ss58_address
343
+ # Only use multi-account mode
344
+ # Use active account if no account specified
345
+ name_to_use = account_name
346
+ if name_to_use is None:
347
+ name_to_use = config["substrate"].get("active_account")
348
+ if not name_to_use:
349
+ # If no active account, we need a name
350
+ print("Error: No account name specified and no active account")
351
+ return False
352
+
353
+ # Ensure accounts structure exists
354
+ if "accounts" not in config["substrate"]:
355
+ config["substrate"]["accounts"] = {}
356
+
357
+ # Store the account data
358
+ config["substrate"]["accounts"][name_to_use] = {
359
+ "seed_phrase": encrypted_data,
360
+ "seed_phrase_encoded": True,
361
+ "seed_phrase_salt": salt,
362
+ "ss58_address": ss58_address,
363
+ }
364
+
365
+ # Set as active account if no active account exists
366
+ if not config["substrate"].get("active_account"):
367
+ config["substrate"]["active_account"] = name_to_use
371
368
 
372
369
  return save_config(config)
373
370
 
@@ -384,60 +381,43 @@ def decrypt_seed_phrase(
384
381
 
385
382
  Args:
386
383
  password: Optional password (if None, will prompt)
387
- account_name: Optional account name (if None, uses active account or legacy mode)
384
+ account_name: Optional account name (if None, uses active account)
388
385
 
389
386
  Returns:
390
387
  Optional[str]: The decrypted seed phrase, or None if decryption failed
391
388
  """
392
- try:
393
- config = load_config()
394
-
395
- # Determine if we're using multi-account mode
396
- if account_name is not None or config["substrate"].get("active_account"):
397
- # Multi-account mode
398
- name_to_use = account_name or config["substrate"].get("active_account")
399
-
400
- if not name_to_use:
401
- print("Error: No account specified and no active account")
402
- return None
403
-
404
- if name_to_use not in config["substrate"].get("accounts", {}):
405
- print(f"Error: Account '{name_to_use}' not found")
406
- return None
407
-
408
- account_data = config["substrate"]["accounts"][name_to_use]
409
- is_encoded = account_data.get("seed_phrase_encoded", False)
389
+ config = load_config()
410
390
 
411
- if not is_encoded:
412
- return account_data.get("seed_phrase")
391
+ # Only use the multi-account system
392
+ name_to_use = account_name or config["substrate"].get("active_account")
413
393
 
414
- encrypted_data = account_data.get("seed_phrase")
415
- salt = account_data.get("seed_phrase_salt")
394
+ if not name_to_use:
395
+ print("Error: No account specified and no active account")
396
+ return None
416
397
 
417
- else:
418
- # Legacy mode - single account
419
- is_encoded = config["substrate"].get("seed_phrase_encoded", False)
398
+ if name_to_use not in config["substrate"].get("accounts", {}):
399
+ print(f"Error: Account '{name_to_use}' not found")
400
+ return None
420
401
 
421
- if not is_encoded:
422
- return config["substrate"].get("seed_phrase")
402
+ account_data = config["substrate"]["accounts"][name_to_use]
403
+ is_encoded = account_data.get("seed_phrase_encoded", False)
423
404
 
424
- encrypted_data = config["substrate"].get("seed_phrase")
425
- salt = config["substrate"].get("seed_phrase_salt")
405
+ if not is_encoded:
406
+ return account_data.get("seed_phrase")
426
407
 
427
- if not encrypted_data or not salt:
428
- print("Error: No encrypted seed phrase found or missing salt")
429
- return None
408
+ encrypted_data = account_data.get("seed_phrase")
409
+ salt = account_data.get("seed_phrase_salt")
430
410
 
431
- # Get password from user if not provided
432
- if password is None:
433
- password = getpass.getpass("Enter password to decrypt seed phrase: ")
411
+ if not encrypted_data or not salt:
412
+ print("Error: No encrypted seed phrase found or missing salt")
413
+ return None
434
414
 
435
- # Decrypt the seed phrase
436
- return decrypt_with_password(encrypted_data, salt, password)
415
+ # Get password from user if not provided
416
+ if password is None:
417
+ password = getpass.getpass("Enter password to decrypt seed phrase: \n\n")
437
418
 
438
- except Exception as e:
439
- print(f"Error decrypting seed phrase: {e}")
440
- return None
419
+ # Decrypt the seed phrase
420
+ return decrypt_with_password(encrypted_data, salt, password)
441
421
 
442
422
 
443
423
  def get_seed_phrase(
@@ -448,45 +428,31 @@ def get_seed_phrase(
448
428
 
449
429
  Args:
450
430
  password: Optional password for decryption (if None and needed, will prompt)
451
- account_name: Optional account name (if None, uses active account or legacy mode)
431
+ account_name: Optional account name (if None, uses active account)
452
432
 
453
433
  Returns:
454
434
  Optional[str]: The seed phrase, or None if not available
455
435
  """
456
436
  config = load_config()
457
437
 
458
- # Determine if we're using multi-account mode
459
- if account_name is not None or config["substrate"].get("active_account"):
460
- # Multi-account mode
461
- name_to_use = account_name or config["substrate"].get("active_account")
438
+ # Only use the multi-account system
439
+ name_to_use = account_name or config["substrate"].get("active_account")
462
440
 
463
- if not name_to_use:
464
- print("Error: No account specified and no active account")
465
- return None
466
-
467
- if name_to_use not in config["substrate"].get("accounts", {}):
468
- print(f"Error: Account '{name_to_use}' not found")
469
- return None
441
+ if not name_to_use:
442
+ print("Error: No account specified and no active account")
443
+ return None
470
444
 
471
- account_data = config["substrate"]["accounts"][name_to_use]
472
- is_encoded = account_data.get("seed_phrase_encoded", False)
445
+ if name_to_use not in config["substrate"].get("accounts", {}):
446
+ print(f"Error: Account '{name_to_use}' not found")
447
+ return None
473
448
 
474
- else:
475
- # Legacy mode - single account
476
- is_encoded = config["substrate"].get("seed_phrase_encoded", False)
449
+ account_data = config["substrate"]["accounts"][name_to_use]
450
+ is_encoded = account_data.get("seed_phrase_encoded", False)
477
451
 
478
452
  if is_encoded:
479
- # If encoded, decrypt it
480
- return decrypt_seed_phrase(password, account_name)
453
+ return decrypt_seed_phrase(password, name_to_use)
481
454
  else:
482
- # If not encoded, just return the plain text seed phrase
483
- if account_name is not None or config["substrate"].get("active_account"):
484
- # Multi-account mode
485
- name_to_use = account_name or config["substrate"].get("active_account")
486
- return config["substrate"]["accounts"][name_to_use].get("seed_phrase")
487
- else:
488
- # Legacy mode
489
- return config["substrate"].get("seed_phrase")
455
+ return account_data.get("seed_phrase")
490
456
 
491
457
 
492
458
  def set_seed_phrase(
@@ -502,7 +468,7 @@ def set_seed_phrase(
502
468
  seed_phrase: The seed phrase to store
503
469
  encode: Whether to encrypt the seed phrase (requires password)
504
470
  password: Optional password for encryption (if None and encode=True, will prompt)
505
- account_name: Optional name for the account (if None, uses legacy mode or active account)
471
+ account_name: Optional name for the account (if None, uses active account)
506
472
 
507
473
  Returns:
508
474
  bool: True if saving was successful, False otherwise
@@ -520,30 +486,31 @@ def set_seed_phrase(
520
486
  except Exception as e:
521
487
  print(f"Warning: Could not derive SS58 address: {e}")
522
488
 
523
- # Determine if we're using multi-account mode
524
- if account_name is not None:
525
- # Multi-account mode
526
- if "accounts" not in config["substrate"]:
527
- config["substrate"]["accounts"] = {}
528
-
529
- # Store the account data
530
- config["substrate"]["accounts"][account_name] = {
531
- "seed_phrase": seed_phrase,
532
- "seed_phrase_encoded": False,
533
- "seed_phrase_salt": None,
534
- "ss58_address": ss58_address,
535
- }
536
-
537
- # Set as active account if no active account exists
538
- if not config["substrate"].get("active_account"):
539
- config["substrate"]["active_account"] = account_name
540
-
541
- else:
542
- # Legacy mode - single account
543
- config["substrate"]["seed_phrase"] = seed_phrase
544
- config["substrate"]["seed_phrase_encoded"] = False
545
- config["substrate"]["seed_phrase_salt"] = None
546
- config["substrate"]["ss58_address"] = ss58_address
489
+ # Only use multi-account mode
490
+ # Use active account if no account specified
491
+ name_to_use = account_name
492
+ if name_to_use is None:
493
+ name_to_use = config["substrate"].get("active_account")
494
+ if not name_to_use:
495
+ # If no active account, we need a name
496
+ print("Error: No account name specified and no active account")
497
+ return False
498
+
499
+ # Ensure accounts structure exists
500
+ if "accounts" not in config["substrate"]:
501
+ config["substrate"]["accounts"] = {}
502
+
503
+ # Store the account data
504
+ config["substrate"]["accounts"][name_to_use] = {
505
+ "seed_phrase": seed_phrase,
506
+ "seed_phrase_encoded": False,
507
+ "seed_phrase_salt": None,
508
+ "ss58_address": ss58_address,
509
+ }
510
+
511
+ # Set as active account if no active account exists
512
+ if not config["substrate"].get("active_account"):
513
+ config["substrate"]["active_account"] = name_to_use
547
514
 
548
515
  return save_config(config)
549
516
 
hippius_sdk/errors.py ADDED
@@ -0,0 +1,77 @@
1
+ """
2
+ Custom exceptions for the Hippius SDK.
3
+ """
4
+
5
+
6
+ class HippiusError(Exception):
7
+ """Base exception for all Hippius-specific errors."""
8
+
9
+ pass
10
+
11
+
12
+ class HippiusSubstrateError(HippiusError):
13
+ """Base exception for Substrate-related errors."""
14
+
15
+ pass
16
+
17
+
18
+ class HippiusIPFSError(HippiusError):
19
+ """Base exception for IPFS-related errors."""
20
+
21
+ pass
22
+
23
+
24
+ # Specific blockchain errors
25
+ class HippiusNotFoundError(HippiusSubstrateError):
26
+ """Raised when a resource is not found on the blockchain."""
27
+
28
+ pass
29
+
30
+
31
+ class HippiusAlreadyDeletedError(HippiusSubstrateError):
32
+ """Raised when trying to delete a file that's already deleted from the blockchain."""
33
+
34
+ pass
35
+
36
+
37
+ class HippiusSubstrateConnectionError(HippiusSubstrateError):
38
+ """Raised when there's an issue connecting to the Substrate node."""
39
+
40
+ pass
41
+
42
+
43
+ class HippiusSubstrateAuthError(HippiusSubstrateError):
44
+ """Raised when there's an authentication issue with the Substrate client."""
45
+
46
+ pass
47
+
48
+
49
+ class HippiusFailedSubstrateDelete(HippiusSubstrateError):
50
+ """Raised when deletion from blockchain storage fails."""
51
+
52
+ pass
53
+
54
+
55
+ # IPFS-specific errors
56
+ class HippiusIPFSConnectionError(HippiusIPFSError):
57
+ """Raised when there's an issue connecting to IPFS."""
58
+
59
+ pass
60
+
61
+
62
+ class HippiusFailedIPFSUnpin(HippiusIPFSError):
63
+ """Raised when unpinning from IPFS fails."""
64
+
65
+ pass
66
+
67
+
68
+ class HippiusMetadataError(HippiusIPFSError):
69
+ """Raised when there's an issue with the metadata file."""
70
+
71
+ pass
72
+
73
+
74
+ class HippiusInvalidCIDError(HippiusIPFSError):
75
+ """Raised when an invalid CID is provided."""
76
+
77
+ pass