hippius 0.1.14__py3-none-any.whl → 0.2.0__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.
@@ -0,0 +1,216 @@
1
+ import json
2
+ import os
3
+ from typing import Any, Dict
4
+
5
+ import httpx
6
+
7
+
8
+ class AsyncIPFSClient:
9
+ """
10
+ Asynchronous IPFS client using httpx.
11
+ """
12
+
13
+ def __init__(
14
+ self, api_url: str = "http://localhost:5001", gateway: str = "https://ipfs.io"
15
+ ):
16
+ # Handle multiaddr format
17
+ if api_url and api_url.startswith("/"):
18
+ # Extract host and port from multiaddr
19
+ try:
20
+ parts = api_url.split("/")
21
+ # Handle /ip4/127.0.0.1/tcp/5001
22
+ if len(parts) >= 5 and parts[1] in ["ip4", "ip6"]:
23
+ host = parts[2]
24
+ port = parts[4]
25
+ api_url = f"https://{host}:{port}"
26
+ print(f"Converted multiaddr {api_url} to HTTP URL {api_url}")
27
+ else:
28
+ print(f"Warning: Unsupported multiaddr format: {api_url}")
29
+ print("Falling back to default: http://localhost:5001")
30
+ api_url = "http://localhost:5001"
31
+ except Exception as e:
32
+ print(f"Error parsing multiaddr: {e}")
33
+ print("Falling back to default: http://localhost:5001")
34
+ api_url = "http://localhost:5001"
35
+ self.api_url = api_url
36
+ self.gateway = gateway
37
+ self.client = httpx.AsyncClient(timeout=60.0)
38
+
39
+ async def close(self):
40
+ """Close the httpx client."""
41
+ await self.client.aclose()
42
+
43
+ async def __aenter__(self):
44
+ return self
45
+
46
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
47
+ await self.close()
48
+
49
+ async def add_file(self, file_path: str) -> Dict[str, Any]:
50
+ """
51
+ Add a file to IPFS.
52
+
53
+ Args:
54
+ file_path: Path to the file to add
55
+
56
+ Returns:
57
+ Dict containing the CID and other information
58
+ """
59
+ with open(file_path, "rb") as f:
60
+ files = {"file": f}
61
+ response = await self.client.post(f"{self.api_url}/api/v0/add", files=files)
62
+ response.raise_for_status()
63
+ return response.json()
64
+
65
+ async def add_bytes(self, data: bytes, filename: str = "file") -> Dict[str, Any]:
66
+ """
67
+ Add bytes to IPFS.
68
+
69
+ Args:
70
+ data: Bytes to add
71
+ filename: Name to give the file (default: "file")
72
+
73
+ Returns:
74
+ Dict containing the CID and other information
75
+ """
76
+ files = {"file": (filename, data)}
77
+ response = await self.client.post(f"{self.api_url}/api/v0/add", files=files)
78
+ response.raise_for_status()
79
+ return response.json()
80
+
81
+ async def add_str(self, content: str, filename: str = "file") -> Dict[str, Any]:
82
+ """
83
+ Add a string to IPFS.
84
+
85
+ Args:
86
+ content: String to add
87
+ filename: Name to give the file (default: "file")
88
+
89
+ Returns:
90
+ Dict containing the CID and other information
91
+ """
92
+ return await self.add_bytes(content.encode(), filename)
93
+
94
+ async def cat(self, cid: str) -> bytes:
95
+ """
96
+ Retrieve content from IPFS by its CID.
97
+
98
+ Args:
99
+ cid: Content Identifier to retrieve
100
+
101
+ Returns:
102
+ Content as bytes
103
+ """
104
+ response = await self.client.post(f"{self.api_url}/api/v0/cat?arg={cid}")
105
+ response.raise_for_status()
106
+ return response.content
107
+
108
+ async def pin(self, cid: str) -> Dict[str, Any]:
109
+ """
110
+ Pin content by CID.
111
+
112
+ Args:
113
+ cid: Content Identifier to pin
114
+
115
+ Returns:
116
+ Response from the IPFS node
117
+ """
118
+ response = await self.client.post(f"{self.api_url}/api/v0/pin/add?arg={cid}")
119
+ response.raise_for_status()
120
+ return response.json()
121
+
122
+ async def ls(self, cid: str) -> Dict[str, Any]:
123
+ """
124
+ List objects linked to the specified CID.
125
+
126
+ Args:
127
+ cid: Content Identifier
128
+
129
+ Returns:
130
+ Dict with links information
131
+ """
132
+ response = await self.client.post(f"{self.api_url}/api/v0/ls?arg={cid}")
133
+ response.raise_for_status()
134
+ return response.json()
135
+
136
+ async def exists(self, cid: str) -> bool:
137
+ """
138
+ Check if content exists.
139
+
140
+ Args:
141
+ cid: Content Identifier to check
142
+
143
+ Returns:
144
+ True if content exists, False otherwise
145
+ """
146
+ try:
147
+ await self.client.head(f"{self.gateway}/ipfs/{cid}")
148
+ return True
149
+ except httpx.HTTPError:
150
+ return False
151
+
152
+ async def download_file(self, cid: str, output_path: str) -> str:
153
+ """
154
+ Download content from IPFS to a file.
155
+
156
+ Args:
157
+ cid: Content identifier
158
+ output_path: Path where to save the file
159
+
160
+ Returns:
161
+ Path to the saved file
162
+ """
163
+ content = await self.cat(cid)
164
+ with open(output_path, "wb") as f:
165
+ f.write(content)
166
+ return output_path
167
+
168
+ async def add_directory(
169
+ self, dir_path: str, recursive: bool = True
170
+ ) -> Dict[str, Any]:
171
+ """
172
+ Add a directory to IPFS.
173
+
174
+ Args:
175
+ dir_path: Path to the directory to add
176
+
177
+ Returns:
178
+ Dict containing the CID and other information about the directory
179
+
180
+ Raises:
181
+ FileNotFoundError: If the directory doesn't exist
182
+ httpx.HTTPError: If the IPFS API request fails
183
+ """
184
+ if not os.path.isdir(dir_path):
185
+ raise FileNotFoundError(f"Directory {dir_path} not found")
186
+
187
+ # Collect all files in the directory
188
+ files = []
189
+ for root, _, filenames in os.walk(dir_path):
190
+ for filename in filenames:
191
+ file_path = os.path.join(root, filename)
192
+ rel_path = os.path.relpath(file_path, dir_path)
193
+
194
+ with open(file_path, "rb") as f:
195
+ file_content = f.read()
196
+
197
+ # Add the file to the multipart request
198
+ files.append(
199
+ ("file", (rel_path, file_content, "application/octet-stream"))
200
+ )
201
+
202
+ # Make the request with directory flags
203
+ response = await self.client.post(
204
+ f"{self.api_url}/api/v0/add?recursive=true&wrap-with-directory=true",
205
+ files=files,
206
+ timeout=300.0, # 5 minute timeout for directory uploads
207
+ )
208
+ response.raise_for_status()
209
+
210
+ # The IPFS API returns a JSON object for each file, one per line
211
+ # The last one should be the directory itself
212
+ lines = response.text.strip().split("\n")
213
+ if not lines:
214
+ raise ValueError("Empty response from IPFS API")
215
+
216
+ return json.loads(lines[-1])
hippius_sdk/substrate.py CHANGED
@@ -6,8 +6,9 @@ Note: This functionality is coming soon and not implemented yet.
6
6
 
