hippius 0.2.2__py3-none-any.whl → 0.2.4__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
@@ -10,16 +11,12 @@ from dotenv import load_dotenv
10
11
  from mnemonic import Mnemonic
11
12
  from substrateinterface import Keypair, SubstrateInterface
12
13
 
13
- from hippius_sdk.config import (
14
- get_account_address,
15
- get_active_account,
16
- get_all_config,
17
- get_config_value,
18
- get_seed_phrase,
19
- set_active_account,
20
- set_seed_phrase,
21
- )
22
- from hippius_sdk.utils import hex_to_ipfs_cid
14
+ from hippius_sdk.config import (get_account_address, get_active_account,
15
+ get_all_config, get_config_value,
16
+ get_seed_phrase, set_active_account,
17
+ set_seed_phrase)
18
+ from hippius_sdk.utils import (format_size, hex_to_ipfs_cid,
19
+ initialize_substrate_connection)
23
20
 
24
21
  # Load environment variables
25
22
  load_dotenv()
@@ -128,16 +125,12 @@ class SubstrateClient:
128
125
  print("Connected successfully (read-only mode, no account)")
129
126
  self._read_only = True
130
127
 
131
- return True
132
-
133
128
  except Exception as e:
134
129
  print(f"Failed to connect to Substrate node: {e}")
135
130
  raise ConnectionError(
136
131
  f"Could not connect to Substrate node at {self.url}: {e}"
137
132
  )
138
133
 
139
- return False
140
-
141
134
  def _ensure_keypair(self) -> bool:
142
135
  """
143
136
  Ensure we have a keypair for signing transactions.
@@ -154,7 +147,6 @@ class SubstrateClient:
154
147
  try:
155
148
  self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
156
149
  self._account_address = self._keypair.ss58_address
157
- print(f"Keypair created for account: {self._keypair.ss58_address}")
158
150
  self._read_only = False
159
151
  return True
160
152
  except Exception as e:
@@ -162,22 +154,14 @@ class SubstrateClient:
162
154
  return False
163
155
 
164
156
  # Otherwise, try to get the seed phrase from config
165
- try:
166
- config_seed = get_seed_phrase(
167
- self._seed_phrase_password, self._account_name
168
- )
169
- if config_seed:
170
- self._seed_phrase = config_seed
171
- self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
172
- self._account_address = self._keypair.ss58_address
173
- print(f"Keypair created for account: {self._keypair.ss58_address}")
174
- self._read_only = False
175
- return True
176
- else:
177
- print("No seed phrase available. Cannot sign transactions.")
178
- return False
179
- except Exception as e:
180
- print(f"Warning: Could not get seed phrase from config: {e}")
157
+ config_seed = get_seed_phrase(self._seed_phrase_password, self._account_name)
158
+ if config_seed:
159
+ self._seed_phrase = config_seed
160
+ self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
161
+ self._account_address = self._keypair.ss58_address
162
+ self._read_only = False
163
+ return True
164
+ else:
181
165
  return False
182
166
 
183
167
  def generate_mnemonic(self) -> str:
@@ -193,6 +177,15 @@ class SubstrateClient:
193
177
  except Exception as e:
194
178
  raise ValueError(f"Error generating mnemonic: {e}")
195
179
 
180
+ def generate_seed_phrase(self) -> str:
181
+ """
182
+ Generate a new random seed phrase (alias for generate_mnemonic).
183
+
184
+ Returns:
185
+ str: A 12-word mnemonic seed phrase
186
+ """
187
+ return self.generate_mnemonic()
188
+
196
189
  def create_account(
197
190
  self, name: str, encode: bool = False, password: Optional[str] = None
198
191
  ) -> Dict[str, Any]:
@@ -201,7 +194,7 @@ class SubstrateClient:
201
194
 
202
195
  Args:
203
196
  name: Name for the new account
204
- encode: Whether to encrypt the seed phrase with a password
197
+ encode: Whether to encrypt the seed phrase with a password.
205
198
  password: Optional password for encryption (will prompt if not provided and encode=True)
206
199
 
207
200
  Returns:
@@ -431,9 +424,9 @@ class SubstrateClient:
431
424
  account_info["storage_stats"] = {
432
425
  "files": total_files,
433
426
  "bytes_used": total_size_bytes,
434
- "size_formatted": self._format_size(total_size_bytes)
427
+ "size_formatted": format_size(total_size_bytes)
435
428
  if total_size_bytes
436
- else "0 B",
429
+ else "0 bytes",
437
430
  }
438
431
 
439
432
  # Include file list if requested
@@ -463,17 +456,6 @@ class SubstrateClient:
463
456
 
464
457
  return account_info
465
458
 
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
459
  def set_seed_phrase(self, seed_phrase: str) -> None:
478
460
  """
