hippius 0.1.14__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hippius_sdk/substrate.py CHANGED
@@ -1,24 +1,25 @@
1
- """
2
- Substrate operations for the Hippius SDK.
3
-
4
- Note: This functionality is coming soon and not implemented yet.
5
- """
6
-
1
+ import datetime
7
2
  import json
8
3
  import os
4
+ import tempfile
5
+ import time
9
6
  import uuid
10
7
  from typing import Any, Dict, List, Optional, Union
11
8
 
12
9
  from dotenv import load_dotenv
10
+ from mnemonic import Mnemonic
13
11
  from substrateinterface import Keypair, SubstrateInterface
14
12
 
15
13
  from hippius_sdk.config import (
16
14
  get_account_address,
17
15
  get_active_account,
16
+ get_all_config,
18
17
  get_config_value,
19
18
  get_seed_phrase,
19
+ set_active_account,
20
20
  set_seed_phrase,
21
21
  )
22
+ from hippius_sdk.utils import hex_to_ipfs_cid
22
23
 
23
24
  # Load environment variables
24
25
  load_dotenv()
@@ -179,6 +180,300 @@ class SubstrateClient:
179
180
  print(f"Warning: Could not get seed phrase from config: {e}")
180
181
  return False
181
182
 
183
+ def generate_mnemonic(self) -> str:
184
+ """
185
+ Generate a new random 12-word mnemonic phrase.
186
+
187
+ Returns:
188
+ str: A 12-word mnemonic seed phrase
189
+ """
190
+ try:
191
+ mnemo = Mnemonic("english")
192
+ return mnemo.generate(strength=128) # 128 bits = 12 words
193
+ except Exception as e:
194
+ raise ValueError(f"Error generating mnemonic: {e}")
195
+
196
+ def create_account(
197
+ self, name: str, encode: bool = False, password: Optional[str] = None
198
+ ) -> Dict[str, Any]:
199
+ """
200
+ Create a new account with a generated seed phrase.
201
+
202
+ Args:
203
+ name: Name for the new account
204
+ encode: Whether to encrypt the seed phrase with a password
205
+ password: Optional password for encryption (will prompt if not provided and encode=True)
206
+
207
+ Returns:
208
+ Dict[str, Any]: Dictionary with new account details
209
+ """
210
+ # Check if account name already exists
211
+ config = get_all_config()
212
+ if name in config["substrate"].get("accounts", {}):
213
+ raise ValueError(f"Account with name '{name}' already exists")
214
+
215
+ # Generate a new mnemonic seed phrase
216
+ mnemonic = self.generate_mnemonic()
217
+
218
+ # Create a keypair from the mnemonic
219
+ keypair = Keypair.create_from_mnemonic(mnemonic)
220
+ ss58_address = keypair.ss58_address
221
+
222
+ # Save the seed phrase to configuration
223
+ if encode:
224
+ result = set_seed_phrase(
225
+ mnemonic, encode=True, password=password, account_name=name
226
+ )
227
+ else:
228
+ result = set_seed_phrase(mnemonic, encode=False, account_name=name)
229
+
230
+ if not result:
231
+ raise RuntimeError("Failed to save account to configuration")
232
+
233
+ # Set this as the active account
234
+ set_active_account(name)
235
+
236
+ # Update the client's state to use this account
237
+ self._account_name = name
238
+ self._account_address = ss58_address
239
+ self._seed_phrase = mnemonic
240
+ self._keypair = keypair
241
+ self._read_only = False
242
+
243
+ # Return the new account details
244
+ return {
245
+ "name": name,
246
+ "address": ss58_address,
247
+ "mnemonic": mnemonic,
248
+ "is_active": True,
249
+ "creation_date": datetime.datetime.now().isoformat(),
250
+ }
251
+
252
+ def export_account(
253
+ self, account_name: Optional[str] = None, file_path: Optional[str] = None
254
+ ) -> str:
255
+ """
256
+ Export an account to a JSON file.
257
+
258
+ Args:
259
+ account_name: Name of the account to export (uses active account if None)
260
+ file_path: Path to save the exported account file (auto-generated if None)
261
+
262
+ Returns:
263
+ str: Path to the exported account file
264
+ """
265
+ # Determine which account to export
266
+ name_to_use = account_name or self._account_name or get_active_account()
267
+ if not name_to_use:
268
+ raise ValueError("No account specified and no active account")
269
+
270
+ # Get the seed phrase and address
271
+ seed_phrase = get_seed_phrase(account_name=name_to_use)
272
+ if not seed_phrase:
273
+ raise ValueError(
274
+ f"Could not retrieve seed phrase for account '{name_to_use}'"
275
+ )
276
+
277
+ address = get_account_address(name_to_use)
278
+ if not address:
279
+ # Generate the address from the seed phrase
280
+ keypair = Keypair.create_from_mnemonic(seed_phrase)
281
+ address = keypair.ss58_address
282
+
283
+ # Create the export data structure
284
+ export_data = {
285
+ "name": name_to_use,
286
+ "address": address,
287
+ "mnemonic": seed_phrase,
288
+ "meta": {
289
+ "exported_at": datetime.datetime.now().isoformat(),
290
+ "description": "Hippius SDK exported account",
291
+ },
292
+ }
293
+
294
+ # Determine the file path if not provided
295
+ if not file_path:
296
+ file_path = f"{name_to_use}_account_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
297
+
298
+ # Write the export file
299
+ try:
300
+ with open(file_path, "w") as f:
301
+ json.dump(export_data, f, indent=2)
302
+ print(f"Account '{name_to_use}' exported to {file_path}")
303
+ return file_path
304
+ except Exception as e:
305
+ raise ValueError(f"Failed to export account: {e}")
306
+
307
+ def import_account(
308
+ self, file_path: str, password: Optional[str] = None
309
+ ) -> Dict[str, Any]:
310
+ """
311
+ Import an account from a JSON file.
312
+
313
+ Args:
314
+ file_path: Path to the account export file
315
+ password: Optional password to use for encrypting the imported seed phrase
316
+
317
+ Returns:
318
+ Dict[str, Any]: Dictionary with imported account details
319
+ """
320
+ try:
321
+ # Read the export file
322
+ with open(file_path, "r") as f:
323
+ import_data = json.load(f)
324
+
325
+ # Validate the import data structure
326
+ required_fields = ["name", "address", "mnemonic"]
327
+ for field in required_fields:
328
+ if field not in import_data:
329
+ raise ValueError(
330
+ f"Invalid account file format: missing '{field}' field"
331
+ )
332
+
333
+ # Extract account details
334
+ name = import_data["name"]
335
+ address = import_data["address"]
336
+ mnemonic = import_data["mnemonic"]
337
+
338
+ # Check if the account name already exists
339
+ config = get_all_config()
340
+ if name in config["substrate"].get("accounts", {}):
341
+ # Modify the name to avoid conflicts
342
+ original_name = name
343
+ name = f"{name}_imported_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
344
+ print(
345
+ f"Account name '{original_name}' already exists, using '{name}' instead"
346
+ )
347
+
348
+ # Save the account to configuration
349
+ if password:
350
+ # Encrypt the seed phrase with the provided password
351
+ result = set_seed_phrase(
352
+ mnemonic, encode=True, password=password, account_name=name
353
+ )
354
+ else:
355
+ # Store the seed phrase in plain text
356
+ result = set_seed_phrase(mnemonic, encode=False, account_name=name)
357
+
358
+ if not result:
359
+ raise RuntimeError("Failed to save imported account to configuration")
360
+
361
+ # Set this as the active account
362
+ set_active_account(name)
363
+
364
+ # Update the client's state to use this account
365
+ self._account_name = name
366
+ self._account_address = address
367
+ self._seed_phrase = mnemonic
368
+ self._keypair = Keypair.create_from_mnemonic(mnemonic)
369
+ self._read_only = False
370
+
371
+ # Return the imported account details
372
+ return {
373
+ "name": name,
374
+ "address": address,
375
+ "is_active": True,
376
+ "imported_at": datetime.datetime.now().isoformat(),
377
+ "original_name": import_data.get("name"),
378
+ }
379
+ except Exception as e:
380
+ raise ValueError(f"Failed to import account: {e}")
381
+
382
+ async def get_account_info(
383
+ self, account_name: Optional[str] = None, include_history: bool = False
384
+ ) -> Dict[str, Any]:
385
+ """
386
+ Get detailed information about an account.
387
+
388
+ Args:
389
+ account_name: Name of the account to get info for (uses active account if None)
390
+ include_history: Whether to include usage history in the results
391
+
392
+ Returns:
393
+ Dict[str, Any]: Detailed account information
394
+ """
395
+ # Determine which account to get info for
396
+ name_to_use = account_name or self._account_name or get_active_account()
397
+ if not name_to_use:
398
+ raise ValueError("No account specified and no active account")
399
+
400
+ # Get the configuration to extract account data
401
+ config = get_all_config()
402
+
403
+ # Check if the account exists
404
+ if name_to_use not in config["substrate"].get("accounts", {}):
405
+ raise ValueError(f"Account '{name_to_use}' not found")
406
+
407
+ # Get account data from config
408
+ account_data = config["substrate"]["accounts"][name_to_use]
409
+ is_active = name_to_use == config["substrate"].get("active_account")
410
+ is_encoded = account_data.get("seed_phrase_encoded", False)
411
+ address = account_data.get("ss58_address")
412
+
413
+ # Create the account info object
414
+ account_info = {
415
+ "name": name_to_use,
416
+ "address": address,
417
+ "is_active": is_active,
418
+ "seed_phrase_encrypted": is_encoded,
419
+ }
420
+
421
+ # Query storage statistics for this account
422
+ try:
423
+ # Get files stored by this account - use await since this is an async method
424
+ files = await self.get_user_files_from_profile(address)
425
+
426
+ # Calculate storage statistics
427
+ total_files = len(files)
428
+ total_size_bytes = sum(file.get("file_size", 0) for file in files)
429
+
430
+ # Add storage stats to account info
431
+ account_info["storage_stats"] = {
432
+ "files": total_files,
433
+ "bytes_used": total_size_bytes,
434
+ "size_formatted": self._format_size(total_size_bytes)
435
+ if total_size_bytes
436
+ else "0 B",
437
+ }
438
+
439
+ # Include file list if requested
440
+ if include_history:
441
+ account_info["files"] = files
442
+
443
+ # Try to get account balance
444
+ try:
445
+ account_info["balance"] = await self.get_account_balance(address)
446
+ except Exception as e:
447
+ # Ignore balance errors, it's optional information
448
+ print(f"Could not fetch balance: {e}")
449
+ pass
450
+
451
+ # Try to get free credits
452
+ try:
453
+ account_info["free_credits"] = await self.get_free_credits(address)
454
+ except Exception as e:
455
+ # Ignore credits errors, it's optional information
456
+ print(f"Could not fetch free credits: {e}")
457
+ pass
458
+ except Exception as e:
459
+ # Add a note about the error but don't fail the whole operation
460
+ account_info["storage_stats"] = {
461
+ "error": f"Could not fetch storage statistics: {str(e)}"
462
+ }
463
+
464
+ return account_info
465
+
466
+ def _format_size(self, size_bytes: int) -> str:
467
+ """Format file size in human-readable format"""
468
+ if size_bytes < 1024:
469
+ return f"{size_bytes} B"
470
+ elif size_bytes < 1024 * 1024:
471
+ return f"{size_bytes / 1024:.2f} KB"
472
+ elif size_bytes < 1024 * 1024 * 1024:
473
+ return f"{size_bytes / (1024 * 1024):.2f} MB"
474
+ else:
475
+ return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
476
+
182
477
  def set_seed_phrase(self, seed_phrase: str) -> None:
