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-0.2.3.dist-info → hippius-0.2.5.dist-info}/METADATA +207 -151
- hippius-0.2.5.dist-info/RECORD +17 -0
- hippius_sdk/__init__.py +1 -1
- hippius_sdk/cli.py +18 -1
- hippius_sdk/cli_assets.py +8 -6
- hippius_sdk/cli_handlers.py +796 -185
- hippius_sdk/cli_parser.py +25 -0
- hippius_sdk/client.py +19 -17
- hippius_sdk/config.py +108 -141
- hippius_sdk/errors.py +77 -0
- hippius_sdk/ipfs.py +301 -340
- hippius_sdk/ipfs_core.py +209 -9
- hippius_sdk/substrate.py +105 -27
- hippius-0.2.3.dist-info/RECORD +0 -16
- {hippius-0.2.3.dist-info → hippius-0.2.5.dist-info}/WHEEL +0 -0
- {hippius-0.2.3.dist-info → hippius-0.2.5.dist-info}/entry_points.txt +0 -0
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
|
-
|
68
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
) ->
|
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
|
-
|
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
|
-
|
282
|
-
|
283
|
-
|
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
|
-
|
287
|
-
|
285
|
+
# Derive the key from the password and salt
|
286
|
+
key, _ = _derive_key_from_password(password, salt_bytes)
|
288
287
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
302
|
-
|
297
|
+
# Create a SecretBox with our derived key
|
298
|
+
box = nacl.secret.SecretBox(key)
|
303
299
|
|
304
|
-
|
305
|
-
|
300
|
+
# Decrypt the data
|
301
|
+
decrypted_data = box.decrypt(encrypted_bytes)
|
306
302
|
|
307
|
-
|
308
|
-
|
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
|
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
|
-
#
|
348
|
-
if
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
}
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
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
|
-
|
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
|
-
|
412
|
-
|
391
|
+
# Only use the multi-account system
|
392
|
+
name_to_use = account_name or config["substrate"].get("active_account")
|
413
393
|
|
414
|
-
|
415
|
-
|
394
|
+
if not name_to_use:
|
395
|
+
print("Error: No account specified and no active account")
|
396
|
+
return None
|
416
397
|
|
417
|
-
|
418
|
-
|
419
|
-
|
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
|
-
|
422
|
-
|
402
|
+
account_data = config["substrate"]["accounts"][name_to_use]
|
403
|
+
is_encoded = account_data.get("seed_phrase_encoded", False)
|
423
404
|
|
424
|
-
|
425
|
-
|
405
|
+
if not is_encoded:
|
406
|
+
return account_data.get("seed_phrase")
|
426
407
|
|
427
|
-
|
428
|
-
|
429
|
-
return None
|
408
|
+
encrypted_data = account_data.get("seed_phrase")
|
409
|
+
salt = account_data.get("seed_phrase_salt")
|
430
410
|
|
431
|
-
|
432
|
-
|
433
|
-
|
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
|
-
|
436
|
-
|
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
|
-
|
439
|
-
|
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
|
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
|
-
#
|
459
|
-
|
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
|
-
|
464
|
-
|
465
|
-
|
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
|
-
|
472
|
-
|
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
|
-
|
475
|
-
|
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
|
-
|
480
|
-
return decrypt_seed_phrase(password, account_name)
|
453
|
+
return decrypt_seed_phrase(password, name_to_use)
|
481
454
|
else:
|
482
|
-
|
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
|
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
|
-
#
|
524
|
-
if
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
}
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
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
|