hippius 0.1.7__py3-none-any.whl → 0.1.10__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/client.py CHANGED
@@ -49,7 +49,7 @@ class HippiusClient:
49
49
 
50
50
  if ipfs_api_url is None:
51
51
  ipfs_api_url = get_config_value(
52
- "ipfs", "api_url", "https://relay-fr.hippius.network"
52
+ "ipfs", "api_url", "https://store.hippius.network"
53
53
  )
54
54
 
55
55
  # Check if local IPFS is enabled in config
hippius_sdk/config.py CHANGED
@@ -19,7 +19,7 @@ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
19
19
  DEFAULT_CONFIG = {
20
20
  "ipfs": {
21
21
  "gateway": "https://ipfs.io",
22
- "api_url": "https://relay-fr.hippius.network",
22
+ "api_url": "https://store.hippius.network",
23
23
  "local_ipfs": False,
24
24
  },
25
25
  "substrate": {
@@ -740,5 +740,76 @@ def reset_config() -> bool:
740
740
  return save_config(DEFAULT_CONFIG.copy())
741
741
 
742
742
 
743
+ def get_keypair(
744
+ ss58_address: Optional[str] = None, account_name: Optional[str] = None
745
+ ) -> "Keypair":
746
+ """
747
+ Get a Keypair object for a given SS58 address or account name.
748
+
749
+ This function will attempt to find the specified account and generate
750
+ a Keypair object using the stored seed phrase.
751
+
752
+ Args:
753
+ ss58_address: SS58 address of the account
754
+ account_name: Name of the account (used if ss58_address is None)
755
+
756
+ Returns:
757
+ Keypair: A substrate Keypair object
758
+
759
+ Raises:
760
+ ValueError: If the account cannot be found or if the seed phrase is not available
761
+ ImportError: If the required dependencies are not installed
762
+ """
763
+ # Import here to avoid circular imports
764
+ try:
765
+ from substrateinterface import Keypair
766
+ except ImportError:
767
+ raise ImportError(
768
+ "Substrate interface is required to get a keypair. "
769
+ "Install with: pip install substrate-interface"
770
+ )
771
+
772
+ # If ss58_address is provided, look for a matching account
773
+ if ss58_address:
774
+ accounts = list_accounts()
775
+ found_account = None
776
+
777
+ for name, data in accounts.items():
778
+ if data.get("ss58_address") == ss58_address:
779
+ found_account = name
780
+ break
781
+
782
+ if found_account:
783
+ account_name = found_account
784
+ else:
785
+ raise ValueError(f"No account found with SS58 address: {ss58_address}")
786
+
787
+ # If no account_name by this point, use the active account
788
+ if not account_name:
789
+ account_name = get_active_account()
790
+ if not account_name:
791
+ raise ValueError(
792
+ "No account specified and no active account. "
793
+ "Set an active account with: hippius account switch <account_name>"
794
+ )
795
+
796
+ # Get the seed phrase for the account
797
+ seed_phrase = get_seed_phrase(account_name=account_name)
798
+ if not seed_phrase:
799
+ if get_config_value("substrate", "seed_phrase_encoded"):
800
+ raise ValueError(
801
+ f"The seed phrase for account '{account_name}' is encrypted. "
802
+ f"Please decrypt it first with: hippius seed decode --account {account_name}"
803
+ )
804
+ else:
805
+ raise ValueError(
806
+ f"No seed phrase found for account '{account_name}'. "
807
+ f'Set one with: hippius seed set "your seed phrase" --account {account_name}'
808
+ )
809
+
810
+ # Create and return the keypair
811
+ return Keypair.create_from_mnemonic(seed_phrase)
812
+
813
+
743
814
  # Initialize configuration on module import
744
815
  ensure_config_dir()
hippius_sdk/ipfs.py CHANGED
@@ -62,7 +62,7 @@ class IPFSClient:
62
62
 
63
63
  if api_url is None:
64
64
  api_url = get_config_value(
65
- "ipfs", "api_url", "https://relay-fr.hippius.network"
65
+ "ipfs", "api_url", "https://store.hippius.network"
66
66
  )
67
67
 
68
68
  # Check if local IPFS is enabled in config
hippius_sdk/substrate.py CHANGED
@@ -6,6 +6,7 @@ Note: This functionality is coming soon and not implemented yet.
6
6
 
7
7
  import os
8
8
  import json
9
+ import uuid
9
10
  from typing import Dict, Any, Optional, List, Union
10
11
  from substrateinterface import SubstrateInterface, Keypair
11
12
  from dotenv import load_dotenv
@@ -205,8 +206,8 @@ class SubstrateClient:
205
206
  """
206
207
  Submit a storage request for IPFS files to the marketplace.
207
208
 
208
- This method batches all files into a single transaction to efficiently store
209
- multiple files at once.
209
+ This method creates a JSON file with the list of files to pin, uploads it to IPFS,
210
+ and submits the CID of this file to the chain.
210
211
 
211
212
  Args:
212
213
  files: List of FileInput objects or dictionaries with fileHash and fileName
@@ -241,7 +242,7 @@ class SubstrateClient:
241
242
  file_inputs.append(file)
242
243
 
243
244
  # Print what is being submitted
244
- print(f"Submitting storage request for {len(file_inputs)} files as a batch:")
245
+ print(f"Preparing storage request for {len(file_inputs)} files:")
245
246
  for file in file_inputs:
246
247
  print(f" - {file.file_name}: {file.file_hash}")
247
248
 
@@ -261,22 +262,50 @@ class SubstrateClient:
261
262
  )
262
263
  print(f"Connected to Substrate node at {self.url}")
263
264
 
264
- # Format files for the batch call - all files are included in a single array
265
- formatted_files = []
265
+ # Step 1: Create a JSON file with the list of files to pin
266
+ file_list = []
266
267
  for file_input in file_inputs:
267
- formatted_files.append(
268
- {
269
- "file_hash": file_input.file_hash,
270
- "file_name": file_input.file_name,
271
- }
268
+ file_list.append(
269
+ {"filename": file_input.file_name, "cid": file_input.file_hash}
272
270
  )
273
271
 
274
- # Create call parameters with all files in a single batch
272
+ # Convert to JSON
273
+ files_json = json.dumps(file_list, indent=2)
274
+ print(f"Created file list with {len(file_list)} entries")
275
+
276
+ # Step 2: Upload the JSON file to IPFS
277
+ import tempfile
278
+ from hippius_sdk.ipfs import IPFSClient
279
+
280
+ ipfs_client = IPFSClient()
281
+
282
+ # Create a temporary file with the JSON content
283
+ with tempfile.NamedTemporaryFile(
284
+ mode="w+", suffix=".json", delete=False
285
+ ) as temp_file:
286
+ temp_file_path = temp_file.name
287
+ temp_file.write(files_json)
288
+
289
+ try:
290
+ print("Uploading file list to IPFS...")
291
+ upload_result = ipfs_client.upload_file(temp_file_path)
292
+ files_list_cid = upload_result["cid"]
293
+ print(f"File list uploaded to IPFS with CID: {files_list_cid}")
294
+ finally:
295
+ # Clean up the temporary file
296
+ if os.path.exists(temp_file_path):
297
+ os.remove(temp_file_path)
298
+
299
+ # Step 3: Submit the CID of the JSON file to the chain
300
+ # Create call parameters with the CID of the JSON file
275
301
  call_params = {
276
- "files_input": formatted_files,
277
- "miner_ids": miner_ids
278
- if miner_ids
279
- else [], # Always include miner_ids, empty array if not specified
302
+ "files_input": [
303
+ {
304
+ "file_hash": files_list_cid,
305
+ "file_name": f"files_list_{uuid.uuid4()}", # Generate a unique ID
306
+ }
307
+ ],
308
+ "miner_ids": miner_ids if miner_ids else [],
280
309
  }
281
310
 
282
311
  # Create the call to the marketplace
@@ -300,7 +329,9 @@ class SubstrateClient:
300
329
  call=call, keypair=self._keypair
301
330
  )
302
331
 
303
- print(f"Submitting batch transaction for {len(formatted_files)} files...")
332
+ print(
333
+ f"Submitting transaction to store {len(file_list)} files via file list CID..."
334
+ )
304
335
 
305
336
  # Submit the transaction
306
337
  response = self._substrate.submit_extrinsic(
@@ -310,11 +341,10 @@ class SubstrateClient:
310
341
  # Get the transaction hash
311
342
  tx_hash = response.extrinsic_hash
312
343
 
313
- print(f"Batch transaction submitted successfully!")
344
+ print(f"Transaction submitted successfully!")
314
345
  print(f"Transaction hash: {tx_hash}")
315
- print(
316
- f"All {len(formatted_files)} files have been stored in a single transaction"
317
- )
346
+ print(f"File list CID: {files_list_cid}")
347
+ print(f"All {len(file_list)} files will be stored through this request")
318
348
 
319
349
  return tx_hash
320
350
 
@@ -569,6 +599,29 @@ class SubstrateClient:
569
599
  ConnectionError: If connection to Substrate fails
570
600
  ValueError: If query fails
571
601
  """
602
+ # For backward compatibility, this method now calls get_user_files_from_profile
603
+ # with appropriate conversions
604
+ return self.get_user_files_from_profile(account_address)
605
+
606
+ def get_user_files_from_profile(
607
+ self,
608
+ account_address: Optional[str] = None,
609
+ ) -> List[Dict[str, Any]]:
610
+ """
611
+ Get user files by fetching the user profile CID from ipfsPallet and then retrieving
612
+ the profile JSON from IPFS.
613
+
614
+ Args:
615
+ account_address: Substrate account address (uses keypair address if not specified)
616
+ Format: 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
617
+
618
+ Returns:
619
+ List[Dict[str, Any]]: List of file objects from the user profile
620
+
621
+ Raises:
622
+ ConnectionError: If connection to Substrate fails
623
+ ValueError: If query fails or profile cannot be retrieved
624
+ """
572
625
  try:
573
626
  # Initialize Substrate connection if not already connected
574
627
  if not hasattr(self, "_substrate") or self._substrate is None:
@@ -592,159 +645,204 @@ class SubstrateClient:
592
645
  account_address = self._keypair.ss58_address
593
646
  print(f"Using keypair address: {account_address}")
594
647
 
595
- # Prepare the JSON-RPC request
596
- request = {
597
- "jsonrpc": "2.0",
598
- "method": "get_user_files",
599
- "params": [account_address],
600
- "id": 1,
601
- }
648
+ # Query the blockchain for the user profile CID
649
+ print(f"Querying user profile for account: {account_address}")
650
+ result = self._substrate.query(
651
+ module="IpfsPallet",
652
+ storage_function="UserProfile",
653
+ params=[account_address],
654
+ )
602
655
 
603
- print(f"Querying detailed file information for account: {account_address}")
656
+ # Check if a profile was found
657
+ if not result.value:
658
+ print(f"No profile found for account: {account_address}")
659
+ return []
604
660
 
605
- # Make the JSON-RPC call
606
- response = self._substrate.rpc_request(
607
- method="get_user_files", params=[account_address]
608
- )
661
+ # The result is a hex-encoded IPFS CID
662
+ # Handle both cases: bytes (needs .hex()) and string (already hex)
663
+ if isinstance(result.value, bytes):
664
+ hex_cid = result.value.hex()
665
+ else:
666
+ # If it's already a string, use it directly
667
+ hex_cid = result.value
609
668
 
610
- # Check for errors in the response
611
- if "error" in response:
612
- error_msg = (
613
- f"RPC error: {response['error'].get('message', 'Unknown error')}"
614
- )
615
- print(error_msg)
616
- raise ValueError(error_msg)
617
-
618
- # Extract the result
619
- files = response.get("result", [])
620
- print(f"Found {len(files)} files stored by this account")
621
-
622
- # Helper function to convert ASCII code arrays to strings
623
- def ascii_to_string(value):
624
- if isinstance(value, list) and all(isinstance(x, int) for x in value):
625
- return "".join(chr(code) for code in value)
626
- return str(value)
627
-
628
- # Helper function to properly format CIDs
629
- def format_cid(cid_str):
630
- # If it already looks like a proper CID, return it as is
631
- if cid_str.startswith(("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")):
632
- return cid_str
633
-
634
- # Check if it's a hex string
635
- if all(c in "0123456789abcdefABCDEF" for c in cid_str):
636
- # First try the special case where the hex string is actually ASCII encoded
669
+ # Remove '0x' prefix if present
670
+ if hex_cid.startswith("0x"):
671
+ hex_cid = hex_cid[2:]
672
+
673
+ print(f"Found user profile CID (hex): {hex_cid}")
674
+
675
+ # Convert the hex CID to a readable IPFS CID
676
+ profile_cid = self._hex_to_ipfs_cid(hex_cid)
677
+ print(f"Decoded IPFS CID: {profile_cid}")
678
+
679
+ # Fetch the profile JSON from IPFS
680
+ from hippius_sdk.ipfs import IPFSClient
681
+
682
+ ipfs_client = IPFSClient()
683
+
684
+ print(f"Fetching user profile from IPFS: {profile_cid}")
685
+ profile_data = ipfs_client.cat(profile_cid)
686
+
687
+ # Parse the JSON content
688
+ if not profile_data.get("is_text", False):
689
+ raise ValueError("User profile is not in text format")
690
+
691
+ profile_json = json.loads(profile_data.get("content", "{}"))
692
+ print(f"Successfully retrieved user profile")
693
+
694
+ # Extract the file list from the profile
695
+ # The profile might be either a dictionary with a 'files' key or a direct list of files
696
+ files = []
697
+ if isinstance(profile_json, dict):
698
+ files = profile_json.get("files", [])
699
+ elif isinstance(profile_json, list):
700
+ # The profile itself might be a list of files
701
+ files = profile_json
702
+ else:
703
+ print(f"Warning: Unexpected profile structure: {type(profile_json)}")
704
+
705
+ print(f"Found {len(files)} files in user profile")
706
+
707
+ # Process the files to match the expected format
708
+ processed_files = []
709
+ for file in files:
710
+ # Make sure file is a dictionary
711
+ if not isinstance(file, dict):
712
+ # Skip non-dictionary entries silently
713
+ continue
714
+
715
+ # Convert numeric arrays to strings if needed
716
+ # Handle file_hash: could be an array of ASCII/UTF-8 code points
717
+ file_hash = None
718
+ raw_file_hash = file.get("file_hash")
719
+ if isinstance(raw_file_hash, list) and all(
720
+ isinstance(n, int) for n in raw_file_hash
721
+ ):
637
722
  try:
638
- # Try to decode the hex as ASCII characters
639
- # (This is the case with some substrate responses where the CID is hex-encoded ASCII)
640
- hex_bytes = bytes.fromhex(cid_str)
641
- ascii_str = hex_bytes.decode("ascii")
642
-
643
- # If the decoded string starts with a valid CID prefix, return it
644
- if ascii_str.startswith(
645
- ("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")
646
- ):
647
- return ascii_str
723
+ # Convert array of numbers to bytes, then to a string
724
+ file_hash = bytes(raw_file_hash).decode("utf-8")
648
725
  except Exception:
649
726
  pass
727
+ else:
728
+ # Try different field names for the CID that might be in the profile
729
+ file_hash = (
730
+ file.get("cid")
731
+ or file.get("hash")
732
+ or file.get("fileHash")
733
+ or raw_file_hash
734
+ )
650
735
 
651
- # If the above doesn't work, try the standard CID decoding
736
+ # Handle file_name: could be an array of ASCII/UTF-8 code points
737
+ file_name = None
738
+ raw_file_name = file.get("file_name")
739
+ if isinstance(raw_file_name, list) and all(
740
+ isinstance(n, int) for n in raw_file_name
741
+ ):
652
742
  try:
653
- import base58
654
- import binascii
655
-
656
- # Try to decode hex to binary then to base58 for CIDv0
657
- try:
658
- binary_data = binascii.unhexlify(cid_str)
659
- if (
660
- len(binary_data) > 2
661
- and binary_data[0] == 0x12
662
- and binary_data[1] == 0x20
663
- ):
664
- # This looks like a CIDv0 (Qm...)
665
- decoded_cid = base58.b58encode(binary_data).decode(
666
- "utf-8"
667
- )
668
- return decoded_cid
669
- except Exception:
670
- pass
671
-
672
- # If not successful, just return hex with 0x prefix as fallback
673
- return f"0x{cid_str}"
674
- except ImportError:
675
- # If base58 is not available, return hex with prefix
676
- return f"0x{cid_str}"
677
-
678
- # Default case - return as is
679
- return cid_str
680
-
681
- # Helper function to format file sizes
682
- def format_file_size(size_bytes):
683
- if size_bytes >= 1024 * 1024:
684
- return f"{size_bytes / (1024 * 1024):.2f} MB"
743
+ # Convert array of numbers to bytes, then to a string
744
+ file_name = bytes(raw_file_name).decode("utf-8")
745
+ except Exception:
746
+ pass
685
747
  else:
686
- return f"{size_bytes / 1024:.2f} KB"
687
-
688
- # Helper function to format miner IDs for display
689
- def format_miner_id(miner_id):
690
- if (
691
- truncate_miners
692
- and isinstance(miner_id, str)
693
- and miner_id.startswith("1")
694
- and len(miner_id) > 40
695
- ):
696
- # Truncate long peer IDs
697
- return f"{miner_id[:12]}...{miner_id[-4:]}"
698
- return miner_id
699
-
700
- # Process the response
701
- processed_files = []
702
- for file in files:
703
- processed_file = {"file_size": file.get("file_size", 0)}
748
+ # Try different field names for the filename
749
+ file_name = (
750
+ file.get("filename")
751
+ or file.get("name")
752
+ or file.get("fileName")
753
+ or raw_file_name
754
+ )
704
755
 
705
- # Add formatted file size
706
- processed_file["size_formatted"] = format_file_size(
707
- processed_file["file_size"]
756
+ # Try different field names for the size
757
+ file_size = (
758
+ file.get("size")
759
+ or file.get("fileSize")
760
+ or file.get("file_size")
761
+ or 0
708
762
  )
709
763
 
710
- # Convert file_hash from byte array to string
711
- if "file_hash" in file:
712
- cid_str = ascii_to_string(file["file_hash"])
713
- processed_file["file_hash"] = format_cid(cid_str)
714
-
715
- # Convert file_name from byte array to string
716
- if "file_name" in file:
717
- processed_file["file_name"] = ascii_to_string(file["file_name"])
718
-
719
- # Convert miner_ids from byte arrays to strings
720
- if "miner_ids" in file and isinstance(file["miner_ids"], list):
721
- all_miners = [
722
- ascii_to_string(miner_id) for miner_id in file["miner_ids"]
723
- ]
724
- processed_file["miner_ids_full"] = all_miners
725
- processed_file["miner_count"] = len(all_miners)
726
-
727
- # Truncate miner list if requested
728
- if max_miners > 0 and len(all_miners) > max_miners:
729
- displayed_miners = all_miners[:max_miners]
730
- else:
731
- displayed_miners = all_miners
764
+ processed_file = {
765
+ "file_hash": file_hash,
766
+ "file_name": file_name,
767
+ # Add any other fields available in the profile
768
+ "miner_ids": file.get(
769
+ "miner_ids", []
770
+ ), # Try to get miners if available
771
+ "miner_count": len(file.get("miner_ids", [])), # Count the miners
772
+ "file_size": file_size,
773
+ }
732
774
 
733
- # Format and store the displayed miners
734
- processed_file["miner_ids"] = [
735
- {"id": miner_id, "formatted": format_miner_id(miner_id)}
736
- for miner_id in displayed_miners
737
- ]
775
+ # Add formatted file size if available
776
+ if file_size:
777
+ size_bytes = file_size
778
+ if size_bytes >= 1024 * 1024:
779
+ processed_file[
780
+ "size_formatted"
781
+ ] = f"{size_bytes / (1024 * 1024):.2f} MB"
782
+ else:
783
+ processed_file["size_formatted"] = f"{size_bytes / 1024:.2f} KB"
738
784
  else:
739
- processed_file["miner_ids"] = []
740
- processed_file["miner_ids_full"] = []
741
- processed_file["miner_count"] = 0
785
+ processed_file["size_formatted"] = "Unknown"
742
786
 
743
787
  processed_files.append(processed_file)
744
788
 
745
789
  return processed_files
746
790
 
747
791
  except Exception as e:
748
- error_msg = f"Error querying user files: {str(e)}"
792
+ error_msg = f"Error retrieving user files from profile: {str(e)}"
749
793
  print(error_msg)
750
794
  raise ValueError(error_msg)
795
+
796
+ def _hex_to_ipfs_cid(self, hex_string: str) -> str:
797
+ """
798
+ Convert a hex-encoded IPFS CID to a regular IPFS CID.
799
+
800
+ Args:
801
+ hex_string: Hex string representation of an IPFS CID
802
+
803
+ Returns:
804
+ str: Regular IPFS CID
805
+ """
806
+ # First, try to decode as ASCII if it's a hex representation of ASCII characters
807
+ try:
808
+ if hex_string.startswith("0x"):
809
+ hex_string = hex_string[2:]
810
+
811
+ bytes_data = bytes.fromhex(hex_string)
812
+ ascii_str = bytes_data.decode("ascii")
813
+
814
+ # If the decoded string starts with a valid CID prefix, return it
815
+ if ascii_str.startswith(("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")):
816
+ return ascii_str
817
+ except Exception:
818
+ # If ASCII decoding fails, continue with other methods
819
+ pass
820
+
821
+ # Try to decode as a binary CID
822
+ try:
823
+ import base58
824
+
825
+ if hex_string.startswith("0x"):
826
+ hex_string = hex_string[2:]
827
+
828
+ binary_data = bytes.fromhex(hex_string)
829
+
830
+ # Check if it matches CIDv0 pattern (starts with 0x12, 0x20)
831
+ if (
832
+ len(binary_data) > 2
833
+ and binary_data[0] == 0x12
834
+ and binary_data[1] == 0x20
835
+ ):
836
+ # CIDv0 (Qm...)
837
+ return base58.b58encode(binary_data).decode("utf-8")
838
+
839
+ # If it doesn't match CIDv0, for CIDv1 just return the hex without 0x prefix
840
+ # since adding 0x breaks IPFS gateway URLs
841
+ return hex_string
842
+ except ImportError:
843
+ # If base58 is not available
844
+ print("Warning: base58 module not available for proper CID conversion")
845
+ return hex_string
846
+ except Exception as e:
847
+ print(f"Error converting hex to CID: {e}")
848
+ return hex_string