183
478
  """
184
479
  Set or update the seed phrase used for signing transactions.
@@ -202,7 +497,7 @@ class SubstrateClient:
202
497
  print(f"Warning: Could not create keypair from seed phrase: {e}")
203
498
  print(f"Keypair will be created when needed")
204
499
 
205
- def storage_request(
500
+ async def storage_request(
206
501
  self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
207
502
  ) -> str:
208
503
  """
@@ -276,8 +571,7 @@ class SubstrateClient:
276
571
  print(f"Created file list with {len(file_list)} entries")
277
572
 
278
573
  # Step 2: Upload the JSON file to IPFS
279
- import tempfile
280
-
574
+ # Defer import to avoid circular imports
281
575
  from hippius_sdk.ipfs import IPFSClient
282
576
 
283
577
  ipfs_client = IPFSClient()
@@ -291,7 +585,7 @@ class SubstrateClient:
291
585
 
292
586
  try:
293
587
  print("Uploading file list to IPFS...")
294
- upload_result = ipfs_client.upload_file(temp_file_path)
588
+ upload_result = await ipfs_client.upload_file(temp_file_path)
295
589
  files_list_cid = upload_result["cid"]
296
590
  print(f"File list uploaded to IPFS with CID: {files_list_cid}")
297
591
  finally:
