hippius 0.2.1__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_sdk/substrate.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import datetime
2
2
  import json
3
3
  import os
4
+ import pprint
4
5
  import tempfile
5
6
  import time
6
7
  import uuid
@@ -19,7 +20,11 @@ from hippius_sdk.config import (
19
20
  set_active_account,
20
21
  set_seed_phrase,
21
22
  )
22
- from hippius_sdk.utils import hex_to_ipfs_cid
23
+ from hippius_sdk.utils import (
24
+ format_size,
25
+ hex_to_ipfs_cid,
26
+ initialize_substrate_connection,
27
+ )
23
28
 
24
29
  # Load environment variables
25
30
  load_dotenv()
@@ -128,16 +133,12 @@ class SubstrateClient:
128
133
  print("Connected successfully (read-only mode, no account)")
129
134
  self._read_only = True
130
135
 
131
- return True
132
-
133
136
  except Exception as e:
134
137
  print(f"Failed to connect to Substrate node: {e}")
135
138
  raise ConnectionError(
136
139
  f"Could not connect to Substrate node at {self.url}: {e}"
137
140
  )
138
141
 
139
- return False
140
-
141
142
  def _ensure_keypair(self) -> bool:
142
143
  """
143
144
  Ensure we have a keypair for signing transactions.
@@ -201,7 +202,7 @@ class SubstrateClient:
201
202
 
202
203
  Args:
203
204
  name: Name for the new account
204
- encode: Whether to encrypt the seed phrase with a password
205
+ encode: Whether to encrypt the seed phrase with a password.
205
206
  password: Optional password for encryption (will prompt if not provided and encode=True)
206
207
 
207
208
  Returns:
@@ -431,9 +432,9 @@ class SubstrateClient:
431
432
  account_info["storage_stats"] = {
432
433
  "files": total_files,
433
434
  "bytes_used": total_size_bytes,
434
- "size_formatted": self._format_size(total_size_bytes)
435
+ "size_formatted": format_size(total_size_bytes)
435
436
  if total_size_bytes
436
- else "0 B",
437
+ else "0 bytes",
437
438
  }
438
439
 
439
440
  # Include file list if requested
@@ -463,17 +464,6 @@ class SubstrateClient:
463
464
 
464
465
  return account_info
465
466
 
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
-
477
467
  def set_seed_phrase(self, seed_phrase: str) -> None:
478
468
  """
479
469
  Set or update the seed phrase used for signing transactions.
@@ -495,7 +485,6 @@ class SubstrateClient:
495
485
  print(f"Keypair created for account: {self._keypair.ss58_address}")
496
486
  except Exception as e:
497
487
  print(f"Warning: Could not create keypair from seed phrase: {e}")
498
- print(f"Keypair will be created when needed")
499
488
 