479
461
  Set or update the seed phrase used for signing transactions.
@@ -492,10 +474,8 @@ class SubstrateClient:
492
474
  try:
493
475
  self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
494
476
  self._account_address = self._keypair.ss58_address
495
- print(f"Keypair created for account: {self._keypair.ss58_address}")
496
477
  except Exception as e:
497
- print(f"Warning: Could not create keypair from seed phrase: {e}")
498
- print(f"Keypair will be created when needed")
478
+ raise ValueError(f"Could not create keypair from seed phrase: {e}")
499
479
 
500
480
  async def storage_request(
501
481
  self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
@@ -539,147 +519,119 @@ class SubstrateClient:
539
519
  file_inputs.append(file)
540
520
 
541
521
  # Print what is being submitted
542
- print(f"Preparing storage request for {len(file_inputs)} files:")
543
522
  for file in file_inputs:
544
523
  print(f" - {file.file_name}: {file.file_hash}")
545
524
 
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
- )
568
-
569
- # Convert to JSON
570
- files_json = json.dumps(file_list, indent=2)
571
- print(f"Created file list with {len(file_list)} entries")
525
+ # Initialize Substrate connection
526
+ substrate, _ = initialize_substrate_connection(self)
572
527
 
573
- # Step 2: Upload the JSON file to IPFS
574
- # Defer import to avoid circular imports
575
- from hippius_sdk.ipfs import IPFSClient
528
+ # Step 1: Create a JSON file with the list of files to pin
529
+ file_list = []
530
+ for file_input in file_inputs:
531
+ file_list.append(
532
+ {"filename": file_input.file_name, "cid": file_input.file_hash}
533
+ )
576
534
 
577
- ipfs_client = IPFSClient()
535
+ # Convert to JSON
536
+ files_json = json.dumps(file_list, indent=2)
537
+ print(f"Created file list with {len(file_list)} entries")
578
538
 
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)
539
+ # Step 2: Upload the JSON file to IPFS
540
+ # Defer import to avoid circular imports
541
+ from hippius_sdk.ipfs import IPFSClient
585
542
 
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
- }
543
+ ipfs_client = IPFSClient()
607
544
 
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...")
545
+ # Create a temporary file with the JSON content
546
+ with tempfile.NamedTemporaryFile(
547
+ mode="w+", suffix=".json", delete=False
548
+ ) as temp_file:
549
+ temp_file_path = temp_file.name
550
+ temp_file.write(files_json)
619
551
 
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
552
+ try:
553
+ print("Uploading file list to IPFS...")
554
+ upload_result = await ipfs_client.upload_file(temp_file_path)
555
+ files_list_cid = upload_result["cid"]
556
+ print(f"File list uploaded to IPFS with CID: {files_list_cid}")
557
+ finally:
558
+ # Clean up the temporary file
559
+ if os.path.exists(temp_file_path):
560
+ os.remove(temp_file_path)
561
+
562
+ # Step 3: Submit the CID of the JSON file to the chain
563
+ # Create call parameters with the CID of the JSON file
564
+ call_params = {
565
+ "files_input": [
566
+ {
567
+ "file_hash": files_list_cid,
568
+ "file_name": f"files_list_{uuid.uuid4()}", # Generate a unique ID
624
569
  }
625
- call = self._substrate.compose_call(
626
- call_module="IpfsPallet",
627
- call_function="storeFile",
628
- call_params=alt_call_params,
629
- )
570
+ ],
571
+ "miner_ids": miner_ids if miner_ids else [],
572
+ }
630
573
 