@@ -313,19 +607,42 @@ class SubstrateClient:
313
607
 
314
608
  # Create the call to the marketplace
315
609
  print(f"Call parameters: {json.dumps(call_params, indent=2)}")
316
- call = self._substrate.compose_call(
317
- call_module="Marketplace",
318
- call_function="storage_request",
319
- call_params=call_params,
320
- )
610
+ try:
611
+ call = self._substrate.compose_call(
612
+ call_module="Marketplace",
613
+ call_function="storage_request",
614
+ call_params=call_params,
615
+ )
616
+ except Exception as e:
617
+ print(f"Warning: Error composing call: {e}")
618
+ print("Attempting to use IpfsPallet.storeFile instead...")
619
+
620
+ # Try with IpfsPallet.storeFile as an alternative
621
+ alt_call_params = {
622
+ "fileHash": files_list_cid,
623
+ "fileName": f"files_list_{uuid.uuid4()}", # Generate a unique ID
624
+ }
625
+ call = self._substrate.compose_call(
626
+ call_module="IpfsPallet",
627
+ call_function="storeFile",
628
+ call_params=alt_call_params,
629
+ )
321
630
 
322
631
  # Get payment info to estimate the fee
323
632
  payment_info = self._substrate.get_payment_info(
324
633
  call=call, keypair=self._keypair
325
634
  )
