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-0.1.7.dist-info → hippius-0.1.10.dist-info}/METADATA +189 -9
- hippius-0.1.10.dist-info/RECORD +12 -0
- hippius_sdk/__init__.py +4 -0
- hippius_sdk/account.py +648 -0
- hippius_sdk/cli.py +645 -445
- hippius_sdk/client.py +1 -1
- hippius_sdk/config.py +72 -1
- hippius_sdk/ipfs.py +1 -1
- hippius_sdk/substrate.py +252 -154
- hippius_sdk/utils.py +87 -0
- hippius-0.1.7.dist-info/RECORD +0 -10
- {hippius-0.1.7.dist-info → hippius-0.1.10.dist-info}/WHEEL +0 -0
- {hippius-0.1.7.dist-info → hippius-0.1.10.dist-info}/entry_points.txt +0 -0
hippius_sdk/client.py
CHANGED
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://
|
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
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
|
209
|
-
|
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"
|
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
|
-
#
|
265
|
-
|
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
|
-
|
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
|
-
#
|
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":
|
277
|
-
|
278
|
-
|
279
|
-
|
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(
|
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"
|
344
|
+
print(f"Transaction submitted successfully!")
|
314
345
|
print(f"Transaction hash: {tx_hash}")
|
315
|
-
print(
|
316
|
-
|
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
|
-
#
|
596
|
-
|
597
|
-
|
598
|
-
"
|
599
|
-
"
|
600
|
-
|
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
|
-
|
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
|
-
#
|
606
|
-
|
607
|
-
|
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
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
#
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
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
|
-
#
|
639
|
-
|
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
|
-
|
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
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
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
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
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
|
-
#
|
706
|
-
|
707
|
-
|
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
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
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
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
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["
|
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
|
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
|