631
- # Get payment info to estimate the fee
632
- payment_info = self._substrate.get_payment_info(
633
- call=call, keypair=self._keypair
574
+ # Create the call to the marketplace
575
+ try:
576
+ call = self._substrate.compose_call(
577
+ call_module="Marketplace",
578
+ call_function="storage_request",
579
+ call_params=call_params,
634
580
  )
581
+ except Exception as e:
582
+ print(f"Warning: Error composing call: {e}")
583
+ print("Attempting to use IpfsPallet.storeFile instead...")
635
584
 
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)"
585
+ # Try with IpfsPallet.storeFile as an alternative
586
+ alt_call_params = {
587
+ "fileHash": files_list_cid,
588
+ "fileName": f"files_list_{uuid.uuid4()}", # Generate a unique ID
589
+ }
590
+ call = self._substrate.compose_call(
591
+ call_module="IpfsPallet",
592
+ call_function="storeFile",
593
+ call_params=alt_call_params,
645
594
  )
646
595
 
647
- # Create a signed extrinsic
648
- extrinsic = self._substrate.create_signed_extrinsic(
649
- call=call, keypair=self._keypair
650
- )
596
+ # Get payment info to estimate the fee
597
+ payment_info = self._substrate.get_payment_info(
598
+ call=call, keypair=self._keypair
599
+ )
651
600
 
652
- print(
653
- f"Submitting transaction to store {len(file_list)} files via file list CID..."
654
- )
601
+ print(f"Payment info: {json.dumps(payment_info, indent=2)}")
655
602
 
656
- # Submit the transaction
657
- response = self._substrate.submit_extrinsic(
658
- extrinsic=extrinsic, wait_for_inclusion=True
659
- )
603
+ # Convert partialFee from Substrate (10^18 units) to a more readable format
604
+ estimated_fee = payment_info.get("partialFee", 0)
605
+ estimated_fee_formatted = (
606
+ float(estimated_fee) / 1_000_000_000_000_000_000 if estimated_fee else 0
607
+ )
608
+ print(
609
+ f"Estimated transaction fee: {estimated_fee} ({estimated_fee_formatted:.10f} tokens)"
610
+ )
660
611
 
661
- # Get the transaction hash
662
- tx_hash = response.extrinsic_hash
612
+ # Create a signed extrinsic
613
+ extrinsic = self._substrate.create_signed_extrinsic(
614
+ call=call, keypair=self._keypair
615
+ )
663
616
 
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")
617
+ print(
618
+ f"Submitting transaction to store {len(file_list)} files via file list CID..."
619
+ )
668
620
 
669
- return tx_hash
621
+ # Submit the transaction
622
+ response = self._substrate.submit_extrinsic(
623
+ extrinsic=extrinsic, wait_for_inclusion=True
624
+ )
670
625
 
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
626
+ # Get the transaction hash
627
+ tx_hash = response.extrinsic_hash
678
628
 
679
- return "simulated-tx-hash"
629
+ return tx_hash
680
630
 
681
631
  async def store_cid(
682
- self, cid: str, filename: str = None, metadata: Optional[Dict[str, Any]] = None
632
+ self,
633
+ cid: str,
634
+ filename: str = None,
683
635
  ) -> str:
684
636
  """
685
637
  Store a CID on the blockchain.
@@ -687,7 +639,6 @@ class SubstrateClient:
687
639
  Args:
688
640
  cid: Content Identifier (CID) to store
689
641
  filename: Original filename (optional)
690
- metadata: Additional metadata to store with the CID
691
642
 
692
643
  Returns:
693
644
  str: Transaction hash
@@ -695,58 +646,6 @@ class SubstrateClient:
695
646
  file_input = FileInput(file_hash=cid, file_name=filename or "unnamed_file")
696
647
  return await self.storage_request([file_input])
697
648
 
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
649
  async def get_account_balance(
751
650
  self, account_address: Optional[str] = None
752
651
  ) -> Dict[str, float]:
@@ -760,30 +659,17 @@ class SubstrateClient:
760
659
  Dict[str, float]: Account balances (free, reserved, total)
761
660
  """
762
661
  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}")