7
7
  import json
8
8
  import os
9
+ import tempfile
9
10
  import uuid
10
- from typing import Any, Dict, List, Optional, Union
11
+ from typing import Any, Dict, List, Optional, Type, Union
11
12
 
12
13
  from dotenv import load_dotenv
13
14
  from substrateinterface import Keypair, SubstrateInterface
@@ -19,6 +20,7 @@ from hippius_sdk.config import (
19
20
  get_seed_phrase,
20
21
  set_seed_phrase,
21
22
  )
23
+ from hippius_sdk.utils import hex_to_ipfs_cid
22
24
 
23
25
  # Load environment variables
24
26
  load_dotenv()
@@ -202,7 +204,7 @@ class SubstrateClient:
202
204
  print(f"Warning: Could not create keypair from seed phrase: {e}")
203
205
  print(f"Keypair will be created when needed")
204
206
 
205
- def storage_request(
207
+ async def storage_request(
206
208
  self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
207
209
  ) -> str:
208
210
  """
@@ -276,8 +278,7 @@ class SubstrateClient:
276
278
  print(f"Created file list with {len(file_list)} entries")
277
279
 
278
280
  # Step 2: Upload the JSON file to IPFS
279
- import tempfile
280
-
281
+ # Defer import to avoid circular imports
281
282
  from hippius_sdk.ipfs import IPFSClient
282
283
 
283
284
  ipfs_client = IPFSClient()
@@ -291,7 +292,7 @@ class SubstrateClient:
291
292
 
292
293
  try:
293
294
  print("Uploading file list to IPFS...")
294
- upload_result = ipfs_client.upload_file(temp_file_path)
295
+ upload_result = await ipfs_client.upload_file(temp_file_path)
295
296
  files_list_cid = upload_result["cid"]
296
297
  print(f"File list uploaded to IPFS with CID: {files_list_cid}")
297
298
  finally:
@@ -313,19 +314,42 @@ class SubstrateClient:
313
314
 
314
315
  # Create the call to the marketplace
315
316
  print(f"Call parameters: {json.dumps(call_params, indent=2)}")
316
- call = self._substrate.compose_call(
317
- call_module="Marketplace",
318
- call_function="storage_request",
319
- call_params=call_params,
320
- )
317
+ try:
318
+ call = self._substrate.compose_call(
319
+ call_module="Marketplace",
320
+ call_function="storage_request",
321
+ call_params=call_params,
322
+ )
323
+ except Exception as e:
324
+ print(f"Warning: Error composing call: {e}")
325
+ print("Attempting to use IpfsPallet.storeFile instead...")
326
+
327
+ # Try with IpfsPallet.storeFile as an alternative
328
+ alt_call_params = {
329
+ "fileHash": files_list_cid,
330
+ "fileName": f"files_list_{uuid.uuid4()}", # Generate a unique ID
331
+ }
332
+ call = self._substrate.compose_call(
333
+ call_module="IpfsPallet",
334
+ call_function="storeFile",
335
+ call_params=alt_call_params,
336
+ )
321
337
 
322
338
  # Get payment info to estimate the fee
323
339
  payment_info = self._substrate.get_payment_info(
324
340
  call=call, keypair=self._keypair
325
341
  )
326
342
 
343
+ print(f"Payment info: {json.dumps(payment_info, indent=2)}")
344
+
345
+ # Convert partialFee from Substrate (10^18 units) to a more readable format
327
346
  estimated_fee = payment_info.get("partialFee", 0)
328
- print(f"Estimated transaction fee: {estimated_fee}")
347
+ estimated_fee_formatted = (
348
+ float(estimated_fee) / 1_000_000_000_000_000_000 if estimated_fee else 0
349
+ )
350
+ print(
351
+ f"Estimated transaction fee: {estimated_fee} ({estimated_fee_formatted:.10f} tokens)"
352
+ )
329
353
 
330
354
  # Create a signed extrinsic
331
355
  extrinsic = self._substrate.create_signed_extrinsic(
@@ -361,7 +385,7 @@ class SubstrateClient:
361
385
 
362
386
  return "simulated-tx-hash"
363
387
 
364
- def store_cid(
388
+ async def store_cid(
365
389
  self, cid: str, filename: str = None, metadata: Optional[Dict[str, Any]] = None
366
390
  ) -> str:
367
391
  """
@@ -376,7 +400,7 @@ class SubstrateClient:
376
400
  str: Transaction hash
377
401
  """
378
402
  file_input = FileInput(file_hash=cid, file_name=filename or "unnamed_file")
379
- return self.storage_request([file_input])
403
+ return await self.storage_request([file_input])
380
404
 
381
405
  def get_cid_metadata(self, cid: str) -> Dict[str, Any]:
382
406
  """
@@ -569,7 +593,7 @@ class SubstrateClient:
569
593
  print(error_msg)
570
594
  raise ValueError(error_msg)
571
595
 
572
- def get_user_files(
596
+ async def get_user_files(
573
597
  self,
574
598
  account_address: Optional[str] = None,
575
599
  truncate_miners: bool = True,
@@ -604,9 +628,9 @@ class SubstrateClient:
604
628
  """
605
629
  # For backward compatibility, this method now calls get_user_files_from_profile
606
630
  # with appropriate conversions
607
- return self.get_user_files_from_profile(account_address)
631
+ return await self.get_user_files_from_profile(account_address)
608
632
 
609
- def get_user_files_from_profile(
633
+ async def get_user_files_from_profile(
610
634
  self,
611
635
  account_address: Optional[str] = None,
612
636
  ) -> List[Dict[str, Any]]:
@@ -680,12 +704,13 @@ class SubstrateClient:
680
704
  print(f"Decoded IPFS CID: {profile_cid}")
681
705
 
682
706
  # Fetch the profile JSON from IPFS
707
+ # Defer import to avoid circular imports
683
708
  from hippius_sdk.ipfs import IPFSClient
684
709
 
685
710
  ipfs_client = IPFSClient()
686
711
 
687
712
  print(f"Fetching user profile from IPFS: {profile_cid}")
688
- profile_data = ipfs_client.cat(profile_cid)
713
+ profile_data = await ipfs_client.cat(profile_cid)
689
714
 
690
715
  # Parse the JSON content
691
716
  if not profile_data.get("is_text", False):
@@ -797,56 +822,205 @@ class SubstrateClient:
797
822
  print(error_msg)
798
823
  raise ValueError(error_msg)
799
824
 
800
- def _hex_to_ipfs_cid(self, hex_string: str) -> str:
825
+ def get_pinning_status(
826
+ self, account_address: Optional[str] = None
827
+ ) -> List[Dict[str, Any]]:
801
828
  """
802
- Convert a hex-encoded IPFS CID to a regular IPFS CID.
829
+ Get the status of file pinning requests for an account.
830
+
831
+ This method queries the blockchain for all storage requests made by the user
832
+ to check their pinning status.
803
833
 
804
834
  Args:
805
- hex_string: Hex string representation of an IPFS CID
835
+ account_address: Substrate account address (uses keypair address if not specified)
836
+ Format: 5HoreGVb17XhY3wanDvzoAWS7yHYbc5uMteXqRNTiZ6Txkqq
806
837
 
807
838
  Returns:
808
- str: Regular IPFS CID
839
+ List[Dict[str, Any]]: List of storage requests with their status information:
840
+ {
841
+ "cid": str, # The IPFS CID of the file
842
+ "file_name": str, # The name of the file
843
+ "total_replicas": int, # Total number of replicas requested
844
+ "owner": str, # Owner's address
845
+ "created_at": int, # Block number when request was created
846
+ "last_charged_at": int, # Block number when last charged
847
+ "miner_ids": List[str], # List of miners assigned to pin the file
848
+ "selected_validator": str, # Selected validator address
849
+ "is_assigned": bool, # Whether request has been assigned to miners
850
+ }
851
+
852
+ Raises:
853
+ ConnectionError: If connection to Substrate fails
854
+ ValueError: If query fails or no requests found
809
855
  """
810
- # First, try to decode as ASCII if it's a hex representation of ASCII characters
811
856
  try:
812
- if hex_string.startswith("0x"):
813
- hex_string = hex_string[2:]
857
+ # Initialize Substrate connection if not already connected
858
+ if not hasattr(self, "_substrate") or self._substrate is None:
859
+ print("Initializing Substrate connection...")
860
+ self._substrate = SubstrateInterface(
861
+ url=self.url,
862
+ ss58_format=42, # Substrate default
863
+ type_registry_preset="substrate-node-template",
864
+ )
865
+ print(f"Connected to Substrate node at {self.url}")
814
866
 
815
- bytes_data = bytes.fromhex(hex_string)
816
- ascii_str = bytes_data.decode("ascii")
867
+ # Use provided account address or default to keypair/configured address
868
+ if not account_address:
869
+ if self._account_address:
870
+ account_address = self._account_address
871
+ print(f"Using account address: {account_address}")
872
+ else:
873
+ # Try to get the address from the keypair (requires seed phrase)
874
+ if not self._ensure_keypair():
875
+ raise ValueError("No account address available")
876
+ account_address = self._keypair.ss58_address
877
+ print(f"Using keypair address: {account_address}")
817
878
 
818
- # If the decoded string starts with a valid CID prefix, return it
819
- if ascii_str.startswith(("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")):
820
- return ascii_str
821
- except Exception:
822
- # If ASCII decoding fails, continue with other methods
823
- pass
879
+ # Query the blockchain for storage requests
880
+ print(f"Querying storage requests for account: {account_address}")
881
+ try:
882
+ # First, try with query_map which is more suitable for iterating over collections
883
+ result = self._substrate.query_map(
884
+ module="IpfsPallet",
885
+ storage_function="UserStorageRequests",
886
+ params=[account_address],
887
+ )
888
+ results_list = list(result)
889
+ except Exception as e:
890
+ print(f"Error with query_map: {e}")
891
+ try:
892
+ # Try again with query to double check storage function requirements
893
+ result = self._substrate.query(
894
+ module="IpfsPallet",
895
+ storage_function="UserStorageRequests",
896
+ params=[
897
+ account_address,
898
+ None,
899
+ ], # Try with a None second parameter
900
+ )
901
+
902
+ # If the query returns a nested structure, extract it
903
+ if result.value and isinstance(result.value, list):
904
+ # Convert to a list format similar to query_map for processing
905
+ results_list = []
906
+ for item in result.value:
907
+ if isinstance(item, list) and len(item) >= 2:
908
+ key = item[0]
909
+ value = item[1]
910
+ results_list.append((key, value))
911
+ else:
912
+ # If it's not a nested structure, use a simpler format
913
+ results_list = [(None, result.value)] if result.value else []
914
+ except Exception as e_inner:
915
+ print(f"Error with fallback query: {e_inner}")
916
+ # If both methods fail, return an empty list
917
+ results_list = []
918
+
919
+ # Process the storage requests
920
+ storage_requests = []
921
+
922
+ if not results_list:
923
+ print(f"No storage requests found for account: {account_address}")
924
+ return []
925
+
926
+ print(f"Found {len(results_list)} storage request entries")
927
+
928
+ for i, (key, value) in enumerate(results_list):
929
+ try:
930
+ # For debugging, print raw data
931
+ print(f"Entry {i+1}:")
932
+ print(f" Raw key: {key}, type: {type(key)}")
933
+ print(f" Raw value: {value}, type: {type(value)}")
934
+
935
+ # Extract file hash from key if possible
936
+ file_hash_hex = None
937
+ if key is not None:
938
+ if hasattr(key, "hex"):
939
+ file_hash_hex = key.hex()
940
+ elif isinstance(key, bytes):
941
+ file_hash_hex = key.hex()
942
+ elif isinstance(key, str) and key.startswith("0x"):
943
+ file_hash_hex = key[2:]
944
+ else:
945
+ file_hash_hex = str(key)
946
+
947
+ # Try to extract value data
948
+ request_data = None
949
+ if isinstance(value, dict):
950
+ request_data = value
951
+ elif hasattr(value, "get"):
952
+ request_data = value
953
+ elif hasattr(value, "__dict__"):
954
+ # Convert object to dict
955
+ request_data = {
956
+ k: getattr(value, k)
957
+ for k in dir(value)
958
+ if not k.startswith("_") and not callable(getattr(value, k))
959
+ }
960
+
961
+ # If we can't extract data, just use value as string for debugging
962
+ if request_data is None:
963
+ request_data = {"raw_value": str(value)}
964
+
965
+ # Create formatted request with available data
966
+ formatted_request = {"raw_key": str(key), "raw_value": str(value)}
967
+
968
+ # Directly extract file_name from the value if it's a dict-like object
969
+ if hasattr(value, "get"):
970
+ if value.get("file_name"):
971
+ formatted_request["file_name"] = value.get("file_name")
972
+ elif value.get("fileName"):
973
+ formatted_request["file_name"] = value.get("fileName")
974
+
975
+ # Add CID if we have it
976
+ if file_hash_hex:
977
+ file_cid = self._hex_to_ipfs_cid(file_hash_hex)
978
+ formatted_request["cid"] = file_cid
979
+
980
+ # Add other fields from request_data if available
981
+ for source_field, target_field in [
982
+ ("fileName", "file_name"),
983
+ ("totalReplicas", "total_replicas"),
984
+ ("owner", "owner"),
985
+ ("createdAt", "created_at"),
986
+ ("lastChargedAt", "last_charged_at"),
987
+ ("minerIds", "miner_ids"),
988
+ ("selectedValidator", "selected_validator"),
989
+ ("isAssigned", "is_assigned"),
990
+ # Add variants that might appear differently in the chain storage
991
+ ("file_name", "file_name"),
992
+ ("file_hash", "file_hash"),
993
+ ("total_replicas", "total_replicas"),
994
+ ]:
995
+ if source_field in request_data:
996
+ formatted_request[target_field] = request_data[source_field]
997
+ # Fallback to attribute access for different types of objects
998
+ elif hasattr(value, source_field):
999
+ formatted_request[target_field] = getattr(
1000
+ value, source_field
1001
+ )
1002
+
1003
+ storage_requests.append(formatted_request)
1004
+
1005
+ except Exception as e:
1006
+ print(f"Error processing request entry {i+1}: {e}")
1007
+
1008
+ print(f"Successfully processed {len(storage_requests)} storage requests")
1009
+ return storage_requests
824
1010
 
825
- # Try to decode as a binary CID
826
- try:
827
- import base58
828
-
829
- if hex_string.startswith("0x"):
830
- hex_string = hex_string[2:]
831
-
832
- binary_data = bytes.fromhex(hex_string)
833
-
834
- # Check if it matches CIDv0 pattern (starts with 0x12, 0x20)
835
- if (
836
- len(binary_data) > 2
837
- and binary_data[0] == 0x12
838
- and binary_data[1] == 0x20
839
- ):
840
- # CIDv0 (Qm...)
841
- return base58.b58encode(binary_data).decode("utf-8")
842
-
843
- # If it doesn't match CIDv0, for CIDv1 just return the hex without 0x prefix
844
- # since adding 0x breaks IPFS gateway URLs
845
- return hex_string
846
- except ImportError:
847
- # If base58 is not available
848
- print("Warning: base58 module not available for proper CID conversion")
849
- return hex_string
850
1011
  except Exception as e:
851
- print(f"Error converting hex to CID: {e}")
852
- return hex_string
1012
+ error_msg = f"Error querying storage requests: {str(e)}"
1013
+ print(error_msg)
1014
+ raise ValueError(error_msg)
1015
+
1016
+ def _hex_to_ipfs_cid(self, hex_string: str) -> str:
1017
+ """
1018
+ Convert a hex-encoded IPFS CID to a regular IPFS CID.
1019
+
1020
+ Args:
1021
+ hex_string: Hex string representation of an IPFS CID
1022
+
1023
+ Returns:
1024
+ str: Regular IPFS CID
1025
+ """
1026
+ return hex_to_ipfs_cid(hex_string)