326
635
 
636
+ print(f"Payment info: {json.dumps(payment_info, indent=2)}")
637
+
638
+ # Convert partialFee from Substrate (10^18 units) to a more readable format
327
639
  estimated_fee = payment_info.get("partialFee", 0)
328
- print(f"Estimated transaction fee: {estimated_fee}")
640
+ estimated_fee_formatted = (
641
+ float(estimated_fee) / 1_000_000_000_000_000_000 if estimated_fee else 0
642
+ )
643
+ print(
644
+ f"Estimated transaction fee: {estimated_fee} ({estimated_fee_formatted:.10f} tokens)"
645
+ )
329
646
 
330
647
  # Create a signed extrinsic
331
648
  extrinsic = self._substrate.create_signed_extrinsic(
@@ -361,7 +678,7 @@ class SubstrateClient:
361
678
 
362
679
  return "simulated-tx-hash"
363
680
 
364
- def store_cid(
681
+ async def store_cid(
365
682
  self, cid: str, filename: str = None, metadata: Optional[Dict[str, Any]] = None
366
683
  ) -> str:
367
684
  """
@@ -376,7 +693,7 @@ class SubstrateClient:
376
693
  str: Transaction hash
377
694
  """
378
695
  file_input = FileInput(file_hash=cid, file_name=filename or "unnamed_file")
379
- return self.storage_request([file_input])
696
+ return await self.storage_request([file_input])
380
697
 
381
698
  def get_cid_metadata(self, cid: str) -> Dict[str, Any]:
382
699
  """
@@ -430,7 +747,7 @@ class SubstrateClient:
430
747
  """
431
748
  raise NotImplementedError("Substrate functionality is not implemented yet.")
432
749
 
433
- def get_account_balance(
750
+ async def get_account_balance(
434
751
  self, account_address: Optional[str] = None
435
752
  ) -> Dict[str, float]:
436
753
  """
@@ -442,9 +759,174 @@ class SubstrateClient:
442
759
  Returns:
443
760
  Dict[str, float]: Account balances (free, reserved, total)
444
761
  """
445
- raise NotImplementedError("Substrate functionality is not implemented yet.")
762
+ try:
763
+ # Initialize Substrate connection if not already connected
764
+ if not hasattr(self, "_substrate") or self._substrate is None:
765
+ print("Initializing Substrate connection...")
766
+ self._substrate = SubstrateInterface(
767
+ url=self.url,
768
+ ss58_format=42, # Substrate default
769
+ type_registry_preset="substrate-node-template",
770
+ )
771
+ print(f"Connected to Substrate node at {self.url}")
772
+
773
+ # Use provided account address or default to keypair/configured address
774
+ if not account_address:
775
+ if self._account_address:
776
+ account_address = self._account_address
777
+ print(f"Using account address: {account_address}")
778
+ else:
779
+ # Try to get the address from the keypair (requires seed phrase)
780
+ if not self._ensure_keypair():
781
+ raise ValueError("No account address available")
782
+ account_address = self._keypair.ss58_address
783
+ print(f"Using keypair address: {account_address}")
784
+
785
+ # Query the blockchain for account balance
786
+ print(f"Querying balance for account: {account_address}")
787
+ result = self._substrate.query(
788
+ module="System",
789
+ storage_function="Account",
790
+ params=[account_address],
791
+ )
792
+
793
+ # If account exists, extract the balance information
794
+ if result.value:
795
+ data = result.value
796
+ print(data)
797
+ # Extract balance components
798
+ free_balance = data.get("data", {}).get("free", 0)
799
+ reserved_balance = data.get("data", {}).get("reserved", 0)
800
+ frozen_balance = data.get("data", {}).get("frozen", 0)
801
+
802
+ # Convert from blockchain units to float (divide by 10^18)
803
+ divisor = 1_000_000_000_000_000_000 # 18 zeros for decimals
804
+
805
+ free = float(free_balance) / divisor
806
+ reserved = float(reserved_balance) / divisor
807
+ frozen = float(frozen_balance) / divisor
808
+
809
+ # Calculate total (free + reserved - frozen)
810
+ total = free + reserved - frozen
811
+
812
+ return {
813
+ "free": free,
814
+ "reserved": reserved,
815
+ "frozen": frozen,
816
+ "total": total,
817
+ "raw": {
818
+ "free": free_balance,
819
+ "reserved": reserved_balance,
820
+ "frozen": frozen_balance,
821
+ },
822
+ }
823
+ else:
824
+ print(f"No account data found for: {account_address}")
825
+ return {
826
+ "free": 0.0,
827
+ "reserved": 0.0,
828
+ "frozen": 0.0,
829
+ "total": 0.0,
830
+ "raw": {"free": 0, "reserved": 0, "frozen": 0},
831
+ }
446
832
 
447
- def get_free_credits(self, account_address: Optional[str] = None) -> float:
833
+ except Exception as e:
834
+ error_msg = f"Error querying account balance: {str(e)}"
835
+ print(error_msg)
836
+ raise ValueError(error_msg)
837
+
838
+ async def watch_account_balance(
839
+ self, account_address: Optional[str] = None, interval: int = 5
840
+ ) -> None:
841
+ """
842
+ Watch account balance in real-time, updating at specified intervals.
843
+
844
+ The function runs until interrupted with Ctrl+C.
845
+
846
+ Args:
847
+ account_address: Substrate account address (uses keypair address if not specified)
848
+ interval: Polling interval in seconds (default: 5)
849
+ """
850
+ try:
851
+ # Use provided account address or default to keypair/configured address
852
+ if not account_address:
853
+ if self._account_address:
854
+ account_address = self._account_address
855
+ else:
856
+ # Try to get the address from the keypair (requires seed phrase)
857
+ if not self._ensure_keypair():
858
+ raise ValueError("No account address available")
859
+ account_address = self._keypair.ss58_address
860
+
861
+ print(f"Watching balance for account: {account_address}")
862
+ print(f"Updates every {interval} seconds. Press Ctrl+C to stop.")
863
+ print("-" * 80)
864
+
865
+ # Keep track of previous balance to show changes
866
+ previous_balance = None
867
+
868
+ try:
869
+ while True:
870
+ # Get current time for display
871
+ current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
872
+
873
+ # Get current balance
874
+ try:
875
+ balance = await self.get_account_balance(account_address)
876
+
877
+ # Clear screen (ANSI escape sequence)
878
+ print("\033c", end="")
879
+
880
+ # Display header
881
+ print(f"Account Balance Watch for: {account_address}")
882
+ print(f"Last update: {current_time}")
883
+ print("-" * 80)
884
+
885
+ # Display current balance
886
+ print(f"Free: {balance['free']:.6f}")
887
+ print(f"Reserved: {balance['reserved']:.6f}")
888
+ print(f"Frozen: {balance['frozen']:.6f}")
889
+ print(f"Total: {balance['total']:.6f}")
890
+
891
+ # Show changes since last update if available
892
+ if previous_balance:
893
+ print("\nChanges since last update:")
894
+ free_change = balance["free"] - previous_balance["free"]
895
+ reserved_change = (
896
+ balance["reserved"] - previous_balance["reserved"]
897
+ )
898
+ total_change = balance["total"] - previous_balance["total"]
899
+
900
+ # Format changes with + or - sign
901
+ print(f"Free: {free_change:+.6f}")
902
+ print(f"Reserved: {reserved_change:+.6f}")
903
+ print(f"Total: {total_change:+.6f}")
904
+
905
+ # Store current balance for next comparison
906
+ previous_balance = balance
907
+
908
+ # Show instructions at the bottom
909
+ print(
910
+ "\nUpdating every",
911
+ interval,
912
+ "seconds. Press Ctrl+C to stop.",
913
+ )
914
+
915
+ except Exception as e:
916
+ # Show error but continue watching
917
+ print(f"Error: {e}")
918
+ print(f"Will try again in {interval} seconds...")
919
+
920
+ # Wait for next update
921
+ time.sleep(interval)
922
+
923
+ except KeyboardInterrupt:
924
+ print("\nBalance watch stopped.")
925
+
926
+ except Exception as e:
927
+ print(f"Error in watch_account_balance: {e}")
928
+
929
+ async def get_free_credits(self, account_address: Optional[str] = None) -> float:
448
930
  """
449
931
  Get the free credits available for an account in the marketplace.
450
932
 
@@ -569,7 +1051,7 @@ class SubstrateClient:
569
1051
  print(error_msg)
570
1052
  raise ValueError(error_msg)
571
1053
 
572
- def get_user_files(
1054
+ async def get_user_files(
573
1055
  self,
574
1056
  account_address: Optional[str] = None,
575
1057
  truncate_miners: bool = True,
@@ -604,9 +1086,9 @@ class SubstrateClient:
604
1086
  """
605
1087
  # For backward compatibility, this method now calls get_user_files_from_profile
606
1088
  # with appropriate conversions
607
- return self.get_user_files_from_profile(account_address)
1089
+ return await self.get_user_files_from_profile(account_address)
608
1090
 
609
- def get_user_files_from_profile(
1091
+ async def get_user_files_from_profile(
610
1092
  self,
611
1093
  account_address: Optional[str] = None,
612
1094
  ) -> List[Dict[str, Any]]:
@@ -680,12 +1162,13 @@ class SubstrateClient:
680
1162
  print(f"Decoded IPFS CID: {profile_cid}")
681
1163
 
682
1164
  # Fetch the profile JSON from IPFS
1165
+ # Defer import to avoid circular imports
683
1166
  from hippius_sdk.ipfs import IPFSClient
684
1167
 
685
1168
  ipfs_client = IPFSClient()
686
1169
 
687
1170
  print(f"Fetching user profile from IPFS: {profile_cid}")
688
- profile_data = ipfs_client.cat(profile_cid)
1171
+ profile_data = await ipfs_client.cat(profile_cid)
689
1172
 
690
1173
  # Parse the JSON content
691
1174
  if not profile_data.get("is_text", False):
@@ -797,56 +1280,205 @@ class SubstrateClient:
797
1280
  print(error_msg)
798
1281
  raise ValueError(error_msg)
799
1282
 
800
- def _hex_to_ipfs_cid(self, hex_string: str) -> str:
1283
+ def get_pinning_status(
1284
+ self, account_address: Optional[str] = None
1285
+ ) -> List[Dict[str, Any]]:
801
1286
  """
802
- Convert a hex-encoded IPFS CID to a regular IPFS CID.
1287
+ Get the status of file pinning requests for an account.
1288
+
1289
+ This method queries the blockchain for all storage requests made by the user
1290
+ to check their pinning status.
803
1291
 
804
1292
  Args:
805
- hex_string: Hex string representation of an IPFS CID
1293
+ account_address: Substrate account address (uses keypair address if not specified)
1294
+ Format: 5HoreGVb17XhY3wanDvzoAWS7yHYbc5uMteXqRNTiZ6Txkqq
806
1295
 
807
1296
  Returns:
808
- str: Regular IPFS CID
1297
+ List[Dict[str, Any]]: List of storage requests with their status information:
1298
+ {
1299
+ "cid": str, # The IPFS CID of the file
1300
+ "file_name": str, # The name of the file
1301
+ "total_replicas": int, # Total number of replicas requested
1302
+ "owner": str, # Owner's address
1303
+ "created_at": int, # Block number when request was created
1304
+ "last_charged_at": int, # Block number when last charged
1305
+ "miner_ids": List[str], # List of miners assigned to pin the file
1306
+ "selected_validator": str, # Selected validator address
1307
+ "is_assigned": bool, # Whether request has been assigned to miners
1308
+ }
1309
+
1310
+ Raises:
1311
+ ConnectionError: If connection to Substrate fails
1312
+ ValueError: If query fails or no requests found
809
1313
  """
810
- # First, try to decode as ASCII if it's a hex representation of ASCII characters
811
1314
  try:
812
- if hex_string.startswith("0x"):
813
- hex_string = hex_string[2:]
1315
+ # Initialize Substrate connection if not already connected
1316
+ if not hasattr(self, "_substrate") or self._substrate is None:
1317
+ print("Initializing Substrate connection...")
1318
+ self._substrate = SubstrateInterface(
1319
+ url=self.url,
1320
+ ss58_format=42, # Substrate default
1321
+ type_registry_preset="substrate-node-template",
1322
+ )
1323
+ print(f"Connected to Substrate node at {self.url}")
1324
+
1325
+ # Use provided account address or default to keypair/configured address
1326
+ if not account_address:
1327
+ if self._account_address:
1328
+ account_address = self._account_address
1329
+ print(f"Using account address: {account_address}")
1330
+ else:
1331
+ # Try to get the address from the keypair (requires seed phrase)
1332
+ if not self._ensure_keypair():
1333
+ raise ValueError("No account address available")
1334
+ account_address = self._keypair.ss58_address
1335
+ print(f"Using keypair address: {account_address}")
1336
+
1337
+ # Query the blockchain for storage requests
1338
+ print(f"Querying storage requests for account: {account_address}")
1339
+ try:
1340
+ # First, try with query_map which is more suitable for iterating over collections
1341
+ result = self._substrate.query_map(
1342
+ module="IpfsPallet",
1343
+ storage_function="UserStorageRequests",
1344
+ params=[account_address],
1345
+ )
1346
+ results_list = list(result)
1347
+ except Exception as e:
1348
+ print(f"Error with query_map: {e}")
1349
+ try:
1350
+ # Try again with query to double check storage function requirements
1351
+ result = self._substrate.query(
1352
+ module="IpfsPallet",
1353
+ storage_function="UserStorageRequests",
1354
+ params=[
1355
+ account_address,
1356
+ None,
1357
+ ], # Try with a None second parameter
1358
+ )
814
1359
 
815
- bytes_data = bytes.fromhex(hex_string)
816
- ascii_str = bytes_data.decode("ascii")
1360
+ # If the query returns a nested structure, extract it
1361
+ if result.value and isinstance(result.value, list):
1362
+ # Convert to a list format similar to query_map for processing
1363
+ results_list = []
1364
+ for item in result.value:
1365
+ if isinstance(item, list) and len(item) >= 2:
1366
+ key = item[0]
1367
+ value = item[1]
1368
+ results_list.append((key, value))
1369
+ else:
1370
+ # If it's not a nested structure, use a simpler format
1371
+ results_list = [(None, result.value)] if result.value else []
1372
+ except Exception as e_inner:
1373
+ print(f"Error with fallback query: {e_inner}")
1374
+ # If both methods fail, return an empty list
1375
+ results_list = []
1376
+
1377
+ # Process the storage requests
1378
+ storage_requests = []
1379
+
1380
+ if not results_list:
1381
+ print(f"No storage requests found for account: {account_address}")
1382
+ return []
817
1383
 
818
- # If the decoded string starts with a valid CID prefix, return it
819
- if ascii_str.startswith(("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")):
820
- return ascii_str
821
- except Exception:
822
- # If ASCII decoding fails, continue with other methods
823
- pass
1384
+ print(f"Found {len(results_list)} storage request entries")
1385
+
1386
+ for i, (key, value) in enumerate(results_list):
1387
+ try:
1388
+ # For debugging, print raw data
1389
+ print(f"Entry {i+1}:")
1390
+ print(f" Raw key: {key}, type: {type(key)}")
1391
+ print(f" Raw value: {value}, type: {type(value)}")
1392
+
1393
+ # Extract file hash from key if possible
1394
+ file_hash_hex = None
1395
+ if key is not None:
1396
+ if hasattr(key, "hex"):
1397
+ file_hash_hex = key.hex()
1398
+ elif isinstance(key, bytes):
1399
+ file_hash_hex = key.hex()
1400
+ elif isinstance(key, str) and key.startswith("0x"):
1401
+ file_hash_hex = key[2:]
1402
+ else:
1403
+ file_hash_hex = str(key)
1404
+
1405
+ # Try to extract value data
1406
+ request_data = None
1407
+ if isinstance(value, dict):
1408
+ request_data = value
1409
+ elif hasattr(value, "get"):
1410
+ request_data = value
1411
+ elif hasattr(value, "__dict__"):
1412
+ # Convert object to dict
1413
+ request_data = {
1414
+ k: getattr(value, k)
1415
+ for k in dir(value)
1416
+ if not k.startswith("_") and not callable(getattr(value, k))
1417
+ }
1418
+
1419
+ # If we can't extract data, just use value as string for debugging
1420
+ if request_data is None:
1421
+ request_data = {"raw_value": str(value)}
1422
+
1423
+ # Create formatted request with available data
1424
+ formatted_request = {"raw_key": str(key), "raw_value": str(value)}
1425
+
1426
+ # Directly extract file_name from the value if it's a dict-like object
1427
+ if hasattr(value, "get"):
1428
+ if value.get("file_name"):
1429
+ formatted_request["file_name"] = value.get("file_name")
1430
+ elif value.get("fileName"):
1431
+ formatted_request["file_name"] = value.get("fileName")
1432
+
1433
+ # Add CID if we have it
1434
+ if file_hash_hex:
1435
+ file_cid = self._hex_to_ipfs_cid(file_hash_hex)
1436
+ formatted_request["cid"] = file_cid
1437
+
1438
+ # Add other fields from request_data if available
1439
+ for source_field, target_field in [
1440
+ ("fileName", "file_name"),
1441
+ ("totalReplicas", "total_replicas"),
1442
+ ("owner", "owner"),
1443
+ ("createdAt", "created_at"),
1444
+ ("lastChargedAt", "last_charged_at"),
1445
+ ("minerIds", "miner_ids"),
1446
+ ("selectedValidator", "selected_validator"),
1447
+ ("isAssigned", "is_assigned"),
1448
+ # Add variants that might appear differently in the chain storage
1449
+ ("file_name", "file_name"),
1450
+ ("file_hash", "file_hash"),
1451
+ ("total_replicas", "total_replicas"),
1452
+ ]:
1453
+ if source_field in request_data:
1454
+ formatted_request[target_field] = request_data[source_field]
1455
+ # Fallback to attribute access for different types of objects
1456
+ elif hasattr(value, source_field):
1457
+ formatted_request[target_field] = getattr(
1458
+ value, source_field
1459
+ )
1460
+
1461
+ storage_requests.append(formatted_request)
1462
+
1463
+ except Exception as e:
1464
+ print(f"Error processing request entry {i+1}: {e}")
1465
+
1466
+ print(f"Successfully processed {len(storage_requests)} storage requests")
1467
+ return storage_requests
824
1468
 
825
- # Try to decode as a binary CID
826
- try:
827
- import base58
828
-
829
- if hex_string.startswith("0x"):
830
- hex_string = hex_string[2:]
831
-
832
- binary_data = bytes.fromhex(hex_string)
833
-
834
- # Check if it matches CIDv0 pattern (starts with 0x12, 0x20)
835
- if (
836
- len(binary_data) > 2
837
- and binary_data[0] == 0x12
838
- and binary_data[1] == 0x20
839
- ):
840
- # CIDv0 (Qm...)
841
- return base58.b58encode(binary_data).decode("utf-8")
842
-
843
- # If it doesn't match CIDv0, for CIDv1 just return the hex without 0x prefix
844
- # since adding 0x breaks IPFS gateway URLs
845
- return hex_string
846
- except ImportError:
847
- # If base58 is not available
848
- print("Warning: base58 module not available for proper CID conversion")
849
- return hex_string
850
1469
  except Exception as e:
851
- print(f"Error converting hex to CID: {e}")
852
- return hex_string
1470
+ error_msg = f"Error querying storage requests: {str(e)}"
1471
+ print(error_msg)
1472
+ raise ValueError(error_msg)
1473
+
1474
+ def _hex_to_ipfs_cid(self, hex_string: str) -> str:
1475
+ """
1476
+ Convert a hex-encoded IPFS CID to a regular IPFS CID.
1477
+
1478
+ Args:
1479
+ hex_string: Hex string representation of an IPFS CID
1480
+
1481
+ Returns:
1482
+ str: Regular IPFS CID
1483
+ """
1484
+ return hex_to_ipfs_cid(hex_string)