662
+ # Initialize Substrate connection and get account address
663
+ substrate, derived_address = initialize_substrate_connection(self)
772
664
 
773
- # Use provided account address or default to keypair/configured address
665
+ # Use provided account address or the one derived from initialization
774
666
  if not account_address:
775
- if self._account_address:
776
- account_address = self._account_address
777
- print(f"Using account address: {account_address}")
667
+ if derived_address:
668
+ account_address = derived_address
778
669
  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}")
670
+ raise ValueError("No account address available")
784
671
 
785
672
  # Query the blockchain for account balance
786
- print(f"Querying balance for account: {account_address}")
787
673
  result = self._substrate.query(
788
674
  module="System",
789
675
  storage_function="Account",
@@ -793,7 +679,6 @@ class SubstrateClient:
793
679
  # If account exists, extract the balance information
794
680
  if result.value:
795
681
  data = result.value
796
- print(data)
797
682
  # Extract balance components
798
683
  free_balance = data.get("data", {}).get("free", 0)
799
684
  reserved_balance = data.get("data", {}).get("reserved", 0)
@@ -821,7 +706,6 @@ class SubstrateClient:
821
706
  },
822
707
  }
823
708
  else:
824
- print(f"No account data found for: {account_address}")
825
709
  return {
826
710
  "free": 0.0,
827
711
  "reserved": 0.0,
@@ -831,9 +715,7 @@ class SubstrateClient:
831
715
  }
832
716
 
833
717
  except Exception as e:
834
- error_msg = f"Error querying account balance: {str(e)}"
835
- print(error_msg)
836
- raise ValueError(error_msg)
718
+ raise ValueError(f"Error querying account balance: {str(e)}")
837
719
 
838
720
  async def watch_account_balance(
839
721
  self, account_address: Optional[str] = None, interval: int = 5
@@ -858,10 +740,6 @@ class SubstrateClient:
858
740
  raise ValueError("No account address available")
859
741
  account_address = self._keypair.ss58_address
860
742
 
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
743
  # Keep track of previous balance to show changes
866
744
  previous_balance = None
867
745
 
@@ -942,27 +820,15 @@ class SubstrateClient:
942
820
  ValueError: If account has no credits
943
821
  """
944
822
  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}")
823
+ # Initialize Substrate connection and get account address
824
+ substrate, derived_address = initialize_substrate_connection(self)
954
825
 
955
- # Use provided account address or default to keypair/configured address
826
+ # Use provided account address or the one derived from initialization
956
827
  if not account_address:
957
- if self._account_address:
958
- account_address = self._account_address
959
- print(f"Using account address: {account_address}")
828
+ if derived_address:
829
+ account_address = derived_address
960
830
  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}")
831
+ raise ValueError("No account address available")
966
832
 
967
833
  # Query the blockchain for free credits
968
834
  print(f"Querying free credits for account: {account_address}")
@@ -1006,30 +872,17 @@ class SubstrateClient:
1006
872
  ValueError: If query fails or no files found
1007
873
  """
1008
874
  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}")
875
+ # Initialize Substrate connection and get account address
876
+ substrate, derived_address = initialize_substrate_connection(self)
1018
877
 
1019
- # Use provided account address or default to keypair/configured address
878
+ # Use provided account address or the one derived from initialization
1020
879
  if not account_address:
1021
- if self._account_address:
1022
- account_address = self._account_address
1023
- print(f"Using account address: {account_address}")
880
+ if derived_address:
881
+ account_address = derived_address
1024
882
  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}")
883
+ raise ValueError("No account address available")
1030
884
 
1031
885
  # Query the blockchain for user file hashes