500
489
  async def storage_request(
501
490
  self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
@@ -539,147 +528,119 @@ class SubstrateClient:
539
528
  file_inputs.append(file)
540
529
 
541
530
  # Print what is being submitted
542
- print(f"Preparing storage request for {len(file_inputs)} files:")
543
531
  for file in file_inputs:
544
532
  print(f" - {file.file_name}: {file.file_hash}")
545
533
 
546
- if miner_ids:
547
- print(f"Targeted miners: {', '.join(miner_ids)}")
548
- else:
549
- print("No specific miners targeted (using default selection)")
550
-
551
- try:
552
- # Initialize Substrate connection
553
- if not hasattr(self, "_substrate") or self._substrate is None:
554
- print("Initializing Substrate connection...")
555
- self._substrate = SubstrateInterface(
556
- url=self.url,
557
- ss58_format=42, # Substrate default
558
- type_registry_preset="substrate-node-template",
559
- )
560
- print(f"Connected to Substrate node at {self.url}")
561
-
562
- # Step 1: Create a JSON file with the list of files to pin
563
- file_list = []
564
- for file_input in file_inputs:
565
- file_list.append(
566
- {"filename": file_input.file_name, "cid": file_input.file_hash}
567
- )
534
+ # Initialize Substrate connection
535
+ substrate, _ = initialize_substrate_connection(self)
568
536
 
569
- # Convert to JSON
570
- files_json = json.dumps(file_list, indent=2)
571
- print(f"Created file list with {len(file_list)} entries")
537
+ # Step 1: Create a JSON file with the list of files to pin
538
+ file_list = []
539
+ for file_input in file_inputs:
540
+ file_list.append(
541
+ {"filename": file_input.file_name, "cid": file_input.file_hash}
542
+ )
572
543
 
573
- # Step 2: Upload the JSON file to IPFS
574
- # Defer import to avoid circular imports
575
- from hippius_sdk.ipfs import IPFSClient
544
+ # Convert to JSON
545
+ files_json = json.dumps(file_list, indent=2)
546
+ print(f"Created file list with {len(file_list)} entries")
576
547
 
577
- ipfs_client = IPFSClient()
548
+ # Step 2: Upload the JSON file to IPFS
549
+ # Defer import to avoid circular imports
550
+ from hippius_sdk.ipfs import IPFSClient
578
551
 
579
- # Create a temporary file with the JSON content
580
- with tempfile.NamedTemporaryFile(
581
- mode="w+", suffix=".json", delete=False
582
- ) as temp_file:
583
- temp_file_path = temp_file.name
584
- temp_file.write(files_json)
552
+ ipfs_client = IPFSClient()
585
553
 
586
- try:
587
- print("Uploading file list to IPFS...")
588
- upload_result = await ipfs_client.upload_file(temp_file_path)
589
- files_list_cid = upload_result["cid"]
590
- print(f"File list uploaded to IPFS with CID: {files_list_cid}")
591
- finally:
592
- # Clean up the temporary file
593
- if os.path.exists(temp_file_path):
594
- os.remove(temp_file_path)
595
-
596
- # Step 3: Submit the CID of the JSON file to the chain
597
- # Create call parameters with the CID of the JSON file
598
- call_params = {
599
- "files_input": [
600
- {
601
- "file_hash": files_list_cid,
602
- "file_name": f"files_list_{uuid.uuid4()}", # Generate a unique ID
603
- }
604
- ],
605
- "miner_ids": miner_ids if miner_ids else [],
606
- }
607
-
608
- # Create the call to the marketplace
609
- print(f"Call parameters: {json.dumps(call_params, indent=2)}")
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...")
554
+ # Create a temporary file with the JSON content
555
+ with tempfile.NamedTemporaryFile(
556
+ mode="w+", suffix=".json", delete=False
557
+ ) as temp_file:
558
+ temp_file_path = temp_file.name
559
+ temp_file.write(files_json)
619
560
 
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
561
+ try:
562
+ print("Uploading file list to IPFS...")
563
+ upload_result = await ipfs_client.upload_file(temp_file_path)
564
+ files_list_cid = upload_result["cid"]
565
+ print(f"File list uploaded to IPFS with CID: {files_list_cid}")
566
+ finally:
567
+ # Clean up the temporary file
568
+ if os.path.exists(temp_file_path):
569
+ os.remove(temp_file_path)
570
+
571
+ # Step 3: Submit the CID of the JSON file to the chain
572
+ # Create call parameters with the CID of the JSON file
573
+ call_params = {
574
+ "files_input": [
575
+ {
576
+ "file_hash": files_list_cid,
577
+ "file_name": f"files_list_{uuid.uuid4()}", # Generate a unique ID
624
578
  }
625
- call = self._substrate.compose_call(
626
- call_module="IpfsPallet",
627
- call_function="storeFile",
628
- call_params=alt_call_params,
629
- )
579
+ ],
580
+ "miner_ids": miner_ids if miner_ids else [],
581
+ }
630
582
 
631
- # Get payment info to estimate the fee
632
- payment_info = self._substrate.get_payment_info(
633
- call=call, keypair=self._keypair
583
+ # Create the call to the marketplace
584
+ try:
585
+ call = self._substrate.compose_call(
586
+ call_module="Marketplace",
587
+ call_function="storage_request",
588
+ call_params=call_params,
634
589
  )
590
+ except Exception as e:
591
+ print(f"Warning: Error composing call: {e}")
592
+ print("Attempting to use IpfsPallet.storeFile instead...")
635
593
 
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
639
- estimated_fee = payment_info.get("partialFee", 0)
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)"
594
+ # Try with IpfsPallet.storeFile as an alternative
595
+ alt_call_params = {
596
+ "fileHash": files_list_cid,
597
+ "fileName": f"files_list_{uuid.uuid4()}", # Generate a unique ID
598
+ }
599
+ call = self._substrate.compose_call(
600
+ call_module="IpfsPallet",
601
+ call_function="storeFile",
602
+ call_params=alt_call_params,
645
603
  )
646
604
 
647
- # Create a signed extrinsic
648
- extrinsic = self._substrate.create_signed_extrinsic(
649
- call=call, keypair=self._keypair
650
- )
605
+ # Get payment info to estimate the fee
606
+ payment_info = self._substrate.get_payment_info(
607
+ call=call, keypair=self._keypair
608
+ )
651
609
 
652
- print(
653
- f"Submitting transaction to store {len(file_list)} files via file list CID..."
654
- )
610
+ print(f"Payment info: {json.dumps(payment_info, indent=2)}")
655
611
 
656
- # Submit the transaction
657
- response = self._substrate.submit_extrinsic(
658
- extrinsic=extrinsic, wait_for_inclusion=True
659
- )
612
+ # Convert partialFee from Substrate (10^18 units) to a more readable format
613
+ estimated_fee = payment_info.get("partialFee", 0)
614
+ estimated_fee_formatted = (
615
+ float(estimated_fee) / 1_000_000_000_000_000_000 if estimated_fee else 0
616
+ )
617
+ print(
618
+ f"Estimated transaction fee: {estimated_fee} ({estimated_fee_formatted:.10f} tokens)"
619
+ )
660
620
 
661
- # Get the transaction hash
662
- tx_hash = response.extrinsic_hash
621
+ # Create a signed extrinsic
622
+ extrinsic = self._substrate.create_signed_extrinsic(
623
+ call=call, keypair=self._keypair
624
+ )
663
625
 
664
- print(f"Transaction submitted successfully!")
665
- print(f"Transaction hash: {tx_hash}")
666
- print(f"File list CID: {files_list_cid}")
667
- print(f"All {len(file_list)} files will be stored through this request")
626
+ print(
627
+ f"Submitting transaction to store {len(file_list)} files via file list CID..."
628
+ )
668
629
 
669
- return tx_hash
630
+ # Submit the transaction
631
+ response = self._substrate.submit_extrinsic(
632
+ extrinsic=extrinsic, wait_for_inclusion=True
633
+ )
670
634
 
671
- except ValueError as e:
672
- # Handle configuration errors
673
- print(f"Error: {e}")
674
- raise
675
- except Exception as e:
676
- print(f"Error interacting with Substrate: {e}")
677
- raise
635
+ # Get the transaction hash
636
+ tx_hash = response.extrinsic_hash
678
637
 
679
- return "simulated-tx-hash"
638
+ return tx_hash
680
639
 
681
640
  async def store_cid(
682
- self, cid: str, filename: str = None, metadata: Optional[Dict[str, Any]] = None
641
+ self,
642
+ cid: str,
643
+ filename: str = None,
683
644
  ) -> str:
684
645
  """
685
646
  Store a CID on the blockchain.
@@ -687,7 +648,6 @@ class SubstrateClient:
687
648
  Args:
688
649
  cid: Content Identifier (CID) to store
689
650
  filename: Original filename (optional)
690
- metadata: Additional metadata to store with the CID
691
651
 
692
652
  Returns:
693
653
  str: Transaction hash
@@ -695,58 +655,6 @@ class SubstrateClient:
695
655
  file_input = FileInput(file_hash=cid, file_name=filename or "unnamed_file")
696
656
  return await self.storage_request([file_input])
697
657
 
698
- def get_cid_metadata(self, cid: str) -> Dict[str, Any]:
699
- """
700
- Retrieve metadata for a CID from the blockchain.
701
-
702
- Args:
703
- cid: Content Identifier (CID) to query
704
-
705
- Returns:
706
- Dict[str, Any]: Metadata associated with the CID
707
- """
708
- raise NotImplementedError("Substrate functionality is not implemented yet.")
709
-
710
- def get_account_cids(self, account_address: str) -> List[str]:
711
- """
712
- Get all CIDs associated with an account.
713
-
714
- Args:
715
- account_address: Substrate account address
716
-
717
- Returns:
718
- List[str]: List of CIDs owned by the account
719
- """
720
- raise NotImplementedError("Substrate functionality is not implemented yet.")
721
-
722
- def delete_cid(self, cid: str) -> str:
723
- """
724
- Delete a CID from the blockchain (mark as removed).
725
-
726
- Args:
727
- cid: Content Identifier (CID) to delete
728
-
729
- Returns:
730
- str: Transaction hash
731
- """
732
- # This requires a keypair for signing
733
- if not self._ensure_keypair():
734
- raise ValueError("Seed phrase must be set before making transactions")
735
-
736
- raise NotImplementedError("Substrate functionality is not implemented yet.")
737
-
738
- def get_storage_fee(self, file_size_mb: float) -> float:
739
- """
740
- Get the estimated storage fee for a file of given size.
741
-
742
- Args:
743
- file_size_mb: File size in megabytes
744
-
745
- Returns:
746
- float: Estimated fee in native tokens
747
- """
748
- raise NotImplementedError("Substrate functionality is not implemented yet.")
749
-
750
658
  async def get_account_balance(
751
659
  self, account_address: Optional[str] = None
752
660
  ) -> Dict[str, float]:
@@ -760,30 +668,17 @@ class SubstrateClient:
760
668
  Dict[str, float]: Account balances (free, reserved, total)
761
669
  """
762
670
  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}")
