hippius 0.1.13__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.
- {hippius-0.1.13.dist-info → hippius-0.2.0.dist-info}/METADATA +2 -2
- hippius-0.2.0.dist-info/RECORD +12 -0
- hippius_sdk/__init__.py +5 -1
- hippius_sdk/cli.py +399 -49
- hippius_sdk/client.py +20 -22
- hippius_sdk/config.py +19 -26
- hippius_sdk/ipfs.py +62 -408
- hippius_sdk/ipfs_core.py +216 -0
- hippius_sdk/substrate.py +235 -60
- hippius_sdk/utils.py +152 -0
- hippius-0.1.13.dist-info/RECORD +0 -10
- {hippius-0.1.13.dist-info → hippius-0.2.0.dist-info}/WHEEL +0 -0
- {hippius-0.1.13.dist-info → hippius-0.2.0.dist-info}/entry_points.txt +0 -0
hippius_sdk/ipfs_core.py
ADDED
@@ -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
|
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
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
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
|
-
|
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):
|
@@ -761,6 +786,7 @@ class SubstrateClient:
|
|
761
786
|
file.get("size")
|
762
787
|
or file.get("fileSize")
|
763
788
|
or file.get("file_size")
|
789
|
+
or file.get("file_size_in_bytes")
|
764
790
|
or 0
|
765
791
|
)
|
766
792
|
|
@@ -772,7 +798,7 @@ class SubstrateClient:
|
|
772
798
|
"miner_ids", []
|
773
799
|
), # Try to get miners if available
|
774
800
|
"miner_count": len(file.get("miner_ids", [])), # Count the miners
|
775
|
-
"file_size": file_size,
|
801
|
+
"file_size": file_size or 0,
|
776
802
|
}
|
777
803
|
|
778
804
|
# Add formatted file size if available
|
@@ -796,56 +822,205 @@ class SubstrateClient:
|
|
796
822
|
print(error_msg)
|
797
823
|
raise ValueError(error_msg)
|
798
824
|
|
799
|
-
def
|
825
|
+
def get_pinning_status(
|
826
|
+
self, account_address: Optional[str] = None
|
827
|
+
) -> List[Dict[str, Any]]:
|
800
828
|
"""
|
801
|
-
|
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.
|
802
833
|
|
803
834
|
Args:
|
804
|
-
|
835
|
+
account_address: Substrate account address (uses keypair address if not specified)
|
836
|
+
Format: 5HoreGVb17XhY3wanDvzoAWS7yHYbc5uMteXqRNTiZ6Txkqq
|
805
837
|
|
806
838
|
Returns:
|
807
|
-
str:
|
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
|
808
855
|
"""
|
809
|
-
# First, try to decode as ASCII if it's a hex representation of ASCII characters
|
810
856
|
try:
|
811
|
-
if
|
812
|
-
|
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}")
|
813
866
|
|
814
|
-
|
815
|
-
|
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}")
|
816
878
|
|
817
|
-
#
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
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
|
823
1010
|
|
824
|
-
# Try to decode as a binary CID
|
825
|
-
try:
|
826
|
-
import base58
|
827
|
-
|
828
|
-
if hex_string.startswith("0x"):
|
829
|
-
hex_string = hex_string[2:]
|
830
|
-
|
831
|
-
binary_data = bytes.fromhex(hex_string)
|
832
|
-
|
833
|
-
# Check if it matches CIDv0 pattern (starts with 0x12, 0x20)
|
834
|
-
if (
|
835
|
-
len(binary_data) > 2
|
836
|
-
and binary_data[0] == 0x12
|
837
|
-
and binary_data[1] == 0x20
|
838
|
-
):
|
839
|
-
# CIDv0 (Qm...)
|
840
|
-
return base58.b58encode(binary_data).decode("utf-8")
|
841
|
-
|
842
|
-
# If it doesn't match CIDv0, for CIDv1 just return the hex without 0x prefix
|
843
|
-
# since adding 0x breaks IPFS gateway URLs
|
844
|
-
return hex_string
|
845
|
-
except ImportError:
|
846
|
-
# If base58 is not available
|
847
|
-
print("Warning: base58 module not available for proper CID conversion")
|
848
|
-
return hex_string
|
849
1011
|
except Exception as e:
|
850
|
-
|
851
|
-
|
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)
|