1032
- print(f"Querying file hashes for account: {account_address}")
1033
886
  result = self._substrate.query(
1034
887
  module="Marketplace",
1035
888
  storage_function="UserFileHashes",
@@ -1040,16 +893,12 @@ class SubstrateClient:
1040
893
  if result.value:
1041
894
  # The result is already a list of bytes, convert each to string
1042
895
  file_hashes = [cid.hex() for cid in result.value]
1043
- print(f"Found {len(file_hashes)} files stored by this account")
1044
896
  return file_hashes
1045
897
  else:
1046
- print(f"No files found for account: {account_address}")
1047
898
  return []
1048
899
 
1049
900
  except Exception as e:
1050
- error_msg = f"Error querying user file hashes: {str(e)}"
1051
- print(error_msg)
1052
- raise ValueError(error_msg)
901
+ raise ValueError(f"Error querying user file hashes: {str(e)}")
1053
902
 
1054
903
  async def get_user_files(
1055
904
  self,
@@ -1074,7 +923,6 @@ class SubstrateClient:
1074
923
  "file_hash": str, # The IPFS CID of the file
1075
924
  "file_name": str, # The name of the file
1076
925
  "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
926
  "miner_count": int, # Total number of miners
1079
927
  "file_size": int, # Size of the file in bytes
1080
928
  "size_formatted": str # Human-readable file size
@@ -1108,90 +956,37 @@ class SubstrateClient:
1108
956
  ValueError: If query fails or profile cannot be retrieved
1109
957
  """
1110
958
  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}")
959
+ # Initialize Substrate connection and get account address
960
+ substrate, derived_address = initialize_substrate_connection(self)
1120
961
 
1121
- # Use provided account address or default to keypair/configured address
962
+ # Use provided account address or the one derived from initialization
1122
963
  if not account_address:
1123
- if self._account_address:
1124
- account_address = self._account_address
1125
- print(f"Using account address: {account_address}")
964
+ if derived_address:
965
+ account_address = derived_address
1126
966
  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}")
967
+ raise ValueError("No account address available")
1132
968
 
1133
969
  # Query the blockchain for the user profile CID
1134
- print(f"Querying user profile for account: {account_address}")
1135
- result = self._substrate.query(
970
+ profile_hex_cid = self._substrate.query(
1136
971
  module="IpfsPallet",
1137
972
  storage_function="UserProfile",
1138
973
  params=[account_address],
1139
- )
974
+ ).value
1140
975
 
1141
- # Check if a profile was found
1142
- if not result.value:
1143
- print(f"No profile found for account: {account_address}")
976
+ if not profile_hex_cid:
1144
977
  return []
1145
978
 
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}")
979
+ profile_cid = self._hex_to_ipfs_cid(profile_hex_cid)
1163
980
 
1164
981
  # Fetch the profile JSON from IPFS
1165
982
  # Defer import to avoid circular imports
1166
983
  from hippius_sdk.ipfs import IPFSClient
1167
984
 
1168
985
  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
986
+ profile_content = (await ipfs_client.cat(profile_cid))["content"]
987
+ files = json.loads(profile_content)
1194
988
  processed_files = []
989
+
1195
990
  for file in files:
1196
991
  # Make sure file is a dictionary
1197
992
  if not isinstance(file, dict):
@@ -1208,37 +1003,10 @@ class SubstrateClient:
1208
1003
  try:
1209
1004
  # Convert array of numbers to bytes, then to a string
1210
1005
  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
- )
1006
+ except Exception as e:
1007
+ print(e)
1221
1008
 
1222
1009
  # 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
1010
  # Try different field names for the size
1243
1011
  file_size = (
1244
1012
  file.get("size")
@@ -1249,25 +1017,18 @@ class SubstrateClient:
1249
1017
  )
1250
1018
 
1251
1019
  processed_file = {
1020
+ "cid": self._hex_to_ipfs_cid(file_hash),
1252
1021
  "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
1022
+ "file_name": file.get("file_name"),
1023
+ "miner_ids": file.get("miner_ids", []),
1258
1024
  "miner_count": len(file.get("miner_ids", [])), # Count the miners
1259
1025
  "file_size": file_size or 0,
1026
+ "selected_validator": file["selected_validator"],
1260
1027
  }
1261
1028
 
1262
1029
  # Add formatted file size if available
1263
1030
  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"
1031
+ processed_file["size_formatted"] = format_size(file_size)
1271
1032
  else:
1272
1033
  processed_file["size_formatted"] = "Unknown"
1273
1034
 
@@ -1276,9 +1037,7 @@ class SubstrateClient:
1276
1037
  return processed_files
1277
1038
 
1278
1039
  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)
1040
+ raise ValueError(f"Error retrieving user files from profile: {str(e)}")
1282
1041
 
1283
1042
  def get_pinning_status(
1284
1043
  self, account_address: Optional[str] = None
@@ -1311,165 +1070,48 @@ class SubstrateClient:
1311
1070
  ConnectionError: If connection to Substrate fails
1312
1071
  ValueError: If query fails or no requests found
1313
1072
  """
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}")
1073
+ # Initialize Substrate connection and get account address
1074
+ substrate, derived_address = initialize_substrate_connection(self)
1324
1075
 
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}")
1076
+ # Use provided account address or the one derived from initialization
1077
+ if not account_address:
1078
+ if derived_address:
1079
+ account_address = derived_address
1080
+ else:
1081
+ raise ValueError("No account address available")
1082
+
1083
+ # Query the blockchain for storage requests
1084
+ print(f"Querying storage requests for account: {account_address}")
1085
+ storage_requests = []
1086
+
1087
+ # First, try with query_map which is more suitable for iterating over collections
1088
+ result = self._substrate.query_map(
1089
+ module="IpfsPallet",
1090
+ storage_function="UserStorageRequests",
1091
+ params=[account_address],
1092
+ )
1093
+
1094
+ results_list = list(result)
1095
+ for key, substrate_result in results_list:
1096
+ # Extract file hash from key if possible
1097
+ file_hash_hex = None
1098
+ if key is not None:
1099
+ if hasattr(key, "hex"):
1100
+ file_hash_hex = key.hex()
1101
+ elif isinstance(key, bytes):
1102
+ file_hash_hex = key.hex()
1103
+ elif isinstance(key, str) and key.startswith("0x"):
1104
+ file_hash_hex = key[2:]
1330
1105
  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}")
