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_handlers.py
CHANGED
@@ -9,12 +9,11 @@ import asyncio
|
|
9
9
|
import base64
|
10
10
|
import getpass
|
11
11
|
import json
|
12
|
+
import math
|
12
13
|
import os
|
13
|
-
import pprint
|
14
|
-
import sys
|
15
14
|
import tempfile
|
16
15
|
import time
|
17
|
-
from typing import Any,
|
16
|
+
from typing import Any, List, Optional
|
18
17
|
|
19
18
|
from hippius_sdk import (
|
20
19
|
HippiusClient,
|
@@ -26,8 +25,6 @@ from hippius_sdk import (
|
|
26
25
|
get_active_account,
|
27
26
|
get_all_config,
|
28
27
|
get_config_value,
|
29
|
-
get_seed_phrase,
|
30
|
-
initialize_from_env,
|
31
28
|
list_accounts,
|
32
29
|
load_config,
|
33
30
|
reset_config,
|
@@ -48,6 +45,12 @@ from hippius_sdk.cli_rich import (
|
|
48
45
|
success,
|
49
46
|
warning,
|
50
47
|
)
|
48
|
+
from hippius_sdk.errors import (
|
49
|
+
HippiusAlreadyDeletedError,
|
50
|
+
HippiusFailedIPFSUnpin,
|
51
|
+
HippiusFailedSubstrateDelete,
|
52
|
+
HippiusMetadataError,
|
53
|
+
)
|
51
54
|
|
52
55
|
try:
|
53
56
|
import nacl.secret
|
@@ -98,6 +101,21 @@ def create_client(args: Any) -> HippiusClient:
|
|
98
101
|
# Get substrate URL
|
99
102
|
substrate_url = args.substrate_url if hasattr(args, "substrate_url") else None
|
100
103
|
|
104
|
+
# Skip password if we're doing erasure-code with --no-publish
|
105
|
+
# This avoids prompting for password when we don't need to interact with the blockchain
|
106
|
+
password = None
|
107
|
+
if (
|
108
|
+
hasattr(args, "command")
|
109
|
+
and args.command == "erasure-code"
|
110
|
+
and hasattr(args, "no_publish")
|
111
|
+
and args.no_publish
|
112
|
+
):
|
113
|
+
# Don't need a password in this case
|
114
|
+
password = None
|
115
|
+
else:
|
116
|
+
# Use password from args if provided
|
117
|
+
password = args.password if hasattr(args, "password") else None
|
118
|
+
|
101
119
|
# Initialize client with provided parameters
|
102
120
|
client = HippiusClient(
|
103
121
|
ipfs_gateway=gateway,
|
@@ -106,7 +124,7 @@ def create_client(args: Any) -> HippiusClient:
|
|
106
124
|
substrate_seed_phrase=(
|
107
125
|
args.seed_phrase if hasattr(args, "seed_phrase") else None
|
108
126
|
),
|
109
|
-
seed_phrase_password=
|
127
|
+
seed_phrase_password=password,
|
110
128
|
account_name=args.account if hasattr(args, "account") else None,
|
111
129
|
encrypt_by_default=encrypt,
|
112
130
|
encryption_key=encryption_key,
|
@@ -136,6 +154,12 @@ async def handle_download(
|
|
136
154
|
f"Size: [bold cyan]{result['size_bytes']:,}[/bold cyan] bytes ([bold cyan]{result['size_formatted']}[/bold cyan])",
|
137
155
|
]
|
138
156
|
|
157
|
+
# Add details about content type
|
158
|
+
if result.get("is_directory", False):
|
159
|
+
details.append("[bold green]Content type: Directory[/bold green]")
|
160
|
+
else:
|
161
|
+
details.append("[bold blue]Content type: File[/bold blue]")
|
162
|
+
|
139
163
|
if result.get("decrypted"):
|
140
164
|
details.append("[bold yellow]File was decrypted during download[/bold yellow]")
|
141
165
|
|
@@ -160,7 +184,7 @@ async def handle_exists(client: HippiusClient, cid: str) -> int:
|
|
160
184
|
log(f"Gateway URL: [link]{result['gateway_url']}[/link]")
|
161
185
|
|
162
186
|
# Display download command in a panel
|
163
|
-
command = f"hippius download {formatted_cid} <output_path>"
|
187
|
+
command = f"[bold green underline]hippius download {formatted_cid} <output_path>[/bold green underline]"
|
164
188
|
print_panel(command, title="Download Command")
|
165
189
|
else:
|
166
190
|
error(f"CID [bold cyan]{formatted_cid}[/bold cyan] does not exist on IPFS")
|
@@ -331,7 +355,7 @@ async def handle_store(
|
|
331
355
|
)
|
332
356
|
|
333
357
|
# Display download command in a panel
|
334
|
-
command = f"hippius download {result['cid']} <output_path>"
|
358
|
+
command = f"[bold green underline]hippius download {result['cid']} <output_path>[/bold green underline]"
|
335
359
|
print_panel(command, title="Download Command")
|
336
360
|
|
337
361
|
return 0
|
@@ -347,6 +371,7 @@ async def handle_store_dir(
|
|
347
371
|
dir_path: str,
|
348
372
|
miner_ids: Optional[List[str]] = None,
|
349
373
|
encrypt: Optional[bool] = None,
|
374
|
+
publish: bool = True,
|
350
375
|
) -> int:
|
351
376
|
"""Handle the store directory command"""
|
352
377
|
if not os.path.exists(dir_path):
|
@@ -406,12 +431,44 @@ async def handle_store_dir(
|
|
406
431
|
updater = asyncio.create_task(update_progress())
|
407
432
|
|
408
433
|
try:
|
434
|
+
# Upload info message based on publish flag
|
435
|
+
if not publish:
|
436
|
+
upload_info.append(
|
437
|
+
"[bold yellow]Publishing: Disabled (local upload only)[/bold yellow]"
|
438
|
+
)
|
439
|
+
log(
|
440
|
+
"\nUpload will be local only - not publishing to blockchain or pinning to IPFS"
|
441
|
+
)
|
442
|
+
else:
|
443
|
+
upload_info.append(
|
444
|
+
"[bold green]Publishing: Enabled (publishing to blockchain)[/bold green]"
|
445
|
+
)
|
446
|
+
|
447
|
+
# Display updated upload information panel
|
448
|
+
print_panel("\n".join(upload_info), title="Directory Upload Operation")
|
449
|
+
|
409
450
|
# Use the store_directory method
|
410
451
|
result = await client.ipfs_client.upload_directory(
|
411
452
|
dir_path=dir_path,
|
412
453
|
encrypt=encrypt,
|
413
454
|
)
|
414
455
|
|
456
|
+
# Skip publishing to blockchain if publish is False
|
457
|
+
if not publish:
|
458
|
+
# Remove any blockchain-related data from result to ensure we don't try to use it
|
459
|
+
if "transaction_hash" in result:
|
460
|
+
del result["transaction_hash"]
|
461
|
+
else:
|
462
|
+
# If we want to publish, make sure files are pinned globally
|
463
|
+
for file_info in result.get("files", []):
|
464
|
+
if "cid" in file_info:
|
465
|
+
try:
|
466
|
+
await client.ipfs_client.publish_global(file_info["cid"])
|
467
|
+
except Exception as e:
|
468
|
+
warning(
|
469
|
+
f"Failed to publish file {file_info['name']} globally: {str(e)}"
|
470
|
+
)
|
471
|
+
|
415
472
|
# Complete the progress
|
416
473
|
progress.update(task, completed=100)
|
417
474
|
# Cancel the updater task
|
@@ -450,11 +507,15 @@ async def handle_store_dir(
|
|
450
507
|
["Index", "Filename", "CID"],
|
451
508
|
)
|
452
509
|
|
453
|
-
# If we stored in the marketplace
|
454
|
-
if "transaction_hash" in result:
|
510
|
+
# If publishing is enabled and we stored in the marketplace
|
511
|
+
if publish and "transaction_hash" in result:
|
455
512
|
log(
|
456
513
|
f"\nStored in marketplace. Transaction hash: [bold]{result['transaction_hash']}[/bold]"
|
457
514
|
)
|
515
|
+
elif not publish:
|
516
|
+
log(
|
517
|
+
"\n[yellow]Files were uploaded locally only. No blockchain publication or IPFS pinning.[/yellow]"
|
518
|
+
)
|
458
519
|
|
459
520
|
return 0
|
460
521
|
|
@@ -498,8 +559,7 @@ async def handle_credits(
|
|
498
559
|
"Please provide an account address with '--account_address' or set a default with:"
|
499
560
|
)
|
500
561
|
log(
|
501
|
-
" hippius address set-default <your_account_address>"
|
502
|
-
style="bold blue",
|
562
|
+
" [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
|
503
563
|
)
|
504
564
|
|
505
565
|
return 1
|
@@ -555,7 +615,9 @@ async def handle_files(
|
|
555
615
|
info(
|
556
616
|
"Please provide an account address with '--account_address' or set a default with:"
|
557
617
|
)
|
558
|
-
log(
|
618
|
+
log(
|
619
|
+
" [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
|
620
|
+
)
|
559
621
|
return 1
|
560
622
|
|
561
623
|
# Get files from the marketplace
|
@@ -707,7 +769,9 @@ async def handle_ec_files(
|
|
707
769
|
info(
|
708
770
|
"Please provide an account address with '--account_address' or set a default with:"
|
709
771
|
)
|
710
|
-
log(
|
772
|
+
log(
|
773
|
+
" [bold green underline]hippius address set-default <your_account_address>[/bold green underline]"
|
774
|
+
)
|
711
775
|
return 1
|
712
776
|
|
713
777
|
info(f"Getting erasure-coded files for account: [bold]{account_address}[/bold]")
|
@@ -937,7 +1001,7 @@ async def handle_ec_files(
|
|
937
1001
|
# Include a real example with the first metadata CID
|
938
1002
|
example_cid = metadata_cids[0] if metadata_cids else "<METADATA_CID>"
|
939
1003
|
print_panel(
|
940
|
-
f"hippius reconstruct <METADATA_CID> <OUTPUT_FILENAME
|
1004
|
+
f"[bold green underline]hippius reconstruct <METADATA_CID> <OUTPUT_FILENAME>[/bold green underline]\n\nExample:\n[bold green underline]hippius reconstruct {example_cid} reconstructed_file.bin[/bold green underline]",
|
941
1005
|
title="Reconstruction Command",
|
942
1006
|
)
|
943
1007
|
|
@@ -969,10 +1033,44 @@ async def handle_erasure_code(
|
|
969
1033
|
import zfec
|
970
1034
|
except ImportError:
|
971
1035
|
error("zfec is required for erasure coding")
|
972
|
-
log(
|
973
|
-
|
1036
|
+
log(
|
1037
|
+
"Install it with: [bold green underline]pip install zfec[/bold green underline]"
|
1038
|
+
)
|
1039
|
+
log(
|
1040
|
+
"Then update your environment: [bold green underline]poetry add zfec[/bold green underline]"
|
1041
|
+
)
|
974
1042
|
return 1
|
975
1043
|
|
1044
|
+
# Request password early if we're going to publish to the blockchain
|
1045
|
+
if publish and client.substrate_client._seed_phrase is None:
|
1046
|
+
# First check if we have an encrypted seed phrase that will require a password
|
1047
|
+
config = load_config()
|
1048
|
+
account_name = client.substrate_client._account_name or get_active_account()
|
1049
|
+
|
1050
|
+
if account_name and account_name in config["substrate"].get("accounts", {}):
|
1051
|
+
account_data = config["substrate"]["accounts"][account_name]
|
1052
|
+
is_encoded = account_data.get("seed_phrase_encoded", False)
|
1053
|
+
|
1054
|
+
if is_encoded:
|
1055
|
+
warning("Wallet password will be required for publishing to blockchain")
|
1056
|
+
password = getpass.getpass(
|
1057
|
+
"Enter password to decrypt seed phrase: \n\n"
|
1058
|
+
)
|
1059
|
+
|
1060
|
+
# Store the password in client for later use
|
1061
|
+
client.substrate_client._seed_phrase_password = password
|
1062
|
+
|
1063
|
+
# Pre-authenticate to ensure the password is correct
|
1064
|
+
try:
|
1065
|
+
seed_phrase = decrypt_seed_phrase(password, account_name)
|
1066
|
+
if not seed_phrase:
|
1067
|
+
error("Failed to decrypt seed phrase. Incorrect password?")
|
1068
|
+
return 1
|
1069
|
+
client.substrate_client._seed_phrase = seed_phrase
|
1070
|
+
except Exception as e:
|
1071
|
+
error(f"Error decrypting seed phrase: {e}")
|
1072
|
+
return 1
|
1073
|
+
|
976
1074
|
# Get file size
|
977
1075
|
file_size = os.path.getsize(file_path)
|
978
1076
|
file_name = os.path.basename(file_path)
|
@@ -989,21 +1087,31 @@ async def handle_erasure_code(
|
|
989
1087
|
# Calculate new chunk size to get exactly k chunks
|
990
1088
|
new_chunk_size = file_size / k
|
991
1089
|
|
1090
|
+
new_chunk_size = int(new_chunk_size)
|
1091
|
+
new_chunk_size = max(1, new_chunk_size)
|
1092
|
+
|
992
1093
|
# Create a panel with parameter adjustment information
|
993
1094
|
adjustment_info = [
|
994
|
-
f"Original parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold], chunk size=[bold]{chunk_size/1024/1024:.2f} MB[/bold]",
|
1095
|
+
f"Original parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold], chunk size=[bold]{chunk_size / 1024 / 1024:.2f} MB[/bold]",
|
995
1096
|
f"Would create only [bold red]{potential_chunks:.2f}[/bold red] chunks, which is less than k=[bold]{k}[/bold]",
|
996
|
-
f"Automatically adjusting chunk size to [bold green]{new_chunk_size/1024/1024:.6f} MB[/bold green] to create at least {k} chunks",
|
1097
|
+
f"Automatically adjusting chunk size to [bold green]{new_chunk_size / 1024 / 1024:.6f} MB[/bold green] to create at least {k} chunks",
|
997
1098
|
]
|
998
1099
|
print_panel("\n".join(adjustment_info), title="Parameter Adjustment")
|
999
1100
|
|
1000
1101
|
chunk_size = new_chunk_size
|
1001
1102
|
|
1103
|
+
# Calculate total number of chunks that will be created
|
1104
|
+
total_original_chunks = max(1, int(math.ceil(file_size / chunk_size)))
|
1105
|
+
total_encoded_chunks = total_original_chunks * m
|
1106
|
+
estimated_size_per_chunk = min(chunk_size, file_size / total_original_chunks)
|
1107
|
+
|
1002
1108
|
# Create parameter information panel
|
1003
1109
|
param_info = [
|
1004
|
-
f"File: [bold]{file_name}[/bold] ([bold cyan]{file_size/1024/1024:.2f} MB[/bold cyan])",
|
1110
|
+
f"File: [bold]{file_name}[/bold] ([bold cyan]{file_size / 1024 / 1024:.2f} MB[/bold cyan])",
|
1005
1111
|
f"Parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold] (need {k} of {m} chunks to reconstruct)",
|
1006
|
-
f"Chunk size: [bold cyan]{chunk_size/1024/1024:.6f} MB[/bold cyan]",
|
1112
|
+
f"Chunk size: [bold cyan]{chunk_size / 1024 / 1024:.6f} MB[/bold cyan]",
|
1113
|
+
f"Total chunks to be created: [bold yellow]{total_encoded_chunks}[/bold yellow] ({total_original_chunks} original chunks × {m} encoded chunks each)",
|
1114
|
+
f"Estimated storage required: [bold magenta]{(total_encoded_chunks * estimated_size_per_chunk) / (1024 * 1024):.2f} MB[/bold magenta]",
|
1007
1115
|
]
|
1008
1116
|
|
1009
1117
|
# Add encryption status
|
@@ -1012,6 +1120,16 @@ async def handle_erasure_code(
|
|
1012
1120
|
else:
|
1013
1121
|
param_info.append("[bold yellow]Encryption: Disabled[/bold yellow]")
|
1014
1122
|
|
1123
|
+
# Add publish status
|
1124
|
+
if publish:
|
1125
|
+
param_info.append(
|
1126
|
+
"[bold blue]Publishing: Enabled[/bold blue] (will store on blockchain)"
|
1127
|
+
)
|
1128
|
+
else:
|
1129
|
+
param_info.append(
|
1130
|
+
"[bold cyan]Publishing: Disabled[/bold cyan] (local only, no password needed)"
|
1131
|
+
)
|
1132
|
+
|
1015
1133
|
# Parse miner IDs if provided
|
1016
1134
|
miner_id_list = None
|
1017
1135
|
if miner_ids:
|
@@ -1028,7 +1146,9 @@ async def handle_erasure_code(
|
|
1028
1146
|
# Create progress for the erasure coding operation
|
1029
1147
|
with create_progress() as progress:
|
1030
1148
|
# Add tasks for the different stages
|
1031
|
-
processing_task = progress.add_task(
|
1149
|
+
processing_task = progress.add_task(
|
1150
|
+
"[cyan]Processing file...", total=100, visible=False
|
1151
|
+
)
|
1032
1152
|
encoding_task = progress.add_task(
|
1033
1153
|
"[green]Encoding chunks...", total=100, visible=False
|
1034
1154
|
)
|
@@ -1087,6 +1207,7 @@ async def handle_erasure_code(
|
|
1087
1207
|
max_retries=3,
|
1088
1208
|
verbose=verbose,
|
1089
1209
|
progress_callback=update_progress_bar,
|
1210
|
+
publish=publish,
|
1090
1211
|
)
|
1091
1212
|
|
1092
1213
|
# Complete all progress tasks
|
@@ -1145,7 +1266,7 @@ async def handle_erasure_code(
|
|
1145
1266
|
# Use direct values from input parameters when metadata is not available
|
1146
1267
|
summary_lines.extend(
|
1147
1268
|
[
|
1148
|
-
f"Original file: [bold]{file_name}[/bold] ([bold cyan]{file_size/1024/1024:.2f} MB[/bold cyan])",
|
1269
|
+
f"Original file: [bold]{file_name}[/bold] ([bold cyan]{file_size / 1024 / 1024:.2f} MB[/bold cyan])",
|
1149
1270
|
f"Parameters: k=[bold]{k}[/bold], m=[bold]{m}[/bold]",
|
1150
1271
|
f"Total files stored in marketplace: [bold]{total_files_stored}[/bold]",
|
1151
1272
|
f"Metadata CID: [bold cyan]{metadata_cid}[/bold cyan]",
|
@@ -1156,14 +1277,14 @@ async def handle_erasure_code(
|
|
1156
1277
|
if publish:
|
1157
1278
|
summary_lines.extend(
|
1158
1279
|
[
|
1159
|
-
|
1280
|
+
"Published to global IPFS: [bold green]Yes[/bold green]",
|
1160
1281
|
f"Global access URL: [link]{client.ipfs_client.gateway}/ipfs/{metadata_cid}[/link]",
|
1161
1282
|
]
|
1162
1283
|
)
|
1163
1284
|
else:
|
1164
1285
|
summary_lines.extend(
|
1165
1286
|
[
|
1166
|
-
f"Original file: [bold]{original_file.get('name')}[/bold] ([bold cyan]{original_file.get('size', 0)/1024/1024:.2f} MB[/bold cyan])",
|
1287
|
+
f"Original file: [bold]{original_file.get('name')}[/bold] ([bold cyan]{original_file.get('size', 0) / 1024 / 1024:.2f} MB[/bold cyan])",
|
1167
1288
|
f"File ID: [bold]{erasure_coding.get('file_id')}[/bold]",
|
1168
1289
|
f"Parameters: k=[bold]{erasure_coding.get('k')}[/bold], m=[bold]{erasure_coding.get('m')}[/bold]",
|
1169
1290
|
f"Total chunks: [bold]{len(metadata.get('chunks', []))}[/bold]",
|
@@ -1176,7 +1297,7 @@ async def handle_erasure_code(
|
|
1176
1297
|
if publish:
|
1177
1298
|
summary_lines.extend(
|
1178
1299
|
[
|
1179
|
-
|
1300
|
+
"Published to global IPFS: [bold green]Yes[/bold green]",
|
1180
1301
|
f"Global access URL: [link]{client.ipfs_client.gateway}/ipfs/{metadata_cid}[/link]",
|
1181
1302
|
]
|
1182
1303
|
)
|
@@ -1202,7 +1323,7 @@ async def handle_erasure_code(
|
|
1202
1323
|
f" 2. Access to at least [bold]{k}[/bold] chunks for each original chunk",
|
1203
1324
|
"",
|
1204
1325
|
"Reconstruction command:",
|
1205
|
-
f"[bold]hippius reconstruct {metadata_cid} reconstructed_{output_filename}[/bold]",
|
1326
|
+
f"[bold green underline]hippius reconstruct {metadata_cid} reconstructed_{output_filename}[/bold green underline]",
|
1206
1327
|
]
|
1207
1328
|
|
1208
1329
|
print_panel(
|
@@ -1360,89 +1481,210 @@ async def handle_reconstruct(
|
|
1360
1481
|
|
1361
1482
|
|
1362
1483
|
async def handle_delete(client: HippiusClient, cid: str, force: bool = False) -> int:
|
1363
|
-
"""Handle the delete command"""
|
1364
|
-
info(f"Preparing to delete
|
1484
|
+
"""Handle the delete command for files or directories"""
|
1485
|
+
info(f"Preparing to delete content with CID: [bold cyan]{cid}[/bold cyan]")
|
1486
|
+
|
1487
|
+
# First check if this is a directory
|
1488
|
+
try:
|
1489
|
+
exists_result = await client.exists(cid)
|
1490
|
+
if not exists_result["exists"]:
|
1491
|
+
error(f"CID [bold cyan]{cid}[/bold cyan] not found on IPFS")
|
1492
|
+
return 1
|
1493
|
+
except Exception as e:
|
1494
|
+
warning(f"Error checking if CID exists: {e}")
|
1365
1495
|
|
1366
1496
|
if not force:
|
1367
|
-
warning("This will cancel storage and remove the
|
1497
|
+
warning("This will cancel storage and remove the content from the marketplace.")
|
1368
1498
|
confirm = input("Continue? (y/n): ").strip().lower()
|
1369
1499
|
if confirm != "y":
|
1370
1500
|
log("Deletion cancelled", style="yellow")
|
1371
1501
|
return 0
|
1372
1502
|
|
1373
|
-
|
1374
|
-
|
1503
|
+
# Show spinner during deletion
|
1504
|
+
with console.status("[cyan]Deleting content...[/cyan]", spinner="dots") as status:
|
1505
|
+
result = await client.delete_file(cid)
|
1375
1506
|
|
1376
|
-
|
1377
|
-
|
1507
|
+
# Display results
|
1508
|
+
is_directory = result.get("is_directory", False)
|
1509
|
+
child_files = result.get("child_files", [])
|
1378
1510
|
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
)
|
1384
|
-
|
1385
|
-
# Create an informative panel with notes
|
1386
|
-
notes = [
|
1387
|
-
"1. The file is now unpinned from the marketplace",
|
1388
|
-
"2. The CID may still resolve temporarily until garbage collection occurs",
|
1389
|
-
"3. If the file was published to the global IPFS network, it may still be",
|
1390
|
-
" available through other nodes that pinned it",
|
1511
|
+
if is_directory:
|
1512
|
+
# Directory deletion
|
1513
|
+
details = [
|
1514
|
+
f"Successfully deleted directory: [bold cyan]{cid}[/bold cyan]",
|
1515
|
+
f"Child files unpinned: [bold]{len(child_files)}[/bold]",
|
1391
1516
|
]
|
1392
1517
|
|
1393
|
-
|
1394
|
-
|
1518
|
+
# If there are child files, show them in a table
|
1519
|
+
if child_files:
|
1520
|
+
table_data = []
|
1521
|
+
for i, file in enumerate(
|
1522
|
+
child_files[:10], 1
|
1523
|
+
): # Limit to first 10 files if many
|
1524
|
+
table_data.append(
|
1525
|
+
{
|
1526
|
+
"Index": str(i),
|
1527
|
+
"Filename": file.get("name", "unknown"),
|
1528
|
+
"CID": file.get("cid", "unknown"),
|
1529
|
+
}
|
1530
|
+
)
|
1395
1531
|
|
1396
|
-
|
1532
|
+
if len(child_files) > 10:
|
1533
|
+
table_data.append(
|
1534
|
+
{
|
1535
|
+
"Index": "...",
|
1536
|
+
"Filename": f"({len(child_files) - 10} more files)",
|
1537
|
+
"CID": "...",
|
1538
|
+
}
|
1539
|
+
)
|
1397
1540
|
|
1398
|
-
|
1541
|
+
print_table(
|
1542
|
+
"Unpinned Child Files", table_data, ["Index", "Filename", "CID"]
|
1543
|
+
)
|
1399
1544
|
else:
|
1400
|
-
|
1545
|
+
# Regular file deletion
|
1546
|
+
details = [f"Successfully deleted file: [bold cyan]{cid}[/bold cyan]"]
|
1547
|
+
|
1548
|
+
if "duration_seconds" in result.get("timing", {}):
|
1549
|
+
details.append(
|
1550
|
+
f"Deletion completed in [bold green]{result['timing']['duration_seconds']:.2f}[/bold green] seconds"
|
1551
|
+
)
|
1552
|
+
|
1553
|
+
print_panel("\n".join(details), title="Deletion Complete")
|
1554
|
+
|
1555
|
+
# Create an informative panel with notes
|
1556
|
+
notes = [
|
1557
|
+
"1. The content is now unpinned from the marketplace",
|
1558
|
+
"2. The CID may still resolve temporarily until garbage collection occurs",
|
1559
|
+
"3. If the content was published to the global IPFS network, it may still be",
|
1560
|
+
" available through other nodes that pinned it",
|
1561
|
+
]
|
1562
|
+
|
1563
|
+
print_panel("\n".join(notes), title="Important Notes")
|
1564
|
+
|
1565
|
+
return 0
|
1401
1566
|
|
1402
1567
|
|
1403
1568
|
async def handle_ec_delete(
|
1404
1569
|
client: HippiusClient, metadata_cid: str, force: bool = False
|
1405
1570
|
) -> int:
|
1406
|
-
"""Handle the
|
1407
|
-
|
1408
|
-
|
1409
|
-
)
|
1571
|
+
"""Handle the erasure-code delete command"""
|
1572
|
+
|
1573
|
+
# Create a stylish header with the CID
|
1574
|
+
info(f"Preparing to delete erasure-coded file with metadata CID:")
|
1575
|
+
print_panel(f"[bold cyan]{metadata_cid}[/bold cyan]", title="Metadata CID")
|
1410
1576
|
|
1577
|
+
# Confirm the deletion if not forced
|
1411
1578
|
if not force:
|
1412
|
-
|
1413
|
-
|
1579
|
+
warning_text = [
|
1580
|
+
"This will cancel the storage of this file on the Hippius blockchain.",
|
1581
|
+
"The file metadata will be removed from blockchain storage tracking.",
|
1582
|
+
"[dim]Note: Only the metadata CID will be canceled; contents may remain on IPFS.[/dim]",
|
1583
|
+
]
|
1584
|
+
print_panel("\n".join(warning_text), title="Warning")
|
1585
|
+
|
1586
|
+
confirm = input("Continue with deletion? (y/n): ").strip().lower()
|
1414
1587
|
if confirm != "y":
|
1415
1588
|
log("Deletion cancelled", style="yellow")
|
1416
1589
|
return 0
|
1417
1590
|
|
1418
1591
|
try:
|
1592
|
+
# First, pre-authenticate the client to get any password prompts out of the way
|
1593
|
+
# This accesses the substrate client to trigger authentication
|
1594
|
+
if not client.substrate_client._keypair:
|
1595
|
+
client.substrate_client._ensure_keypair()
|
1596
|
+
|
1597
|
+
# Now we can show the spinner after any password prompts
|
1419
1598
|
info("Deleting erasure-coded file from marketplace...")
|
1420
|
-
result = await client.delete_ec_file(metadata_cid)
|
1421
1599
|
|
1422
|
-
|
1423
|
-
|
1600
|
+
# Create a more detailed spinner with phases
|
1601
|
+
with console.status(
|
1602
|
+
"[cyan]Processing file metadata and chunks...[/cyan]", spinner="dots"
|
1603
|
+
) as status:
|
1604
|
+
try:
|
1605
|
+
# Use the specialized delete method that now throws specific exceptions
|
1606
|
+
await client.delete_ec_file(metadata_cid)
|
1424
1607
|
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1428
|
-
details.append(f"Deleted [bold]{chunks_deleted}[/bold] chunks")
|
1608
|
+
# If we get here, deletion was successful
|
1609
|
+
deletion_success = True
|
1610
|
+
already_deleted = False
|
1429
1611
|
|
1430
|
-
|
1431
|
-
|
1432
|
-
|
1612
|
+
except HippiusAlreadyDeletedError:
|
1613
|
+
# Special case - already deleted
|
1614
|
+
deletion_success = False
|
1615
|
+
already_deleted = True
|
1616
|
+
|
1617
|
+
except HippiusFailedSubstrateDelete as e:
|
1618
|
+
# Blockchain deletion failed
|
1619
|
+
error(f"Blockchain storage cancellation failed: {e}")
|
1620
|
+
return 1
|
1621
|
+
|
1622
|
+
except HippiusFailedIPFSUnpin as e:
|
1623
|
+
# IPFS unpinning failed, but blockchain deletion succeeded
|
1624
|
+
warning(
|
1625
|
+
f"Note: Some IPFS operations failed, but blockchain storage was successfully canceled"
|
1433
1626
|
)
|
1627
|
+
# Consider this a success for the user since the more important blockchain part worked
|
1628
|
+
deletion_success = True
|
1629
|
+
already_deleted = False
|
1630
|
+
|
1631
|
+
except HippiusMetadataError as e:
|
1632
|
+
# Metadata parsing failed, but we can still continue
|
1633
|
+
warning(
|
1634
|
+
f"Note: Metadata file was corrupted, but blockchain storage was successfully canceled"
|
1635
|
+
)
|
1636
|
+
# Consider this a success for the user since the blockchain part worked
|
1637
|
+
deletion_success = True
|
1638
|
+
already_deleted = False
|
1434
1639
|
|
1435
|
-
|
1640
|
+
except Exception as e:
|
1641
|
+
# Handle any unexpected errors
|
1642
|
+
error(f"Unexpected error: {e}")
|
1643
|
+
return 1
|
1436
1644
|
|
1645
|
+
# Show the result
|
1646
|
+
if deletion_success:
|
1647
|
+
# Create a success panel
|
1648
|
+
success_panel = [
|
1649
|
+
"[bold green]✓[/bold green] Metadata CID canceled from blockchain storage",
|
1650
|
+
f"[dim]This file is no longer tracked for storage payments[/dim]",
|
1651
|
+
"",
|
1652
|
+
"[dim]To purge file data completely:[/dim]",
|
1653
|
+
"• Individual chunks may still exist on IPFS and nodes",
|
1654
|
+
"• For complete deletion, all chunks should be unpinned manually",
|
1655
|
+
]
|
1656
|
+
print_panel(
|
1657
|
+
"\n".join(success_panel), title="Storage Cancellation Successful"
|
1658
|
+
)
|
1659
|
+
return 0
|
1660
|
+
elif already_deleted:
|
1661
|
+
# Create a panel for the already deleted case
|
1662
|
+
already_panel = [
|
1663
|
+
"[bold yellow]![/bold yellow] This file has already been deleted from storage",
|
1664
|
+
"[dim]The CID was not found in the blockchain storage registry[/dim]",
|
1665
|
+
"",
|
1666
|
+
"This is expected if:",
|
1667
|
+
"• You previously deleted this file",
|
1668
|
+
"• The file was deleted by another process",
|
1669
|
+
"• The file was never stored in the first place",
|
1670
|
+
]
|
1671
|
+
print_panel("\n".join(already_panel), title="Already Deleted")
|
1672
|
+
# Return 0 since this is not an error condition
|
1437
1673
|
return 0
|
1438
1674
|
else:
|
1439
|
-
error
|
1440
|
-
|
1441
|
-
|
1675
|
+
# Create an error panel for all other failures
|
1676
|
+
error_panel = [
|
1677
|
+
"[bold red]×[/bold red] File not found in blockchain storage",
|
1678
|
+
"[dim]The metadata CID was not found in the blockchain storage registry[/dim]",
|
1679
|
+
"",
|
1680
|
+
"Possible reasons:",
|
1681
|
+
"• The CID may be incorrect",
|
1682
|
+
"• You may not be the owner of this file",
|
1683
|
+
]
|
1684
|
+
print_panel("\n".join(error_panel), title="Storage Cancellation Failed")
|
1442
1685
|
return 1
|
1443
|
-
|
1444
1686
|
except Exception as e:
|
1445
|
-
error(f"
|
1687
|
+
error(f"Error deleting erasure-coded file: {e}")
|
1446
1688
|
return 1
|
1447
1689
|
|
1448
1690
|
|
@@ -1593,67 +1835,116 @@ def handle_seed_phrase_set(
|
|
1593
1835
|
def handle_seed_phrase_encode(account_name: Optional[str] = None) -> int:
|
1594
1836
|
"""Handle the seed encode command"""
|
1595
1837
|
try:
|
1596
|
-
# Check if
|
1597
|
-
|
1838
|
+
# Check if account exists and get its encryption status
|
1839
|
+
config = load_config()
|
1840
|
+
accounts = config.get("substrate", {}).get("accounts", {})
|
1598
1841
|
|
1599
|
-
|
1600
|
-
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1606
|
-
|
1842
|
+
# If account name not specified, use active account
|
1843
|
+
if not account_name:
|
1844
|
+
account_name = config.get("substrate", {}).get("active_account")
|
1845
|
+
if not account_name:
|
1846
|
+
error("No account specified and no active account")
|
1847
|
+
return 1
|
1848
|
+
|
1849
|
+
# Check if the account exists
|
1850
|
+
if account_name not in accounts:
|
1851
|
+
error(f"Account '{account_name}' not found")
|
1607
1852
|
return 1
|
1608
1853
|
|
1609
|
-
#
|
1610
|
-
|
1611
|
-
|
1854
|
+
# Get account details
|
1855
|
+
account = accounts.get(account_name, {})
|
1856
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
1857
|
+
seed_phrase = account.get("seed_phrase")
|
1612
1858
|
|
1613
|
-
if
|
1614
|
-
|
1615
|
-
|
1616
|
-
|
1617
|
-
|
1859
|
+
# Check if we have a seed phrase
|
1860
|
+
if not seed_phrase:
|
1861
|
+
error(f"Account '{account_name}' doesn't have a seed phrase")
|
1862
|
+
info(
|
1863
|
+
f"Set a seed phrase first with: [bold green underline]hippius seed set <seed_phrase> --account {account_name}[/bold green underline]"
|
1864
|
+
)
|
1865
|
+
return 1
|
1618
1866
|
|
1867
|
+
# Check if the seed phrase is already encrypted
|
1619
1868
|
if is_encrypted:
|
1620
|
-
|
1869
|
+
info("Seed phrase is already encrypted")
|
1621
1870
|
confirm = (
|
1622
1871
|
input("Do you want to re-encrypt it with a new password? (y/n): ")
|
1623
1872
|
.strip()
|
1624
1873
|
.lower()
|
1625
1874
|
)
|
1626
1875
|
if confirm != "y":
|
1627
|
-
|
1876
|
+
info("Encryption cancelled")
|
1628
1877
|
return 0
|
1629
1878
|
|
1630
|
-
|
1631
|
-
|
1879
|
+
# Need to decrypt with old password first
|
1880
|
+
old_password = getpass.getpass("Enter your current password to decrypt: ")
|
1881
|
+
decrypted_seed_phrase = decrypt_seed_phrase(old_password, account_name)
|
1882
|
+
|
1883
|
+
if not decrypted_seed_phrase:
|
1884
|
+
error("Unable to decrypt the seed phrase. Incorrect password?")
|
1885
|
+
return 1
|
1886
|
+
|
1887
|
+
# Now we have the decrypted seed phrase
|
1888
|
+
seed_phrase = decrypted_seed_phrase
|
1889
|
+
|
1890
|
+
# Get new password for encryption
|
1891
|
+
info("\nYou are about to encrypt your seed phrase.")
|
1632
1892
|
password = getpass.getpass("Enter a password for encryption: ")
|
1633
1893
|
confirm = getpass.getpass("Confirm password: ")
|
1634
1894
|
|
1635
1895
|
if password != confirm:
|
1636
|
-
|
1896
|
+
error("Passwords do not match")
|
1637
1897
|
return 1
|
1638
1898
|
|
1639
1899
|
if not password:
|
1640
|
-
|
1900
|
+
error("Password cannot be empty for encryption")
|
1641
1901
|
return 1
|
1642
1902
|
|
1643
|
-
#
|
1644
|
-
encrypt_seed_phrase(password, account_name)
|
1903
|
+
# Now encrypt the seed phrase - key fix here passing correct parameters
|
1904
|
+
success = encrypt_seed_phrase(seed_phrase, password, account_name)
|
1645
1905
|
|
1646
|
-
|
1647
|
-
|
1906
|
+
# Security: Clear the plaintext seed phrase from memory
|
1907
|
+
# This is a best-effort approach, as Python's garbage collection may still keep copies
|
1908
|
+
seed_phrase = None
|
1909
|
+
|
1910
|
+
if success:
|
1911
|
+
# Create success panel with encryption information
|
1912
|
+
encryption_info = [
|
1913
|
+
f"Account: [bold]{account_name}[/bold]",
|
1914
|
+
"[bold green]Seed phrase encrypted successfully[/bold green]",
|
1915
|
+
"",
|
1916
|
+
"You will need to provide this password when using the account for:",
|
1917
|
+
" - Pinning files to IPFS",
|
1918
|
+
" - Erasure coding with publishing",
|
1919
|
+
" - Any other blockchain operations",
|
1920
|
+
"",
|
1921
|
+
"[bold yellow underline]Security note:[/bold yellow underline] The original unencrypted seed phrase is NOT stored in the config.",
|
1922
|
+
]
|
1923
|
+
|
1924
|
+
# Try to get the address for display
|
1925
|
+
try:
|
1926
|
+
address = get_account_address(account_name)
|
1927
|
+
if address:
|
1928
|
+
encryption_info.append("")
|
1929
|
+
encryption_info.append(
|
1930
|
+
f"Account address: [bold cyan]{address}[/bold cyan]"
|
1931
|
+
)
|
1932
|
+
except Exception:
|
1933
|
+
pass
|
1934
|
+
|
1935
|
+
print_panel("\n".join(encryption_info), title="Encryption Successful")
|
1936
|
+
return 0
|
1937
|
+
else:
|
1938
|
+
error("Failed to encrypt seed phrase")
|
1939
|
+
return 1
|
1648
1940
|
|
1649
|
-
return 0
|
1650
1941
|
except Exception as e:
|
1651
|
-
|
1942
|
+
error(f"Error encrypting seed phrase: {e}")
|
1652
1943
|
return 1
|
1653
1944
|
|
1654
1945
|
|
1655
1946
|
def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
|
1656
|
-
"""Handle the seed decode command"""
|
1947
|
+
"""Handle the seed decode command - temporarily decrypts and displays the seed phrase"""
|
1657
1948
|
try:
|
1658
1949
|
# Check if seed phrase exists and is encrypted
|
1659
1950
|
config = load_config()
|
@@ -1661,19 +1952,29 @@ def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
|
|
1661
1952
|
|
1662
1953
|
if account_name:
|
1663
1954
|
account = accounts.get(account_name, {})
|
1664
|
-
is_encrypted = account.get("
|
1955
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
1665
1956
|
else:
|
1666
|
-
|
1957
|
+
# Get active account
|
1958
|
+
active_account = config.get("substrate", {}).get("active_account")
|
1959
|
+
if active_account and active_account in accounts:
|
1960
|
+
is_encrypted = accounts[active_account].get(
|
1961
|
+
"seed_phrase_encoded", False
|
1962
|
+
)
|
1963
|
+
else:
|
1964
|
+
# Legacy mode
|
1965
|
+
is_encrypted = config.get("substrate", {}).get(
|
1966
|
+
"seed_phrase_encoded", False
|
1967
|
+
)
|
1667
1968
|
|
1668
1969
|
if not is_encrypted:
|
1669
|
-
|
1970
|
+
info("Seed phrase is not encrypted")
|
1670
1971
|
return 0
|
1671
1972
|
|
1672
1973
|
# Get password for decryption
|
1673
1974
|
password = getpass.getpass("Enter your password to decrypt the seed phrase: ")
|
1674
1975
|
|
1675
1976
|
if not password:
|
1676
|
-
|
1977
|
+
error("Password cannot be empty")
|
1677
1978
|
return 1
|
1678
1979
|
|
1679
1980
|
# Try to decrypt the seed phrase
|
@@ -1681,30 +1982,40 @@ def handle_seed_phrase_decode(account_name: Optional[str] = None) -> int:
|
|
1681
1982
|
seed_phrase = decrypt_seed_phrase(password, account_name)
|
1682
1983
|
|
1683
1984
|
if seed_phrase:
|
1684
|
-
|
1685
|
-
|
1985
|
+
# Create info panel for the decrypted seed phrase
|
1986
|
+
seed_info = [
|
1987
|
+
f"Decrypted seed phrase: [bold yellow]{seed_phrase}[/bold yellow]",
|
1988
|
+
"",
|
1989
|
+
"[bold green underline]NOTE: This is a temporary decryption only. Your seed phrase remains encrypted in the config.[/bold green underline]",
|
1990
|
+
"",
|
1991
|
+
"[bold red underline]SECURITY WARNING:[/bold red underline]",
|
1992
|
+
"- Your seed phrase gives full access to your account funds",
|
1993
|
+
"- Never share it with anyone or store it in an insecure location",
|
1994
|
+
"- Be aware that displaying it on screen could expose it to screen capture",
|
1995
|
+
"- Consider clearing your terminal history after this operation",
|
1996
|
+
]
|
1686
1997
|
|
1687
|
-
|
1688
|
-
|
1689
|
-
|
1690
|
-
|
1691
|
-
|
1998
|
+
print_panel("\n".join(seed_info), title="Seed Phrase Decoded")
|
1999
|
+
|
2000
|
+
# Security: Clear the plaintext seed phrase from memory
|
2001
|
+
# This is a best-effort approach, as Python's garbage collection may still keep copies
|
2002
|
+
seed_phrase = None
|
1692
2003
|
|
1693
2004
|
return 0
|
1694
2005
|
else:
|
1695
|
-
|
2006
|
+
error("Failed to decrypt seed phrase")
|
1696
2007
|
return 1
|
1697
2008
|
|
1698
2009
|
except Exception as e:
|
1699
|
-
|
2010
|
+
error(f"Error decrypting seed phrase: {e}")
|
1700
2011
|
|
1701
2012
|
if "decryption failed" in str(e).lower():
|
1702
|
-
|
2013
|
+
warning("Incorrect password")
|
1703
2014
|
|
1704
2015
|
return 1
|
1705
2016
|
|
1706
2017
|
except Exception as e:
|
1707
|
-
|
2018
|
+
error(f"{e}")
|
1708
2019
|
return 1
|
1709
2020
|
|
1710
2021
|
|
@@ -1725,7 +2036,7 @@ def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
|
|
1725
2036
|
|
1726
2037
|
account = accounts[account_name]
|
1727
2038
|
has_seed = "seed_phrase" in account
|
1728
|
-
is_encrypted = account.get("
|
2039
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
1729
2040
|
is_active = account_name == get_active_account()
|
1730
2041
|
|
1731
2042
|
print("\nAccount Status:")
|
@@ -1746,31 +2057,39 @@ def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
|
|
1746
2057
|
print(f" Address: Unable to derive (Error: {e})")
|
1747
2058
|
|
1748
2059
|
else:
|
1749
|
-
print("Checking
|
1750
|
-
|
1751
|
-
# Check default seed phrase
|
1752
|
-
has_seed = "seed_phrase" in config.get("substrate", {})
|
1753
|
-
is_encrypted = config.get("substrate", {}).get("encrypted", False)
|
1754
|
-
|
1755
|
-
print("\nDefault Seed Phrase Status:")
|
1756
|
-
print(f" Has Seed Phrase: {'Yes' if has_seed else 'No'}")
|
1757
|
-
print(f" Encrypted: {'Yes' if is_encrypted else 'No'}")
|
1758
|
-
|
1759
|
-
if has_seed:
|
1760
|
-
try:
|
1761
|
-
# Try to get the address (will use cached if available)
|
1762
|
-
address = get_account_address()
|
1763
|
-
print(f" Address: {address}")
|
1764
|
-
except Exception as e:
|
1765
|
-
if is_encrypted:
|
1766
|
-
print(" Address: Encrypted (password required to view)")
|
1767
|
-
else:
|
1768
|
-
print(f" Address: Unable to derive (Error: {e})")
|
2060
|
+
print("Checking active account seed phrase status")
|
1769
2061
|
|
1770
|
-
#
|
2062
|
+
# Get the active account
|
1771
2063
|
active_account = get_active_account()
|
1772
2064
|
if active_account:
|
1773
|
-
|
2065
|
+
accounts = config.get("substrate", {}).get("accounts", {})
|
2066
|
+
if active_account in accounts:
|
2067
|
+
account = accounts[active_account]
|
2068
|
+
has_seed = "seed_phrase" in account
|
2069
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
2070
|
+
|
2071
|
+
print(f"\nActive Account: {active_account}")
|
2072
|
+
print(f" Has Seed Phrase: {'Yes' if has_seed else 'No'}")
|
2073
|
+
print(f" Encrypted: {'Yes' if is_encrypted else 'No'}")
|
2074
|
+
|
2075
|
+
if has_seed:
|
2076
|
+
try:
|
2077
|
+
# Try to get the address (will use cached if available)
|
2078
|
+
address = get_account_address(active_account)
|
2079
|
+
print(f" Address: {address}")
|
2080
|
+
except Exception as e:
|
2081
|
+
if is_encrypted:
|
2082
|
+
print(
|
2083
|
+
" Address: Encrypted (password required to view)"
|
2084
|
+
)
|
2085
|
+
else:
|
2086
|
+
print(f" Address: Unable to derive (Error: {e})")
|
2087
|
+
else:
|
2088
|
+
print(
|
2089
|
+
f"\nActive account '{active_account}' not found in configuration"
|
2090
|
+
)
|
2091
|
+
else:
|
2092
|
+
print("\nNo active account set")
|
1774
2093
|
|
1775
2094
|
return 0
|
1776
2095
|
|
@@ -1784,6 +2103,88 @@ def handle_seed_phrase_status(account_name: Optional[str] = None) -> int:
|
|
1784
2103
|
#
|
1785
2104
|
|
1786
2105
|
|
2106
|
+
def handle_account_info(account_name: Optional[str] = None) -> int:
|
2107
|
+
"""Handle the account info command - displays detailed information about an account"""
|
2108
|
+
try:
|
2109
|
+
# Load configuration
|
2110
|
+
config = load_config()
|
2111
|
+
|
2112
|
+
# If account name not specified, use active account
|
2113
|
+
if not account_name:
|
2114
|
+
account_name = config.get("substrate", {}).get("active_account")
|
2115
|
+
if not account_name:
|
2116
|
+
error("No account specified and no active account")
|
2117
|
+
return 1
|
2118
|
+
|
2119
|
+
# Check if account exists
|
2120
|
+
accounts = config.get("substrate", {}).get("accounts", {})
|
2121
|
+
if account_name not in accounts:
|
2122
|
+
error(f"Account '{account_name}' not found")
|
2123
|
+
return 1
|
2124
|
+
|
2125
|
+
# Get account details
|
2126
|
+
account = accounts[account_name]
|
2127
|
+
has_seed = "seed_phrase" in account
|
2128
|
+
is_encrypted = account.get("seed_phrase_encoded", False)
|
2129
|
+
is_active = account_name == get_active_account()
|
2130
|
+
ss58_address = account.get("ss58_address", "")
|
2131
|
+
|
2132
|
+
# Account information panel with rich formatting
|
2133
|
+
account_info = [
|
2134
|
+
f"Account Name: [bold]{account_name}[/bold]",
|
2135
|
+
f"Active: [bold cyan]{'Yes' if is_active else 'No'}[/bold cyan]",
|
2136
|
+
f"Has Seed Phrase: [bold]{'Yes' if has_seed else 'No'}[/bold]",
|
2137
|
+
f"Encryption: [bold {'green' if is_encrypted else 'yellow'}]{'Encrypted' if is_encrypted else 'Unencrypted'}[/bold {'green' if is_encrypted else 'yellow'}]",
|
2138
|
+
]
|
2139
|
+
|
2140
|
+
if ss58_address:
|
2141
|
+
account_info.append(f"SS58 Address: [bold cyan]{ss58_address}[/bold cyan]")
|
2142
|
+
elif has_seed:
|
2143
|
+
if is_encrypted:
|
2144
|
+
account_info.append(
|
2145
|
+
"[dim]Address: Encrypted (password required to view)[/dim]"
|
2146
|
+
)
|
2147
|
+
else:
|
2148
|
+
try:
|
2149
|
+
# Try to get the address
|
2150
|
+
address = get_account_address(account_name)
|
2151
|
+
account_info.append(
|
2152
|
+
f"SS58 Address: [bold cyan]{address}[/bold cyan]"
|
2153
|
+
)
|
2154
|
+
except Exception as e:
|
2155
|
+
account_info.append(
|
2156
|
+
f"[yellow]Unable to derive address: {e}[/yellow]"
|
2157
|
+
)
|
2158
|
+
|
2159
|
+
# Add suggestions based on account status
|
2160
|
+
account_info.append("")
|
2161
|
+
if is_active:
|
2162
|
+
account_info.append("[bold green]This is your active account[/bold green]")
|
2163
|
+
else:
|
2164
|
+
account_info.append(
|
2165
|
+
f"[dim]To use this account: [bold green underline]hippius account switch {account_name}[/bold green underline][/dim]"
|
2166
|
+
)
|
2167
|
+
|
2168
|
+
if has_seed and not is_encrypted:
|
2169
|
+
account_info.append(
|
2170
|
+
f"[bold yellow underline]WARNING:[/bold yellow underline] Seed phrase is not encrypted"
|
2171
|
+
)
|
2172
|
+
account_info.append(
|
2173
|
+
f"[dim]To encrypt: [bold green underline]hippius account encode --name {account_name}[/bold green underline][/dim]"
|
2174
|
+
)
|
2175
|
+
|
2176
|
+
# Print the panel with rich formatting
|
2177
|
+
print_panel(
|
2178
|
+
"\n".join(account_info), title=f"Account Information: {account_name}"
|
2179
|
+
)
|
2180
|
+
|
2181
|
+
return 0
|
2182
|
+
|
2183
|
+
except Exception as e:
|
2184
|
+
error(f"Error getting account info: {e}")
|
2185
|
+
return 1
|
2186
|
+
|
2187
|
+
|
1787
2188
|
def handle_account_create(
|
1788
2189
|
client: HippiusClient, name: str, encrypt: bool = False
|
1789
2190
|
) -> int:
|
@@ -1797,6 +2198,9 @@ def handle_account_create(
|
|
1797
2198
|
|
1798
2199
|
print(f"Creating new account: {name}")
|
1799
2200
|
|
2201
|
+
# Import Keypair at the beginning to ensure it's available
|
2202
|
+
from substrateinterface import Keypair
|
2203
|
+
|
1800
2204
|
# Generate a new keypair (seed phrase)
|
1801
2205
|
seed_phrase = client.substrate_client.generate_seed_phrase()
|
1802
2206
|
|
@@ -1820,56 +2224,70 @@ def handle_account_create(
|
|
1820
2224
|
return 1
|
1821
2225
|
|
1822
2226
|
# Set the seed phrase for the new account
|
1823
|
-
|
2227
|
+
# First load the config to directly edit it
|
2228
|
+
config = load_config()
|
1824
2229
|
|
1825
|
-
#
|
1826
|
-
|
2230
|
+
# Ensure accounts structure exists
|
2231
|
+
if "accounts" not in config["substrate"]:
|
2232
|
+
config["substrate"]["accounts"] = {}
|
1827
2233
|
|
1828
|
-
#
|
1829
|
-
|
1830
|
-
|
1831
|
-
# Create a temporary client with the password to get the address
|
1832
|
-
temp_client = HippiusClient(
|
1833
|
-
substrate_seed_phrase=seed_phrase,
|
1834
|
-
seed_phrase_password=password,
|
1835
|
-
account_name=name,
|
1836
|
-
)
|
1837
|
-
address = temp_client.substrate_client.get_account_address()
|
1838
|
-
else:
|
1839
|
-
address = get_account_address(name)
|
2234
|
+
# Create keypair directly from seed phrase
|
2235
|
+
keypair = Keypair.create_from_mnemonic(seed_phrase)
|
2236
|
+
address = keypair.ss58_address
|
1840
2237
|
|
1841
|
-
|
1842
|
-
|
1843
|
-
|
1844
|
-
|
1845
|
-
|
1846
|
-
|
2238
|
+
# Add the new account
|
2239
|
+
config["substrate"]["accounts"][name] = {
|
2240
|
+
"seed_phrase": seed_phrase,
|
2241
|
+
"seed_phrase_encoded": False,
|
2242
|
+
"seed_phrase_salt": None,
|
2243
|
+
"ss58_address": address,
|
2244
|
+
}
|
1847
2245
|
|
1848
|
-
|
1849
|
-
|
1850
|
-
print("\nYour seed phrase is encrypted.")
|
1851
|
-
print(
|
1852
|
-
"You'll need to provide the password whenever using this account."
|
1853
|
-
)
|
1854
|
-
else:
|
1855
|
-
print("\nWARNING: Your seed phrase is stored unencrypted.")
|
1856
|
-
print(
|
1857
|
-
"Consider encrypting it with: hippius account encode --name", name
|
1858
|
-
)
|
2246
|
+
# Set as active account
|
2247
|
+
config["substrate"]["active_account"] = name
|
1859
2248
|
|
1860
|
-
|
2249
|
+
# Save the config
|
2250
|
+
save_config(config)
|
1861
2251
|
|
1862
|
-
|
2252
|
+
# Print account information using rich formatting
|
2253
|
+
account_info = [
|
2254
|
+
f"Account: [bold]{name}[/bold]",
|
2255
|
+
f"Address: [bold cyan]{address}[/bold cyan]",
|
2256
|
+
f"Seed phrase: [bold yellow]{seed_phrase}[/bold yellow]",
|
2257
|
+
"",
|
2258
|
+
"[bold red underline]IMPORTANT:[/bold red underline] Keep your seed phrase safe. It's the only way to recover your account!",
|
2259
|
+
]
|
1863
2260
|
|
1864
|
-
|
1865
|
-
|
1866
|
-
|
1867
|
-
|
2261
|
+
# Add encryption status
|
2262
|
+
if encrypt:
|
2263
|
+
account_info.append("")
|
2264
|
+
account_info.append(
|
2265
|
+
"[bold green]Your seed phrase is encrypted.[/bold green]"
|
1868
2266
|
)
|
1869
|
-
|
2267
|
+
account_info.append(
|
2268
|
+
"You'll need to provide the password whenever using this account."
|
2269
|
+
)
|
2270
|
+
else:
|
2271
|
+
account_info.append("")
|
2272
|
+
account_info.append(
|
2273
|
+
"[bold yellow underline]WARNING:[/bold yellow underline] Your seed phrase is stored unencrypted."
|
2274
|
+
)
|
2275
|
+
account_info.append(
|
2276
|
+
f"[bold green underline]Consider encrypting it with: hippius account encode --name {name}[/bold green underline]"
|
2277
|
+
)
|
2278
|
+
|
2279
|
+
account_info.append("")
|
2280
|
+
account_info.append(
|
2281
|
+
"This account is now active. Use it with: [bold]hippius <command>[/bold]"
|
2282
|
+
)
|
2283
|
+
|
2284
|
+
# Print the panel with rich formatting
|
2285
|
+
print_panel("\n".join(account_info), title="Account Created Successfully")
|
2286
|
+
|
2287
|
+
return 0
|
1870
2288
|
|
1871
2289
|
except Exception as e:
|
1872
|
-
|
2290
|
+
error(f"Error creating account: {e}")
|
1873
2291
|
return 1
|
1874
2292
|
|
1875
2293
|
|
@@ -2105,7 +2523,7 @@ def handle_account_list() -> int:
|
|
2105
2523
|
|
2106
2524
|
is_active = account_name == active_account
|
2107
2525
|
has_seed = "seed_phrase" in account_data
|
2108
|
-
is_encrypted = account_data.get("
|
2526
|
+
is_encrypted = account_data.get("seed_phrase_encoded", False)
|
2109
2527
|
|
2110
2528
|
# Get address
|
2111
2529
|
address = account_data.get("ss58_address", "")
|
@@ -2136,8 +2554,8 @@ def handle_account_list() -> int:
|
|
2136
2554
|
|
2137
2555
|
# Instructions
|
2138
2556
|
help_text = [
|
2139
|
-
"To switch accounts: [bold]hippius account switch <account_name>[/bold]",
|
2140
|
-
"To create a new account: [bold]hippius account create --name <account_name>[/bold]",
|
2557
|
+
"To switch accounts: [bold green underline]hippius account switch <account_name>[/bold green underline]",
|
2558
|
+
"To create a new account: [bold green underline]hippius account create --name <account_name>[/bold green underline]",
|
2141
2559
|
]
|
2142
2560
|
print_panel("\n".join(help_text), title="Account Management")
|
2143
2561
|
|
@@ -2188,6 +2606,199 @@ def handle_account_switch(account_name: str) -> int:
|
|
2188
2606
|
return 1
|
2189
2607
|
|
2190
2608
|
|
2609
|
+
def handle_account_login() -> int:
|
2610
|
+
"""Handle the account login command - prompts for account details and creates an account"""
|
2611
|
+
try:
|
2612
|
+
# Display the login banner
|
2613
|
+
from hippius_sdk.cli_assets import LOGIN_ASSET
|
2614
|
+
|
2615
|
+
console.print(LOGIN_ASSET, style="bold cyan")
|
2616
|
+
console.print(
|
2617
|
+
"\n[bold blue]Welcome to Hippius![/bold blue] Let's set up your account.\n"
|
2618
|
+
)
|
2619
|
+
|
2620
|
+
# Create a style for prompts
|
2621
|
+
prompt_style = "bold green"
|
2622
|
+
input_style = "bold cyan"
|
2623
|
+
|
2624
|
+
# Prompt for account name with nice formatting
|
2625
|
+
console.print(
|
2626
|
+
"[bold]Step 1:[/bold] Choose a name for your account", style=prompt_style
|
2627
|
+
)
|
2628
|
+
console.print(
|
2629
|
+
"This name will be used to identify your account in the Hippius system.",
|
2630
|
+
style="dim",
|
2631
|
+
)
|
2632
|
+
console.print("Account name:", style=input_style, end=" ")
|
2633
|
+
name = input().strip()
|
2634
|
+
|
2635
|
+
if not name:
|
2636
|
+
error("[bold red]Account name cannot be empty[/bold red]")
|
2637
|
+
return 1
|
2638
|
+
|
2639
|
+
# Check if account already exists
|
2640
|
+
accounts = list_accounts()
|
2641
|
+
if name in accounts:
|
2642
|
+
warning(f"Account '[bold]{name}[/bold]' already exists")
|
2643
|
+
console.print(
|
2644
|
+
"Do you want to overwrite it? (y/n):", style=input_style, end=" "
|
2645
|
+
)
|
2646
|
+
confirm = input().strip().lower()
|
2647
|
+
if confirm != "y":
|
2648
|
+
info("Login cancelled")
|
2649
|
+
return 0
|
2650
|
+
|
2651
|
+
# Prompt for seed phrase with detailed explanation
|
2652
|
+
console.print(
|
2653
|
+
"\n[bold]Step 2:[/bold] Enter your seed phrase", style=prompt_style
|
2654
|
+
)
|
2655
|
+
console.print(
|
2656
|
+
"Your seed phrase gives access to your blockchain account and funds.",
|
2657
|
+
style="dim",
|
2658
|
+
)
|
2659
|
+
console.print(
|
2660
|
+
"[yellow]Important:[/yellow] Must be 12 or 24 words separated by spaces.",
|
2661
|
+
style="dim",
|
2662
|
+
)
|
2663
|
+
console.print("Seed phrase:", style=input_style, end=" ")
|
2664
|
+
seed_phrase = input().strip()
|
2665
|
+
|
2666
|
+
# Validate the seed phrase
|
2667
|
+
if not seed_phrase or len(seed_phrase.split()) not in [12, 24]:
|
2668
|
+
error(
|
2669
|
+
"[bold red]Invalid seed phrase[/bold red] - must be 12 or 24 words separated by spaces"
|
2670
|
+
)
|
2671
|
+
return 1
|
2672
|
+
|
2673
|
+
# Prompt for encryption with security explanation
|
2674
|
+
console.print("\n[bold]Step 3:[/bold] Secure your account", style=prompt_style)
|
2675
|
+
console.print(
|
2676
|
+
"Encrypting your seed phrase adds an extra layer of security.", style="dim"
|
2677
|
+
)
|
2678
|
+
console.print(
|
2679
|
+
"[bold yellow]Strongly recommended[/bold yellow] to protect your account.",
|
2680
|
+
style="dim",
|
2681
|
+
)
|
2682
|
+
console.print(
|
2683
|
+
"Encrypt seed phrase? [bold green](Y/n)[/bold green]:",
|
2684
|
+
style=input_style,
|
2685
|
+
end=" ",
|
2686
|
+
)
|
2687
|
+
encrypt_input = input().strip().lower()
|
2688
|
+
encrypt = encrypt_input == "y" or encrypt_input == "" or encrypt_input == "yes"
|
2689
|
+
|
2690
|
+
# Set up encryption if requested
|
2691
|
+
password = None
|
2692
|
+
if encrypt:
|
2693
|
+
console.print(
|
2694
|
+
"\n[bold]Step 4:[/bold] Set encryption password", style=prompt_style
|
2695
|
+
)
|
2696
|
+
console.print(
|
2697
|
+
"This password will be required whenever you use your account for blockchain operations.",
|
2698
|
+
style="dim",
|
2699
|
+
)
|
2700
|
+
|
2701
|
+
password = getpass.getpass("Enter a password: ")
|
2702
|
+
confirm = getpass.getpass("Confirm password: ")
|
2703
|
+
|
2704
|
+
if password != confirm:
|
2705
|
+
error("[bold red]Passwords do not match[/bold red]")
|
2706
|
+
return 1
|
2707
|
+
|
2708
|
+
if not password:
|
2709
|
+
error("[bold red]Password cannot be empty for encryption[/bold red]")
|
2710
|
+
return 1
|
2711
|
+
|
2712
|
+
# Initialize address variable
|
2713
|
+
address = None
|
2714
|
+
|
2715
|
+
# Create and store the account
|
2716
|
+
with console.status("[cyan]Setting up your account...[/cyan]", spinner="dots"):
|
2717
|
+
# First, directly modify the config to ensure account is created
|
2718
|
+
config = load_config()
|
2719
|
+
|
2720
|
+
# Ensure accounts structure exists
|
2721
|
+
if "substrate" not in config:
|
2722
|
+
config["substrate"] = {}
|
2723
|
+
if "accounts" not in config["substrate"]:
|
2724
|
+
config["substrate"]["accounts"] = {}
|
2725
|
+
|
2726
|
+
# Create keypair and get address from seed phrase
|
2727
|
+
from substrateinterface import Keypair
|
2728
|
+
|
2729
|
+
keypair = Keypair.create_from_mnemonic(seed_phrase)
|
2730
|
+
address = keypair.ss58_address
|
2731
|
+
|
2732
|
+
# Add the new account
|
2733
|
+
config["substrate"]["accounts"][name] = {
|
2734
|
+
"seed_phrase": seed_phrase,
|
2735
|
+
"seed_phrase_encoded": False,
|
2736
|
+
"seed_phrase_salt": None,
|
2737
|
+
"ss58_address": address,
|
2738
|
+
}
|
2739
|
+
|
2740
|
+
# Set as active account
|
2741
|
+
config["substrate"]["active_account"] = name
|
2742
|
+
|
2743
|
+
# Save the config first
|
2744
|
+
save_config(config)
|
2745
|
+
|
2746
|
+
# Now encrypt if requested
|
2747
|
+
if encrypt:
|
2748
|
+
encrypt_seed_phrase(seed_phrase, password, name)
|
2749
|
+
|
2750
|
+
time.sleep(0.5) # Small delay for visual feedback
|
2751
|
+
|
2752
|
+
# Success panel with account information
|
2753
|
+
account_info = [
|
2754
|
+
f"[bold]Account Name:[/bold] [bold magenta]{name}[/bold magenta]",
|
2755
|
+
f"[bold]Blockchain Address:[/bold] [bold cyan]{address}[/bold cyan]",
|
2756
|
+
"",
|
2757
|
+
"[bold green]✓ Login successful![/bold green]",
|
2758
|
+
"[bold green]✓ Account set as active[/bold green]",
|
2759
|
+
]
|
2760
|
+
|
2761
|
+
if encrypt:
|
2762
|
+
account_info.append("[bold green]✓ Seed phrase encrypted[/bold green]")
|
2763
|
+
account_info.append("")
|
2764
|
+
account_info.append(
|
2765
|
+
"[dim]You'll need your password when using this account for blockchain operations.[/dim]"
|
2766
|
+
)
|
2767
|
+
else:
|
2768
|
+
account_info.append(
|
2769
|
+
"[bold yellow]⚠ Seed phrase not encrypted[/bold yellow]"
|
2770
|
+
)
|
2771
|
+
account_info.append("")
|
2772
|
+
account_info.append(
|
2773
|
+
"[dim]For better security, consider encrypting your seed phrase:[/dim]"
|
2774
|
+
)
|
2775
|
+
account_info.append(
|
2776
|
+
f"[dim] [bold green underline]hippius account encode --name {name}[/bold green underline][/dim]"
|
2777
|
+
)
|
2778
|
+
|
2779
|
+
# Add next steps
|
2780
|
+
account_info.append("")
|
2781
|
+
account_info.append("[bold blue]Next steps:[/bold blue]")
|
2782
|
+
account_info.append(
|
2783
|
+
"• [bold green underline]hippius credits[/bold green underline] - Check your account balance"
|
2784
|
+
)
|
2785
|
+
account_info.append(
|
2786
|
+
"• [bold green underline]hippius files[/bold green underline] - View your stored files"
|
2787
|
+
)
|
2788
|
+
account_info.append(
|
2789
|
+
"• [bold green underline]hippius store <file>[/bold green underline] - Upload a file to IPFS"
|
2790
|
+
)
|
2791
|
+
|
2792
|
+
print_panel(
|
2793
|
+
"\n".join(account_info), title="[bold green]Account Ready[/bold green]"
|
2794
|
+
)
|
2795
|
+
return 0
|
2796
|
+
|
2797
|
+
except Exception as e:
|
2798
|
+
error(f"[bold red]Error logging in:[/bold red] {e}")
|
2799
|
+
return 1
|
2800
|
+
|
2801
|
+
|
2191
2802
|
def handle_account_delete(account_name: str) -> int:
|
2192
2803
|
"""Handle the account delete command"""
|
2193
2804
|
try:
|