671
+ # Initialize Substrate connection and get account address
672
+ substrate, derived_address = initialize_substrate_connection(self)
772
673
 
773
- # Use provided account address or default to keypair/configured address
674
+ # Use provided account address or the one derived from initialization
774
675
  if not account_address:
775
- if self._account_address:
776
- account_address = self._account_address
777
- print(f"Using account address: {account_address}")
676
+ if derived_address:
677
+ account_address = derived_address
778
678
  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}")
679
+ raise ValueError("No account address available")
784
680
 
785
681
  # Query the blockchain for account balance
786
- print(f"Querying balance for account: {account_address}")
787
682
  result = self._substrate.query(
788
683
  module="System",
789
684
  storage_function="Account",
@@ -793,7 +688,6 @@ class SubstrateClient:
793
688
  # If account exists, extract the balance information
794
689
  if result.value:
795
690
  data = result.value
796
- print(data)
797
691
  # Extract balance components
798
692
  free_balance = data.get("data", {}).get("free", 0)
799
693
  reserved_balance = data.get("data", {}).get("reserved", 0)
@@ -821,7 +715,6 @@ class SubstrateClient:
821
715
  },
822
716
  }
823
717
  else:
824
- print(f"No account data found for: {account_address}")
825
718
  return {
826
719
  "free": 0.0,
827
720
  "reserved": 0.0,
@@ -831,9 +724,7 @@ class SubstrateClient:
831
724
  }
