hippius 0.1.0__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.
@@ -0,0 +1,688 @@
1
+ """
2
+ Substrate operations for the Hippius SDK.
3
+
4
+ Note: This functionality is coming soon and not implemented yet.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ from typing import Dict, Any, Optional, List, Union
10
+ from substrateinterface import SubstrateInterface, Keypair
11
+ from dotenv import load_dotenv
12
+
13
+ # Load environment variables
14
+ load_dotenv()
15
+
16
+
17
+ class FileInput:
18
+ """File input for storage requests"""
19
+
20
+ def __init__(self, file_hash: str, file_name: str):
21
+ """
22
+ Initialize a file input
23
+
24
+ Args:
25
+ file_hash: IPFS hash (CID) of the file
26
+ file_name: Name of the file
27
+ """
28
+ self.file_hash = file_hash
29
+ self.file_name = file_name
30
+
31
+ def to_dict(self) -> Dict[str, str]:
32
+ """Convert to dictionary representation"""
33
+ return {"fileHash": self.file_hash, "fileName": self.file_name}
34
+
35
+
36
+ class SubstrateClient:
37
+ """
38
+ Client for interacting with the Hippius Substrate blockchain.
39
+
40
+ Note: This functionality is not fully implemented yet and is under active development.
41
+ """
42
+
43
+ def __init__(self, url: str = None, seed_phrase: Optional[str] = None):
44
+ """
45
+ Initialize the Substrate client.
46
+
47
+ Args:
48
+ url: WebSocket URL of the Hippius substrate node
49
+ If not provided, uses SUBSTRATE_URL from environment
50
+ seed_phrase: Seed phrase for the account (mnemonic)
51
+ If not provided, uses SUBSTRATE_SEED_PHRASE from environment
52
+ """
53
+ if not url:
54
+ url = os.getenv("SUBSTRATE_URL", "wss://rpc.hippius.network")
55
+
56
+ # Store URL and initialize variables
57
+ self.url = url
58
+ self._substrate = None
59
+ self._keypair = None
60
+
61
+ # Set seed phrase if provided or available in environment
62
+ if seed_phrase:
63
+ self.set_seed_phrase(seed_phrase)
64
+ elif os.getenv("SUBSTRATE_SEED_PHRASE"):
65
+ self.set_seed_phrase(os.getenv("SUBSTRATE_SEED_PHRASE"))
66
+
67
+ # Don't connect immediately to avoid exceptions during initialization
68
+ # Connection will happen lazily when needed
69
+
70
+ def connect(self) -> None:
71
+ """
72
+ Connect to the Substrate node.
73
+
74
+ Initializes the connection to the Substrate node and creates a keypair from the seed phrase.
75
+ """
76
+ try:
77
+ print(f"Connecting to Substrate node at {self.url}...")
78
+ self._substrate = SubstrateInterface(
79
+ url=self.url,
80
+ ss58_format=42, # Substrate default
81
+ type_registry_preset="substrate-node-template",
82
+ )
83
+
84
+ # Only create keypair if seed phrase is available
85
+ if hasattr(self, "_seed_phrase") and self._seed_phrase:
86
+ self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
87
+ print(
88
+ f"Connected successfully. Account address: {self._keypair.ss58_address}"
89
+ )
90
+ else:
91
+ print("Connected successfully (read-only mode, no keypair)")
92
+
93
+ return True
94
+
95
+ except Exception as e:
96
+ print(f"Failed to connect to Substrate node: {e}")
97
+ raise ConnectionError(
98
+ f"Could not connect to Substrate node at {self.url}: {e}"
99
+ )
100
+
101
+ return False
102
+
103
+ def set_seed_phrase(self, seed_phrase: str) -> None:
104
+ """
105
+ Set or update the seed phrase used for signing transactions.
106
+
107
+ Args:
108
+ seed_phrase: Mnemonic seed phrase for the account
109
+ """
110
+ if not seed_phrase or not seed_phrase.strip():
111
+ raise ValueError("Seed phrase cannot be empty")
112
+
113
+ # Store the seed phrase
114
+ self._seed_phrase = seed_phrase.strip()
115
+
116
+ # Try to create the keypair if possible
117
+ try:
118
+ if hasattr(self, "_substrate") and self._substrate:
119
+ # If we already have a connection, create the keypair
120
+ self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
121
+ print(f"Keypair created for account: {self._keypair.ss58_address}")
122
+ else:
123
+ print(f"Seed phrase set (keypair will be created when connecting)")
124
+ except Exception as e:
125
+ print(f"Warning: Could not create keypair from seed phrase: {e}")
126
+ print(f"Keypair will be created when needed")
127
+
128
+ def storage_request(
129
+ self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
130
+ ) -> str:
131
+ """
132
+ Submit a storage request for IPFS files to the marketplace.
133
+
134
+ This method batches all files into a single transaction to efficiently store
135
+ multiple files at once.
136
+
137
+ Args:
138
+ files: List of FileInput objects or dictionaries with fileHash and fileName
139
+ miner_ids: List of miner IDs to store the files (optional)
140
+
141
+ Returns:
142
+ str: Transaction hash
143
+
144
+ Example:
145
+ >>> client.storage_request([
146
+ ... FileInput("QmHash1", "file1.txt"),
147
+ ... FileInput("QmHash2", "file2.jpg")
148
+ ... ])
149
+ """
150
+ # Convert any dict inputs to FileInput objects
151
+ file_inputs = []
152
+ for file in files:
153
+ if isinstance(file, dict):
154
+ file_inputs.append(
155
+ FileInput(
156
+ file_hash=file.get("fileHash") or file.get("cid"),
157
+ file_name=file.get("fileName")
158
+ or file.get("filename")
159
+ or "unknown",
160
+ )
161
+ )
162
+ else:
163
+ file_inputs.append(file)
164
+
165
+ # Print what is being submitted
166
+ print(f"Submitting storage request for {len(file_inputs)} files as a batch:")
167
+ for file in file_inputs:
168
+ print(f" - {file.file_name}: {file.file_hash}")
169
+
170
+ if miner_ids:
171
+ print(f"Targeted miners: {', '.join(miner_ids)}")
172
+ else:
173
+ print("No specific miners targeted (using default selection)")
174
+
175
+ try:
176
+ # Initialize Substrate connection
177
+ if not hasattr(self, "_substrate") or self._substrate is None:
178
+ print("Initializing Substrate connection...")
179
+ self._substrate = SubstrateInterface(
180
+ url=self.url,
181
+ ss58_format=42, # Substrate default
182
+ type_registry_preset="substrate-node-template",
183
+ )
184
+ print(f"Connected to Substrate node at {self.url}")
185
+
186
+ # Create keypair from seed phrase if not already created
187
+ if not hasattr(self, "_keypair") or self._keypair is None:
188
+ if not hasattr(self, "_seed_phrase") or not self._seed_phrase:
189
+ raise ValueError(
190
+ "Seed phrase must be set before making transactions"
191
+ )
192
+
193
+ print("Creating keypair from seed phrase...")
194
+ self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
195
+ print(f"Keypair created for address: {self._keypair.ss58_address}")
196
+
197
+ # Prepare storage request call
198
+ print("Preparing marketplace.storageRequest batch call...")
199
+
200
+ # Format files for the batch call - all files are included in a single array
201
+ formatted_files = []
202
+ for file_input in file_inputs:
203
+ formatted_files.append(
204
+ {
205
+ "file_hash": file_input.file_hash,
206
+ "file_name": file_input.file_name,
207
+ }
208
+ )
209
+
210
+ # Create call parameters with all files in a single batch
211
+ call_params = {
212
+ "files_input": formatted_files,
213
+ "miner_ids": miner_ids
214
+ if miner_ids
215
+ else [], # Always include miner_ids, empty array if not specified
216
+ }
217
+
218
+ # Create the call to the marketplace
219
+ print(f"Call parameters: {json.dumps(call_params, indent=2)}")
220
+ call = self._substrate.compose_call(
221
+ call_module="Marketplace",
222
+ call_function="storage_request",
223
+ call_params=call_params,
224
+ )
225
+
226
+ # Get payment info to estimate the fee
227
+ payment_info = self._substrate.get_payment_info(
228
+ call=call, keypair=self._keypair
229
+ )
230
+
231
+ estimated_fee = payment_info.get("partialFee", 0)
232
+ print(f"Estimated transaction fee: {estimated_fee}")
233
+
234
+ # Create a signed extrinsic
235
+ extrinsic = self._substrate.create_signed_extrinsic(
236
+ call=call, keypair=self._keypair
237
+ )
238
+
239
+ print(f"Submitting batch transaction for {len(formatted_files)} files...")
240
+
241
+ # Submit the transaction
242
+ response = self._substrate.submit_extrinsic(
243
+ extrinsic=extrinsic, wait_for_inclusion=True
244
+ )
245
+
246
+ # Get the transaction hash
247
+ tx_hash = response.extrinsic_hash
248
+
249
+ print(f"Batch transaction submitted successfully!")
250
+ print(f"Transaction hash: {tx_hash}")
251
+ print(
252
+ f"All {len(formatted_files)} files have been stored in a single transaction"
253
+ )
254
+
255
+ return tx_hash
256
+
257
+ except ValueError as e:
258
+ # Handle configuration errors
259
+ print(f"Error: {e}")
260
+ raise
261
+ except Exception as e:
262
+ print(f"Error interacting with Substrate: {e}")
263
+ raise
264
+
265
+ return "simulated-tx-hash"
266
+
267
+ def store_cid(
268
+ self, cid: str, filename: str = None, metadata: Optional[Dict[str, Any]] = None
269
+ ) -> str:
270
+ """
271
+ Store a CID on the blockchain.
272
+
273
+ Args:
274
+ cid: Content Identifier (CID) to store
275
+ filename: Original filename (optional)
276
+ metadata: Additional metadata to store with the CID
277
+
278
+ Returns:
279
+ str: Transaction hash
280
+ """
281
+ file_input = FileInput(file_hash=cid, file_name=filename or "unnamed_file")
282
+ return self.storage_request([file_input])
283
+
284
+ def get_cid_metadata(self, cid: str) -> Dict[str, Any]:
285
+ """
286
+ Retrieve metadata for a CID from the blockchain.
287
+
288
+ Args:
289
+ cid: Content Identifier (CID) to query
290
+
291
+ Returns:
292
+ Dict[str, Any]: Metadata associated with the CID
293
+ """
294
+ raise NotImplementedError("Substrate functionality is not implemented yet.")
295
+
296
+ def get_account_cids(self, account_address: str) -> List[str]:
297
+ """
298
+ Get all CIDs associated with an account.
299
+
300
+ Args:
301
+ account_address: Substrate account address
302
+
303
+ Returns:
304
+ List[str]: List of CIDs owned by the account
305
+ """
306
+ raise NotImplementedError("Substrate functionality is not implemented yet.")
307
+
308
+ def delete_cid(self, cid: str) -> str:
309
+ """
310
+ Delete a CID from the blockchain (mark as removed).
311
+
312
+ Args:
313
+ cid: Content Identifier (CID) to delete
314
+
315
+ Returns:
316
+ str: Transaction hash
317
+ """
318
+ raise NotImplementedError("Substrate functionality is not implemented yet.")
319
+
320
+ def get_storage_fee(self, file_size_mb: float) -> float:
321
+ """
322
+ Get the estimated storage fee for a file of given size.
323
+
324
+ Args:
325
+ file_size_mb: File size in megabytes
326
+
327
+ Returns:
328
+ float: Estimated fee in native tokens
329
+ """
330
+ raise NotImplementedError("Substrate functionality is not implemented yet.")
331
+
332
+ def get_account_balance(
333
+ self, account_address: Optional[str] = None
334
+ ) -> Dict[str, float]:
335
+ """
336
+ Get the balance of an account.
337
+
338
+ Args:
339
+ account_address: Substrate account address (uses keypair address if not specified)
340
+
341
+ Returns:
342
+ Dict[str, float]: Account balances (free, reserved, total)
343
+ """
344
+ raise NotImplementedError("Substrate functionality is not implemented yet.")
345
+
346
+ def get_free_credits(self, account_address: Optional[str] = None) -> float:
347
+ """
348
+ Get the free credits available for an account in the marketplace.
349
+
350
+ Args:
351
+ account_address: Substrate account address (uses keypair address if not specified)
352
+ Format: 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
353
+
354
+ Returns:
355
+ float: Free credits amount (with 18 decimal places)
356
+
357
+ Raises:
358
+ ConnectionError: If connection to Substrate fails
359
+ ValueError: If account has no credits
360
+ """
361
+ try:
362
+ # Initialize Substrate connection if not already connected
363
+ if not hasattr(self, "_substrate") or self._substrate is None:
364
+ print("Initializing Substrate connection...")
365
+ self._substrate = SubstrateInterface(
366
+ url=self.url,
367
+ ss58_format=42, # Substrate default
368
+ type_registry_preset="substrate-node-template",
369
+ )
370
+ print(f"Connected to Substrate node at {self.url}")
371
+
372
+ # Use provided account address or default to keypair address
373
+ if not account_address:
374
+ if not hasattr(self, "_keypair") or self._keypair is None:
375
+ if not hasattr(self, "_seed_phrase") or not self._seed_phrase:
376
+ raise ValueError(
377
+ "No account address provided and no seed phrase is set"
378
+ )
379
+
380
+ print("Creating keypair from seed phrase to get account address...")
381
+ self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
382
+
383
+ account_address = self._keypair.ss58_address
384
+ print(f"Using keypair address: {account_address}")
385
+
386
+ # Query the blockchain for free credits
387
+ print(f"Querying free credits for account: {account_address}")
388
+ result = self._substrate.query(
389
+ module="Credits",
390
+ storage_function="FreeCredits",
391
+ params=[account_address],
392
+ )
393
+
394
+ # If credits exist, convert to a float with 18 decimal places
395
+ if result.value is not None:
396
+ # Convert from blockchain u128 to float (divide by 10^18)
397
+ credits_raw = int(result.value)
398
+ credits_float = (
399
+ credits_raw / 1_000_000_000_000_000_000
400
+ ) # 18 zeros for decimals
401
+ print(f"Free credits: {credits_float} ({credits_raw} raw value)")
402
+ return credits_float
403
+ else:
404
+ print(f"No credits found for account: {account_address}")
405
+ raise ValueError(f"No credits found for account: {account_address}")
406
+
407
+ except Exception as e:
408
+ error_msg = f"Error querying free credits: {str(e)}"
409
+ print(error_msg)
410
+ raise ValueError(error_msg)
411
+
412
+ def get_user_file_hashes(self, account_address: Optional[str] = None) -> List[str]:
413
+ """
414
+ Get all file hashes (CIDs) stored by a user in the marketplace.
415
+
416
+ Args:
417
+ account_address: Substrate account address (uses keypair address if not specified)
418
+ Format: 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
419
+
420
+ Returns:
421
+ List[str]: List of CIDs stored by the user
422
+
423
+ Raises:
424
+ ConnectionError: If connection to Substrate fails
425
+ ValueError: If query fails or no files found
426
+ """
427
+ try:
428
+ # Initialize Substrate connection if not already connected
429
+ if not hasattr(self, "_substrate") or self._substrate is None:
430
+ print("Initializing Substrate connection...")
431
+ self._substrate = SubstrateInterface(
432
+ url=self.url,
433
+ ss58_format=42, # Substrate default
434
+ type_registry_preset="substrate-node-template",
435
+ )
436
+ print(f"Connected to Substrate node at {self.url}")
437
+
438
+ # Use provided account address or default to keypair address
439
+ if not account_address:
440
+ if not hasattr(self, "_keypair") or self._keypair is None:
441
+ if not hasattr(self, "_seed_phrase") or not self._seed_phrase:
442
+ raise ValueError(
443
+ "No account address provided and no seed phrase is set"
444
+ )
445
+
446
+ print("Creating keypair from seed phrase to get account address...")
447
+ self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
448
+
449
+ account_address = self._keypair.ss58_address
450
+ print(f"Using keypair address: {account_address}")
451
+
452
+ # Query the blockchain for user file hashes
453
+ print(f"Querying file hashes for account: {account_address}")
454
+ result = self._substrate.query(
455
+ module="Marketplace",
456
+ storage_function="UserFileHashes",
457
+ params=[account_address],
458
+ )
459
+
460
+ # If files exist, convert to a list of CIDs
461
+ if result.value:
462
+ # The result is already a list of bytes, convert each to string
463
+ file_hashes = [cid.hex() for cid in result.value]
464
+ print(f"Found {len(file_hashes)} files stored by this account")
465
+ return file_hashes
466
+ else:
467
+ print(f"No files found for account: {account_address}")
468
+ return []
469
+
470
+ except Exception as e:
471
+ error_msg = f"Error querying user file hashes: {str(e)}"
472
+ print(error_msg)
473
+ raise ValueError(error_msg)
474
+
475
+ def get_user_files(
476
+ self,
477
+ account_address: Optional[str] = None,
478
+ truncate_miners: bool = True,
479
+ max_miners: int = 3,
480
+ ) -> List[Dict[str, Any]]:
481
+ """
482
+ Get detailed information about all files stored by a user in the marketplace.
483
+
484
+ This method uses a custom JSON-RPC endpoint to get comprehensive file information.
485
+
486
+ Args:
487
+ account_address: Substrate account address (uses keypair address if not specified)
488
+ Format: 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
489
+ truncate_miners: Whether to truncate long miner IDs for display (default: True)
490
+ max_miners: Maximum number of miners to include in the response (default: 3, 0 for all)
491
+
492
+ Returns:
493
+ List[Dict[str, Any]]: List of file objects with the following structure:
494
+ {
495
+ "file_hash": str, # The IPFS CID of the file
496
+ "file_name": str, # The name of the file
497
+ "miner_ids": List[str], # List of miner IDs that have pinned the file
498
+ "miner_ids_full": List[str], # Complete list of miner IDs (if truncated)
499
+ "miner_count": int, # Total number of miners
500
+ "file_size": int, # Size of the file in bytes
501
+ "size_formatted": str # Human-readable file size
502
+ }
503
+
504
+ Raises:
505
+ ConnectionError: If connection to Substrate fails
506
+ ValueError: If query fails
507
+ """
508
+ try:
509
+ # Initialize Substrate connection if not already connected
510
+ if not hasattr(self, "_substrate") or self._substrate is None:
511
+ print("Initializing Substrate connection...")
512
+ self._substrate = SubstrateInterface(
513
+ url=self.url,
514
+ ss58_format=42, # Substrate default
515
+ type_registry_preset="substrate-node-template",
516
+ )
517
+ print(f"Connected to Substrate node at {self.url}")
518
+
519
+ # Use provided account address or default to keypair address
520
+ if not account_address:
521
+ if not hasattr(self, "_keypair") or self._keypair is None:
522
+ if not hasattr(self, "_seed_phrase") or not self._seed_phrase:
523
+ raise ValueError(
524
+ "No account address provided and no seed phrase is set"
525
+ )
526
+
527
+ print("Creating keypair from seed phrase to get account address...")
528
+ self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
529
+
530
+ account_address = self._keypair.ss58_address
531
+ print(f"Using keypair address: {account_address}")
532
+
533
+ # Prepare the JSON-RPC request
534
+ request = {
535
+ "jsonrpc": "2.0",
536
+ "method": "get_user_files",
537
+ "params": [account_address],
538
+ "id": 1,
539
+ }
540
+
541
+ print(f"Querying detailed file information for account: {account_address}")
542
+
543
+ # Make the JSON-RPC call
544
+ response = self._substrate.rpc_request(
545
+ method="get_user_files", params=[account_address]
546
+ )
547
+
548
+ # Check for errors in the response
549
+ if "error" in response:
550
+ error_msg = (
551
+ f"RPC error: {response['error'].get('message', 'Unknown error')}"
552
+ )
553
+ print(error_msg)
554
+ raise ValueError(error_msg)
555
+
556
+ # Extract the result
557
+ files = response.get("result", [])
558
+ print(f"Found {len(files)} files stored by this account")
559
+
560
+ # Helper function to convert ASCII code arrays to strings
561
+ def ascii_to_string(value):
562
+ if isinstance(value, list) and all(isinstance(x, int) for x in value):
563
+ return "".join(chr(code) for code in value)
564
+ return str(value)
565
+
566
+ # Helper function to properly format CIDs
567
+ def format_cid(cid_str):
568
+ # If it already looks like a proper CID, return it as is
569
+ if cid_str.startswith(("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")):
570
+ return cid_str
571
+
572
+ # Check if it's a hex string
573
+ if all(c in "0123456789abcdefABCDEF" for c in cid_str):
574
+ # First try the special case where the hex string is actually ASCII encoded
575
+ try:
576
+ # Try to decode the hex as ASCII characters
577
+ # (This is the case with some substrate responses where the CID is hex-encoded ASCII)
578
+ hex_bytes = bytes.fromhex(cid_str)
579
+ ascii_str = hex_bytes.decode("ascii")
580
+
581
+ # If the decoded string starts with a valid CID prefix, return it
582
+ if ascii_str.startswith(
583
+ ("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")
584
+ ):
585
+ return ascii_str
586
+ except Exception:
587
+ pass
588
+
589
+ # If the above doesn't work, try the standard CID decoding
590
+ try:
591
+ import base58
592
+ import binascii
593
+
594
+ # Try to decode hex to binary then to base58 for CIDv0
595
+ try:
596
+ binary_data = binascii.unhexlify(cid_str)
597
+ if (
598
+ len(binary_data) > 2
599
+ and binary_data[0] == 0x12
600
+ and binary_data[1] == 0x20
601
+ ):
602
+ # This looks like a CIDv0 (Qm...)
603
+ decoded_cid = base58.b58encode(binary_data).decode(
604
+ "utf-8"
605
+ )
606
+ return decoded_cid
607
+ except Exception:
608
+ pass
609
+
610
+ # If not successful, just return hex with 0x prefix as fallback
611
+ return f"0x{cid_str}"
612
+ except ImportError:
613
+ # If base58 is not available, return hex with prefix
614
+ return f"0x{cid_str}"
615
+
616
+ # Default case - return as is
617
+ return cid_str
618
+
619
+ # Helper function to format file sizes
620
+ def format_file_size(size_bytes):
621
+ if size_bytes >= 1024 * 1024:
622
+ return f"{size_bytes / (1024 * 1024):.2f} MB"
623
+ else:
624
+ return f"{size_bytes / 1024:.2f} KB"
625
+
626
+ # Helper function to format miner IDs for display
627
+ def format_miner_id(miner_id):
628
+ if (
629
+ truncate_miners
630
+ and isinstance(miner_id, str)
631
+ and miner_id.startswith("1")
632
+ and len(miner_id) > 40
633
+ ):
634
+ # Truncate long peer IDs
635
+ return f"{miner_id[:12]}...{miner_id[-4:]}"
636
+ return miner_id
637
+
638
+ # Process the response
639
+ processed_files = []
640
+ for file in files:
641
+ processed_file = {"file_size": file.get("file_size", 0)}
642
+
643
+ # Add formatted file size
644
+ processed_file["size_formatted"] = format_file_size(
645
+ processed_file["file_size"]
646
+ )
647
+
648
+ # Convert file_hash from byte array to string
649
+ if "file_hash" in file:
650
+ cid_str = ascii_to_string(file["file_hash"])
651
+ processed_file["file_hash"] = format_cid(cid_str)
652
+
653
+ # Convert file_name from byte array to string
654
+ if "file_name" in file:
655
+ processed_file["file_name"] = ascii_to_string(file["file_name"])
656
+
657
+ # Convert miner_ids from byte arrays to strings
658
+ if "miner_ids" in file and isinstance(file["miner_ids"], list):
659
+ all_miners = [
660
+ ascii_to_string(miner_id) for miner_id in file["miner_ids"]
661
+ ]
662
+ processed_file["miner_ids_full"] = all_miners
663
+ processed_file["miner_count"] = len(all_miners)
664
+
665
+ # Truncate miner list if requested
666
+ if max_miners > 0 and len(all_miners) > max_miners:
667
+ displayed_miners = all_miners[:max_miners]
668
+ else:
669
+ displayed_miners = all_miners
670
+
671
+ # Format and store the displayed miners
672
+ processed_file["miner_ids"] = [
673
+ {"id": miner_id, "formatted": format_miner_id(miner_id)}
674
+ for miner_id in displayed_miners
675
+ ]
676
+ else:
677
+ processed_file["miner_ids"] = []
678
+ processed_file["miner_ids_full"] = []
679
+ processed_file["miner_count"] = 0
680
+
681
+ processed_files.append(processed_file)
682
+
683
+ return processed_files
684
+
685
+ except Exception as e:
686
+ error_msg = f"Error querying user files: {str(e)}"
687
+ print(error_msg)
688
+ raise ValueError(error_msg)