hippius 0.1.9__py3-none-any.whl → 0.1.11__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/config.py CHANGED
@@ -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/substrate.py CHANGED
@@ -599,6 +599,29 @@ class SubstrateClient:
599
599
  ConnectionError: If connection to Substrate fails
600
600
  ValueError: If query fails
601
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
+ """
602
625
  try:
603
626
  # Initialize Substrate connection if not already connected
604
627
  if not hasattr(self, "_substrate") or self._substrate is None:
@@ -622,159 +645,204 @@ class SubstrateClient:
622
645
  account_address = self._keypair.ss58_address
623
646
  print(f"Using keypair address: {account_address}")
624
647
 
625
- # Prepare the JSON-RPC request
626
- request = {
627
- "jsonrpc": "2.0",
628
- "method": "get_user_files",
629
- "params": [account_address],
630
- "id": 1,
631
- }
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
+ )
632
655
 
633
- 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 []
634
660
 
635
- # Make the JSON-RPC call
636
- response = self._substrate.rpc_request(
637
- method="get_user_files", params=[account_address]
638
- )
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
639
668
 
640
- # Check for errors in the response
641
- if "error" in response:
642
- error_msg = (
643
- f"RPC error: {response['error'].get('message', 'Unknown error')}"
644
- )
645
- print(error_msg)
646
- raise ValueError(error_msg)
647
-
648
- # Extract the result
649
- files = response.get("result", [])
650
- print(f"Found {len(files)} files stored by this account")
651
-
652
- # Helper function to convert ASCII code arrays to strings
653
- def ascii_to_string(value):
654
- if isinstance(value, list) and all(isinstance(x, int) for x in value):
655
- return "".join(chr(code) for code in value)
656
- return str(value)
657
-
658
- # Helper function to properly format CIDs
659
- def format_cid(cid_str):
660
- # If it already looks like a proper CID, return it as is
661
- if cid_str.startswith(("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")):
662
- return cid_str
663
-
664
- # Check if it's a hex string
665
- if all(c in "0123456789abcdefABCDEF" for c in cid_str):
666
- # 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
+ ):
667
722
  try:
668
- # Try to decode the hex as ASCII characters
669
- # (This is the case with some substrate responses where the CID is hex-encoded ASCII)
670
- hex_bytes = bytes.fromhex(cid_str)
671
- ascii_str = hex_bytes.decode("ascii")
672
-
673
- # If the decoded string starts with a valid CID prefix, return it
674
- if ascii_str.startswith(
675
- ("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")
676
- ):
677
- return ascii_str
723
+ # Convert array of numbers to bytes, then to a string
724
+ file_hash = bytes(raw_file_hash).decode("utf-8")
678
725
  except Exception:
679
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
+ )
680
735
 
681
- # 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
+ ):
682
742
  try:
683
- import base58
684
- import binascii
685
-
686
- # Try to decode hex to binary then to base58 for CIDv0
687
- try:
688
- binary_data = binascii.unhexlify(cid_str)
689
- if (
690
- len(binary_data) > 2
691
- and binary_data[0] == 0x12
692
- and binary_data[1] == 0x20
693
- ):
694
- # This looks like a CIDv0 (Qm...)
695
- decoded_cid = base58.b58encode(binary_data).decode(
696
- "utf-8"
697
- )
698
- return decoded_cid
699
- except Exception:
700
- pass
701
-
702
- # If not successful, just return hex with 0x prefix as fallback
703
- return f"0x{cid_str}"
704
- except ImportError:
705
- # If base58 is not available, return hex with prefix
706
- return f"0x{cid_str}"
707
-
708
- # Default case - return as is
709
- return cid_str
710
-
711
- # Helper function to format file sizes
712
- def format_file_size(size_bytes):
713
- if size_bytes >= 1024 * 1024:
714
- 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
715
747
  else:
716
- return f"{size_bytes / 1024:.2f} KB"
717
-
718
- # Helper function to format miner IDs for display
719
- def format_miner_id(miner_id):
720
- if (
721
- truncate_miners
722
- and isinstance(miner_id, str)
723
- and miner_id.startswith("1")
724
- and len(miner_id) > 40
725
- ):
726
- # Truncate long peer IDs
727
- return f"{miner_id[:12]}...{miner_id[-4:]}"
728
- return miner_id
729
-
730
- # Process the response
731
- processed_files = []
732
- for file in files:
733
- 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
+ )
734
755
 
735
- # Add formatted file size
736
- processed_file["size_formatted"] = format_file_size(
737
- 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
738
762
  )
739
763
 
740
- # Convert file_hash from byte array to string
741
- if "file_hash" in file:
742
- cid_str = ascii_to_string(file["file_hash"])
743
- processed_file["file_hash"] = format_cid(cid_str)
744
-
745
- # Convert file_name from byte array to string
746
- if "file_name" in file:
747
- processed_file["file_name"] = ascii_to_string(file["file_name"])
748
-
749
- # Convert miner_ids from byte arrays to strings
750
- if "miner_ids" in file and isinstance(file["miner_ids"], list):
751
- all_miners = [
752
- ascii_to_string(miner_id) for miner_id in file["miner_ids"]
753
- ]
754
- processed_file["miner_ids_full"] = all_miners
755
- processed_file["miner_count"] = len(all_miners)
756
-
757
- # Truncate miner list if requested
758
- if max_miners > 0 and len(all_miners) > max_miners:
759
- displayed_miners = all_miners[:max_miners]
760
- else:
761
- 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
+ }
762
774
 
763
- # Format and store the displayed miners
764
- processed_file["miner_ids"] = [
765
- {"id": miner_id, "formatted": format_miner_id(miner_id)}
766
- for miner_id in displayed_miners
767
- ]
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"
768
784
  else:
769
- processed_file["miner_ids"] = []
770
- processed_file["miner_ids_full"] = []
771
- processed_file["miner_count"] = 0
785
+ processed_file["size_formatted"] = "Unknown"
772
786
 
773
787
  processed_files.append(processed_file)
774
788
 
775
789
  return processed_files
776
790
 
777
791
  except Exception as e:
778
- error_msg = f"Error querying user files: {str(e)}"
792
+ error_msg = f"Error retrieving user files from profile: {str(e)}"
779
793
  print(error_msg)
780
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
hippius_sdk/utils.py ADDED
@@ -0,0 +1,87 @@
1
+ """
2
+ Utility functions for the Hippius SDK.
3
+ """
4
+
5
+ import os
6
+ import math
7
+ from typing import Dict, Any, Union, List, Optional
8
+
9
+
10
+ def ensure_directory_exists(directory_path: str) -> None:
11
+ """
12
+ Create a directory if it doesn't exist.
13
+
14
+ Args:
15
+ directory_path: Path to the directory to ensure exists
16
+ """
17
+ if not os.path.exists(directory_path):
18
+ os.makedirs(directory_path, exist_ok=True)
19
+
20
+
21
+ def format_size(size_bytes: int) -> str:
22
+ """
23
+ Format a size in bytes to a human-readable string.
24
+
25
+ Args:
26
+ size_bytes: Size in bytes
27
+
28
+ Returns:
29
+ str: Human-readable size (e.g., "1.23 MB")
30
+ """
31
+ if size_bytes == 0:
32
+ return "0 B"
33
+
34
+ size_names = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
35
+ i = int(math.floor(math.log(size_bytes, 1024)))
36
+ if i >= len(size_names):
37
+ i = len(size_names) - 1
38
+ p = math.pow(1024, i)
39
+ s = round(size_bytes / p, 2)
40
+ return f"{s} {size_names[i]}"
41
+
42
+
43
+ def deep_merge(target: Dict[str, Any], source: Dict[str, Any]) -> Dict[str, Any]:
44
+ """
45
+ Deep merge two dictionaries, with source taking precedence over target.
46
+
47
+ Args:
48
+ target: Target dictionary to merge into
49
+ source: Source dictionary to merge from
50
+
51
+ Returns:
52
+ Dict[str, Any]: Merged dictionary
53
+ """
54
+ for key, value in source.items():
55
+ if key in target and isinstance(target[key], dict) and isinstance(value, dict):
56
+ target[key] = deep_merge(target[key], value)
57
+ else:
58
+ target[key] = value
59
+ return target
60
+
61
+
62
+ def parse_comma_separated(value: Optional[str]) -> List[str]:
63
+ """
64
+ Parse a comma-separated string into a list.
65
+
66
+ Args:
67
+ value: Comma-separated string or None
68
+
69
+ Returns:
70
+ List[str]: List of stripped values, or empty list if value is None
71
+ """
72
+ if not value:
73
+ return []
74
+ return [item.strip() for item in value.split(",") if item.strip()]
75
+
76
+
77
+ def is_valid_url(url: str) -> bool:
78
+ """
79
+ Basic check if a string is a valid URL.
80
+
81
+ Args:
82
+ url: URL to check
83
+
84
+ Returns:
85
+ bool: True if valid URL, False otherwise
86
+ """
87
+ return url.startswith(("http://", "https://", "ws://", "wss://"))
@@ -1,10 +0,0 @@
1
- hippius_sdk/__init__.py,sha256=fs2pZ_wIl3eaUz6qFprcRsZhYakqv6-qm468NZh-8h4,1260
2
- hippius_sdk/cli.py,sha256=0IQZh2by6kN8Sol3eDFfZzgj5otBeIOKC4lq2wIejOc,76317
3
- hippius_sdk/client.py,sha256=54tsg4k29sqt3F77LQJ_vhzzTR73QuZ_edqI_BvZM1E,14905
4
- hippius_sdk/config.py,sha256=gTr8EXeZ3jJNchcG9WyjbQdpPRHTxL5IWZgyIAm_X-c,22869
5
- hippius_sdk/ipfs.py,sha256=9fds5MJwVb7t8IqROM70x9fWgyk9_Ot5psat_hMnRN8,63969
6
- hippius_sdk/substrate.py,sha256=fa8K8vVdYGVtB3VNJ-_Vdw8bOxDUajFmj6se-rHZveQ,30853
7
- hippius-0.1.9.dist-info/METADATA,sha256=SPXMFKAJ_at35FEPB9uDMi3iBP7EXMLxskPvLxHmCQE,28000
8
- hippius-0.1.9.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
9
- hippius-0.1.9.dist-info/entry_points.txt,sha256=b1lo60zRXmv1ud-c5BC-cJcAfGE5FD4qM_nia6XeQtM,98
10
- hippius-0.1.9.dist-info/RECORD,,