832
725
 
833
726
  except Exception as e:
834
- error_msg = f"Error querying account balance: {str(e)}"
835
- print(error_msg)
836
- raise ValueError(error_msg)
727
+ raise ValueError(f"Error querying account balance: {str(e)}")
837
728
 
838
729
  async def watch_account_balance(
839
730
  self, account_address: Optional[str] = None, interval: int = 5
@@ -858,10 +749,6 @@ class SubstrateClient:
858
749
  raise ValueError("No account address available")
859
750
  account_address = self._keypair.ss58_address
860
751
 
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
752
  # Keep track of previous balance to show changes
866
753
  previous_balance = None
867
754
 
@@ -942,27 +829,15 @@ class SubstrateClient:
942
829
  ValueError: If account has no credits
943
830
  """
944
831
  try:
945
- # Initialize Substrate connection if not already connected
946
- if not hasattr(self, "_substrate") or self._substrate is None:
947
- print("Initializing Substrate connection...")
948
- self._substrate = SubstrateInterface(
949
- url=self.url,
950
- ss58_format=42, # Substrate default
951
- type_registry_preset="substrate-node-template",
952
- )
953
- print(f"Connected to Substrate node at {self.url}")
832
+ # Initialize Substrate connection and get account address
833
+ substrate, derived_address = initialize_substrate_connection(self)
954
834
 
955
- # Use provided account address or default to keypair/configured address
835
+ # Use provided account address or the one derived from initialization
956
836
  if not account_address:
957
- if self._account_address:
958
- account_address = self._account_address
959
- print(f"Using account address: {account_address}")
837
+ if derived_address:
838
+ account_address = derived_address
960
839
  else:
961
- # Try to get the address from the keypair (requires seed phrase)
962
- if not self._ensure_keypair():
963
- raise ValueError("No account address available")
964
- account_address = self._keypair.ss58_address
965
- print(f"Using keypair address: {account_address}")
840
+ raise ValueError("No account address available")
966
841
 
967
842
  # Query the blockchain for free credits
968
843
  print(f"Querying free credits for account: {account_address}")
@@ -1006,30 +881,17 @@ class SubstrateClient:
1006
881
  ValueError: If query fails or no files found
1007
882
  """
1008
883
  try:
1009
- # Initialize Substrate connection if not already connected
1010
- if not hasattr(self, "_substrate") or self._substrate is None:
1011
- print("Initializing Substrate connection...")
1012
- self._substrate = SubstrateInterface(
1013
- url=self.url,
1014
- ss58_format=42, # Substrate default
1015
- type_registry_preset="substrate-node-template",
1016
- )
1017
- print(f"Connected to Substrate node at {self.url}")
884
+ # Initialize Substrate connection and get account address
885
+ substrate, derived_address = initialize_substrate_connection(self)
1018
886
 
1019
- # Use provided account address or default to keypair/configured address
887
+ # Use provided account address or the one derived from initialization
1020
888
  if not account_address:
1021
- if self._account_address:
1022
- account_address = self._account_address
1023
- print(f"Using account address: {account_address}")
889
+ if derived_address:
890
+ account_address = derived_address
1024
891
  else:
1025
- # Try to get the address from the keypair (requires seed phrase)
1026
- if not self._ensure_keypair():
1027
- raise ValueError("No account address available")
1028
- account_address = self._keypair.ss58_address
1029
- print(f"Using keypair address: {account_address}")
892
+ raise ValueError("No account address available")
1030
893
 
1031
894
  # Query the blockchain for user file hashes
1032
- print(f"Querying file hashes for account: {account_address}")
1033
895
  result = self._substrate.query(
1034
896
  module="Marketplace",
1035
897
  storage_function="UserFileHashes",
@@ -1040,16 +902,12 @@ class SubstrateClient:
1040
902
  if result.value:
1041
903
  # The result is already a list of bytes, convert each to string
1042
904
  file_hashes = [cid.hex() for cid in result.value]
1043
- print(f"Found {len(file_hashes)} files stored by this account")
1044
905
  return file_hashes
1045
906
  else:
1046
- print(f"No files found for account: {account_address}")
1047
907
  return []
1048
908
 
1049
909
  except Exception as e:
1050
- error_msg = f"Error querying user file hashes: {str(e)}"
1051
- print(error_msg)
1052
- raise ValueError(error_msg)
910
+ raise ValueError(f"Error querying user file hashes: {str(e)}")
1053
911
 
1054
912
  async def get_user_files(
1055
913
  self,
@@ -1074,7 +932,6 @@ class SubstrateClient:
1074
932
  "file_hash": str, # The IPFS CID of the file
1075
933
  "file_name": str, # The name of the file
1076
934
  "miner_ids": List[str], # List of miner IDs that have pinned the file
1077
- "miner_ids_full": List[str], # Complete list of miner IDs (if truncated)
1078
935
  "miner_count": int, # Total number of miners
1079
936
  "file_size": int, # Size of the file in bytes
1080
937
  "size_formatted": str # Human-readable file size
@@ -1108,90 +965,37 @@ class SubstrateClient:
1108
965
  ValueError: If query fails or profile cannot be retrieved
1109
966
  """
1110
967
  try:
1111
- # Initialize Substrate connection if not already connected
1112
- if not hasattr(self, "_substrate") or self._substrate is None:
1113
- print("Initializing Substrate connection...")
1114
- self._substrate = SubstrateInterface(
1115
- url=self.url,
1116
- ss58_format=42, # Substrate default
1117
- type_registry_preset="substrate-node-template",
1118
- )
1119
- print(f"Connected to Substrate node at {self.url}")
968
+ # Initialize Substrate connection and get account address
969
+ substrate, derived_address = initialize_substrate_connection(self)
1120
970
 
1121
- # Use provided account address or default to keypair/configured address
971
+ # Use provided account address or the one derived from initialization
1122
972
  if not account_address:
1123
- if self._account_address:
1124
- account_address = self._account_address
1125
- print(f"Using account address: {account_address}")
973
+ if derived_address:
974
+ account_address = derived_address
1126
975
  else:
1127
- # Try to get the address from the keypair (requires seed phrase)
1128
- if not self._ensure_keypair():
1129
- raise ValueError("No account address available")
1130
- account_address = self._keypair.ss58_address
1131
- print(f"Using keypair address: {account_address}")
976
+ raise ValueError("No account address available")
1132
977
 
1133
978
  # Query the blockchain for the user profile CID
1134
- print(f"Querying user profile for account: {account_address}")
1135
- result = self._substrate.query(
979
+ profile_hex_cid = self._substrate.query(
1136
980
  module="IpfsPallet",
1137
981
  storage_function="UserProfile",
1138
982
  params=[account_address],
1139
- )
983
+ ).value
1140
984
 
1141
- # Check if a profile was found
1142
- if not result.value:
1143
- print(f"No profile found for account: {account_address}")
985
+ if not profile_hex_cid:
1144
986
  return []
1145
987
 
1146
- # The result is a hex-encoded IPFS CID
1147
- # Handle both cases: bytes (needs .hex()) and string (already hex)
1148
- if isinstance(result.value, bytes):
1149
- hex_cid = result.value.hex()
1150
- else:
1151
- # If it's already a string, use it directly
1152
- hex_cid = result.value
1153
-
1154
- # Remove '0x' prefix if present
1155
- if hex_cid.startswith("0x"):
1156
- hex_cid = hex_cid[2:]
1157
-
1158
- print(f"Found user profile CID (hex): {hex_cid}")
1159
-
1160
- # Convert the hex CID to a readable IPFS CID
1161
- profile_cid = self._hex_to_ipfs_cid(hex_cid)
1162
- print(f"Decoded IPFS CID: {profile_cid}")
988
+ profile_cid = self._hex_to_ipfs_cid(profile_hex_cid)
1163
989
 
1164
990
  # Fetch the profile JSON from IPFS
1165
991
  # Defer import to avoid circular imports
1166
992
  from hippius_sdk.ipfs import IPFSClient
1167
993
 
1168
994
  ipfs_client = IPFSClient()
1169
-
1170
- print(f"Fetching user profile from IPFS: {profile_cid}")
1171
- profile_data = await ipfs_client.cat(profile_cid)
1172
-
1173
- # Parse the JSON content
1174
- if not profile_data.get("is_text", False):
1175
- raise ValueError("User profile is not in text format")
1176
-
1177
- profile_json = json.loads(profile_data.get("content", "{}"))
1178
- print(f"Successfully retrieved user profile")
1179
-
1180
- # Extract the file list from the profile
1181
- # The profile might be either a dictionary with a 'files' key or a direct list of files
1182
- files = []
1183
- if isinstance(profile_json, dict):
1184
- files = profile_json.get("files", [])
1185
- elif isinstance(profile_json, list):
1186
- # The profile itself might be a list of files
1187
- files = profile_json
1188
- else:
1189
- print(f"Warning: Unexpected profile structure: {type(profile_json)}")
1190
-
1191
- print(f"Found {len(files)} files in user profile")
1192
-
1193
- # Process the files to match the expected format
995
+ profile_content = (await ipfs_client.cat(profile_cid))["content"]
996
+ files = json.loads(profile_content)
1194
997
  processed_files = []
998
+
1195
999
  for file in files:
1196
1000
  # Make sure file is a dictionary
1197
1001
  if not isinstance(file, dict):
@@ -1208,37 +1012,10 @@ class SubstrateClient:
1208
1012
  try:
1209
1013
  # Convert array of numbers to bytes, then to a string
1210
1014
  file_hash = bytes(raw_file_hash).decode("utf-8")
1211
- except Exception:
1212
- pass
1213
- else:
1214
- # Try different field names for the CID that might be in the profile
1215
- file_hash = (
1216
- file.get("cid")
1217
- or file.get("hash")
1218
- or file.get("fileHash")
1219
- or raw_file_hash
1220
- )
1015
+ except Exception as e:
1016
+ print(e)
1221
1017
 
1222
1018
  # Handle file_name: could be an array of ASCII/UTF-8 code points
1223
- file_name = None
1224
- raw_file_name = file.get("file_name")
1225
- if isinstance(raw_file_name, list) and all(
1226
- isinstance(n, int) for n in raw_file_name
1227
- ):
1228
- try:
1229
- # Convert array of numbers to bytes, then to a string
1230
- file_name = bytes(raw_file_name).decode("utf-8")
1231
- except Exception:
1232
- pass
1233
- else:
1234
- # Try different field names for the filename
1235
- file_name = (
1236
- file.get("filename")
1237
- or file.get("name")
1238
- or file.get("fileName")
1239
- or raw_file_name
1240
- )
1241
-
1242
1019
  # Try different field names for the size
1243
1020
  file_size = (
1244
1021
  file.get("size")
@@ -1249,25 +1026,18 @@ class SubstrateClient:
1249
1026
  )
1250
1027
 
1251
1028
  processed_file = {
1029
+ "cid": self._hex_to_ipfs_cid(file_hash),
1252
1030
  "file_hash": file_hash,
1253
- "file_name": file_name,
1254
- # Add any other fields available in the profile
1255
- "miner_ids": file.get(
1256
- "miner_ids", []
1257
- ), # Try to get miners if available
1031
+ "file_name": file.get("file_name"),
1032
+ "miner_ids": file.get("miner_ids", []),
1258
1033
  "miner_count": len(file.get("miner_ids", [])), # Count the miners
1259
1034
  "file_size": file_size or 0,
1035
+ "selected_validator": file["selected_validator"],
1260
1036
  }
1261
1037
 
1262
1038
  # Add formatted file size if available
1263
1039
  if file_size:
1264
- size_bytes = file_size
1265
- if size_bytes >= 1024 * 1024:
1266
- processed_file[
1267
- "size_formatted"
1268
- ] = f"{size_bytes / (1024 * 1024):.2f} MB"
1269
- else:
1270
- processed_file["size_formatted"] = f"{size_bytes / 1024:.2f} KB"
1040
+ processed_file["size_formatted"] = format_size(file_size)
1271
1041
  else:
1272
1042
  processed_file["size_formatted"] = "Unknown"
1273
1043
 
@@ -1276,9 +1046,7 @@ class SubstrateClient:
1276
1046
  return processed_files
1277
1047
 
1278
1048
  except Exception as e:
1279
- error_msg = f"Error retrieving user files from profile: {str(e)}"
1280
- print(error_msg)
1281
- raise ValueError(error_msg)
1049
+ raise ValueError(f"Error retrieving user files from profile: {str(e)}")
1282
1050
 
1283
1051
  def get_pinning_status(
1284
1052
  self, account_address: Optional[str] = None
@@ -1311,165 +1079,48 @@ class SubstrateClient:
1311
1079
  ConnectionError: If connection to Substrate fails
1312
1080
  ValueError: If query fails or no requests found
1313
1081
  """
1314
- try:
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}")
1082
+ # Initialize Substrate connection and get account address
1083
+ substrate, derived_address = initialize_substrate_connection(self)
1324
1084
 
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}")
1085
+ # Use provided account address or the one derived from initialization
1086
+ if not account_address:
1087
+ if derived_address:
1088
+ account_address = derived_address
1089
+ else:
1090
+ raise ValueError("No account address available")
1091
+
1092
+ # Query the blockchain for storage requests
1093
+ print(f"Querying storage requests for account: {account_address}")
1094
+ storage_requests = []
1095
+
1096
+ # First, try with query_map which is more suitable for iterating over collections
1097
+ result = self._substrate.query_map(
1098
+ module="IpfsPallet",
1099
+ storage_function="UserStorageRequests",
1100
+ params=[account_address],
1101
+ )
1102
+
1103
+ results_list = list(result)
1104
+ for key, substrate_result in results_list:
1105
+ # Extract file hash from key if possible
1106
+ file_hash_hex = None
1107
+ if key is not None:
1108
+ if hasattr(key, "hex"):
1109
+ file_hash_hex = key.hex()
1110
+ elif isinstance(key, bytes):
1111
+ file_hash_hex = key.hex()
1112
+ elif isinstance(key, str) and key.startswith("0x"):
1113
+ file_hash_hex = key[2:]
1330
1114
  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
- )
1359
-
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 []
1383
-
1384
- print(f"Found {len(results_list)} storage request entries")
1115
+ file_hash_hex = str(key)
1385
1116
 
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)
1117
+ if file_hash_hex:
1118
+ file_cid = self._hex_to_ipfs_cid(file_hash_hex)
1119
+ substrate_result.value["cid"] = file_cid
1462
1120
 
