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