1106
+ file_hash_hex = str(key)
1336
1107
 
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
- )
1108
+ if file_hash_hex:
1109
+ file_cid = self._hex_to_ipfs_cid(file_hash_hex)
1110
+ substrate_result.value["cid"] = file_cid
1359
1111
 
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 []
1112
+ storage_requests.append(substrate_result.value)
1383
1113
 
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
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)
1114
+ return storage_requests
1473
1115
 
1474
1116
  def _hex_to_ipfs_cid(self, hex_string: str) -> str:
1475
1117
  """
@@ -1482,3 +1124,42 @@ class SubstrateClient:
1482
1124
  str: Regular IPFS CID
1483
1125
  """
1484
1126
  return hex_to_ipfs_cid(hex_string)
1127
+
1128
+ async def cancel_storage_request(self, cid: str) -> str:
1129
+ """
1130
+ Cancel a storage request by CID from the Hippius blockchain.
1131
+
1132
+ Args:
1133
+ cid: Content Identifier (CID) of the file to cancel
1134
+
1135
+ Returns:
1136
+ str: Transaction hash
1137
+ """
1138
+ if not self._ensure_keypair():
1139
+ raise ValueError("Seed phrase must be set before making transactions")
1140
+
1141
+ substrate, _ = initialize_substrate_connection(self)
1142
+
1143
+ call = self._substrate.compose_call(
1144
+ call_module="Marketplace",
1145
+ call_function="storage_unpin_request",
1146
+ call_params={
1147
+ "file_hash": cid,
1148
+ },
1149
+ )
1150
+
1151
+ # Get payment info and show estimated transaction fee
1152
+ payment_info = self._substrate.get_payment_info(
1153
+ call=call, keypair=self._keypair
1154
+ )
1155
+ print(f"Payment info: {json.dumps(payment_info, indent=2)}")
1156
+ fee = payment_info.get("partialFee", 0)
1157
+ fee_tokens = fee / 10**12 if fee > 0 else 0
1158
+ print(f"Estimated transaction fee: {fee} ({fee_tokens:.10f} tokens)")
1159
+
1160
+ extrinsic = self._substrate.create_signed_extrinsic(
1161
+ call=call, keypair=self._keypair
1162
+ )
1163
+ response = self._substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
1164
+ print(f"Transaction hash: {response.extrinsic_hash}")
1165
+ return response.extrinsic_hash