1463
- except Exception as e:
1464
- print(f"Error processing request entry {i+1}: {e}")
1121
+ storage_requests.append(substrate_result.value)
1465
1122
 
1466
- print(f"Successfully processed {len(storage_requests)} storage requests")
1467
- return storage_requests
1468
-
1469
- except Exception as e:
1470
- error_msg = f"Error querying storage requests: {str(e)}"
1471
- print(error_msg)
1472
- raise ValueError(error_msg)
1123
+ return storage_requests
1473
1124
 
1474
1125
  def _hex_to_ipfs_cid(self, hex_string: str) -> str:
1475
1126
  """
@@ -1482,3 +1133,42 @@ class SubstrateClient:
1482
1133
  str: Regular IPFS CID
1483
1134
  """
1484
1135
  return hex_to_ipfs_cid(hex_string)
1136
+
1137
+ async def cancel_storage_request(self, cid: str) -> str:
1138
+ """
1139
+ Cancel a storage request by CID from the Hippius blockchain.
1140
+
1141
+ Args:
1142
+ cid: Content Identifier (CID) of the file to cancel
1143
+
1144
+ Returns:
1145
+ str: Transaction hash
1146
+ """
1147
+ if not self._ensure_keypair():
1148
+ raise ValueError("Seed phrase must be set before making transactions")
1149
+
1150
+ substrate, _ = initialize_substrate_connection(self)
1151
+
1152
+ call = self._substrate.compose_call(
1153
+ call_module="Marketplace",
1154
+ call_function="storage_unpin_request",
1155
+ call_params={
1156
+ "file_hash": cid,
1157
+ },
1158
+ )
1159
+
1160
+ # Get payment info and show estimated transaction fee
1161
+ payment_info = self._substrate.get_payment_info(
1162
+ call=call, keypair=self._keypair
1163
+ )
1164
+ print(f"Payment info: {json.dumps(payment_info, indent=2)}")
1165
+ fee = payment_info.get("partialFee", 0)
1166
+ fee_tokens = fee / 10**12 if fee > 0 else 0
1167
+ print(f"Estimated transaction fee: {fee} ({fee_tokens:.10f} tokens)")
1168
+
1169
+ extrinsic = self._substrate.create_signed_extrinsic(
1170
+ call=call, keypair=self._keypair
1171
+ )
1172
+ response = self._substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
1173
+ print(f"Transaction hash: {response.extrinsic_hash}")
1174
+ return response.extrinsic_hash