hippius 0.2.1__py3-none-any.whl → 0.2.3__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.2.1.dist-info → hippius-0.2.3.dist-info}/METADATA +8 -7
- hippius-0.2.3.dist-info/RECORD +16 -0
- hippius_sdk/__init__.py +1 -1
- hippius_sdk/cli.py +277 -2628
- hippius_sdk/cli_assets.py +8 -0
- hippius_sdk/cli_handlers.py +2370 -0
- hippius_sdk/cli_parser.py +602 -0
- hippius_sdk/cli_rich.py +253 -0
- hippius_sdk/client.py +56 -8
- hippius_sdk/config.py +1 -1
- hippius_sdk/ipfs.py +540 -130
- hippius_sdk/ipfs_core.py +22 -1
- hippius_sdk/substrate.py +215 -525
- hippius_sdk/utils.py +84 -2
- hippius-0.2.1.dist-info/RECORD +0 -12
- {hippius-0.2.1.dist-info → hippius-0.2.3.dist-info}/WHEEL +0 -0
- {hippius-0.2.1.dist-info → hippius-0.2.3.dist-info}/entry_points.txt +0 -0
hippius_sdk/substrate.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import datetime
|
2
2
|
import json
|
3
3
|
import os
|
4
|
+
import pprint
|
4
5
|
import tempfile
|
5
6
|
import time
|
6
7
|
import uuid
|
@@ -19,7 +20,11 @@ from hippius_sdk.config import (
|
|
19
20
|
set_active_account,
|
20
21
|
set_seed_phrase,
|
21
22
|
)
|
22
|
-
from hippius_sdk.utils import
|
23
|
+
from hippius_sdk.utils import (
|
24
|
+
format_size,
|
25
|
+
hex_to_ipfs_cid,
|
26
|
+
initialize_substrate_connection,
|
27
|
+
)
|
23
28
|
|
24
29
|
# Load environment variables
|
25
30
|
load_dotenv()
|
@@ -128,16 +133,12 @@ class SubstrateClient:
|
|
128
133
|
print("Connected successfully (read-only mode, no account)")
|
129
134
|
self._read_only = True
|
130
135
|
|
131
|
-
return True
|
132
|
-
|
133
136
|
except Exception as e:
|
134
137
|
print(f"Failed to connect to Substrate node: {e}")
|
135
138
|
raise ConnectionError(
|
136
139
|
f"Could not connect to Substrate node at {self.url}: {e}"
|
137
140
|
)
|
138
141
|
|
139
|
-
return False
|
140
|
-
|
141
142
|
def _ensure_keypair(self) -> bool:
|
142
143
|
"""
|
143
144
|
Ensure we have a keypair for signing transactions.
|
@@ -201,7 +202,7 @@ class SubstrateClient:
|
|
201
202
|
|
202
203
|
Args:
|
203
204
|
name: Name for the new account
|
204
|
-
encode: Whether to encrypt the seed phrase with a password
|
205
|
+
encode: Whether to encrypt the seed phrase with a password.
|
205
206
|
password: Optional password for encryption (will prompt if not provided and encode=True)
|
206
207
|
|
207
208
|
Returns:
|
@@ -431,9 +432,9 @@ class SubstrateClient:
|
|
431
432
|
account_info["storage_stats"] = {
|
432
433
|
"files": total_files,
|
433
434
|
"bytes_used": total_size_bytes,
|
434
|
-
"size_formatted":
|
435
|
+
"size_formatted": format_size(total_size_bytes)
|
435
436
|
if total_size_bytes
|
436
|
-
else "0
|
437
|
+
else "0 bytes",
|
437
438
|
}
|
438
439
|
|
439
440
|
# Include file list if requested
|
@@ -463,17 +464,6 @@ class SubstrateClient:
|
|
463
464
|
|
464
465
|
return account_info
|
465
466
|
|
466
|
-
def _format_size(self, size_bytes: int) -> str:
|
467
|
-
"""Format file size in human-readable format"""
|
468
|
-
if size_bytes < 1024:
|
469
|
-
return f"{size_bytes} B"
|
470
|
-
elif size_bytes < 1024 * 1024:
|
471
|
-
return f"{size_bytes / 1024:.2f} KB"
|
472
|
-
elif size_bytes < 1024 * 1024 * 1024:
|
473
|
-
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
474
|
-
else:
|
475
|
-
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
476
|
-
|
477
467
|
def set_seed_phrase(self, seed_phrase: str) -> None:
|
478
468
|
"""
|
479
469
|
Set or update the seed phrase used for signing transactions.
|
@@ -495,7 +485,6 @@ class SubstrateClient:
|
|
495
485
|
print(f"Keypair created for account: {self._keypair.ss58_address}")
|
496
486
|
except Exception as e:
|
497
487
|
print(f"Warning: Could not create keypair from seed phrase: {e}")
|
498
|
-
print(f"Keypair will be created when needed")
|
499
488
|
|
500
489
|
async def storage_request(
|
501
490
|
self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
|
@@ -539,147 +528,119 @@ class SubstrateClient:
|
|
539
528
|
file_inputs.append(file)
|
540
529
|
|
541
530
|
# Print what is being submitted
|
542
|
-
print(f"Preparing storage request for {len(file_inputs)} files:")
|
543
531
|
for file in file_inputs:
|
544
532
|
print(f" - {file.file_name}: {file.file_hash}")
|
545
533
|
|
546
|
-
|
547
|
-
|
548
|
-
else:
|
549
|
-
print("No specific miners targeted (using default selection)")
|
550
|
-
|
551
|
-
try:
|
552
|
-
# Initialize Substrate connection
|
553
|
-
if not hasattr(self, "_substrate") or self._substrate is None:
|
554
|
-
print("Initializing Substrate connection...")
|
555
|
-
self._substrate = SubstrateInterface(
|
556
|
-
url=self.url,
|
557
|
-
ss58_format=42, # Substrate default
|
558
|
-
type_registry_preset="substrate-node-template",
|
559
|
-
)
|
560
|
-
print(f"Connected to Substrate node at {self.url}")
|
561
|
-
|
562
|
-
# Step 1: Create a JSON file with the list of files to pin
|
563
|
-
file_list = []
|
564
|
-
for file_input in file_inputs:
|
565
|
-
file_list.append(
|
566
|
-
{"filename": file_input.file_name, "cid": file_input.file_hash}
|
567
|
-
)
|
534
|
+
# Initialize Substrate connection
|
535
|
+
substrate, _ = initialize_substrate_connection(self)
|
568
536
|
|
569
|
-
|
570
|
-
|
571
|
-
|
537
|
+
# Step 1: Create a JSON file with the list of files to pin
|
538
|
+
file_list = []
|
539
|
+
for file_input in file_inputs:
|
540
|
+
file_list.append(
|
541
|
+
{"filename": file_input.file_name, "cid": file_input.file_hash}
|
542
|
+
)
|
572
543
|
|
573
|
-
|
574
|
-
|
575
|
-
|
544
|
+
# Convert to JSON
|
545
|
+
files_json = json.dumps(file_list, indent=2)
|
546
|
+
print(f"Created file list with {len(file_list)} entries")
|
576
547
|
|
577
|
-
|
548
|
+
# Step 2: Upload the JSON file to IPFS
|
549
|
+
# Defer import to avoid circular imports
|
550
|
+
from hippius_sdk.ipfs import IPFSClient
|
578
551
|
|
579
|
-
|
580
|
-
with tempfile.NamedTemporaryFile(
|
581
|
-
mode="w+", suffix=".json", delete=False
|
582
|
-
) as temp_file:
|
583
|
-
temp_file_path = temp_file.name
|
584
|
-
temp_file.write(files_json)
|
552
|
+
ipfs_client = IPFSClient()
|
585
553
|
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
# Clean up the temporary file
|
593
|
-
if os.path.exists(temp_file_path):
|
594
|
-
os.remove(temp_file_path)
|
595
|
-
|
596
|
-
# Step 3: Submit the CID of the JSON file to the chain
|
597
|
-
# Create call parameters with the CID of the JSON file
|
598
|
-
call_params = {
|
599
|
-
"files_input": [
|
600
|
-
{
|
601
|
-
"file_hash": files_list_cid,
|
602
|
-
"file_name": f"files_list_{uuid.uuid4()}", # Generate a unique ID
|
603
|
-
}
|
604
|
-
],
|
605
|
-
"miner_ids": miner_ids if miner_ids else [],
|
606
|
-
}
|
607
|
-
|
608
|
-
# Create the call to the marketplace
|
609
|
-
print(f"Call parameters: {json.dumps(call_params, indent=2)}")
|
610
|
-
try:
|
611
|
-
call = self._substrate.compose_call(
|
612
|
-
call_module="Marketplace",
|
613
|
-
call_function="storage_request",
|
614
|
-
call_params=call_params,
|
615
|
-
)
|
616
|
-
except Exception as e:
|
617
|
-
print(f"Warning: Error composing call: {e}")
|
618
|
-
print("Attempting to use IpfsPallet.storeFile instead...")
|
554
|
+
# Create a temporary file with the JSON content
|
555
|
+
with tempfile.NamedTemporaryFile(
|
556
|
+
mode="w+", suffix=".json", delete=False
|
557
|
+
) as temp_file:
|
558
|
+
temp_file_path = temp_file.name
|
559
|
+
temp_file.write(files_json)
|
619
560
|
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
561
|
+
try:
|
562
|
+
print("Uploading file list to IPFS...")
|
563
|
+
upload_result = await ipfs_client.upload_file(temp_file_path)
|
564
|
+
files_list_cid = upload_result["cid"]
|
565
|
+
print(f"File list uploaded to IPFS with CID: {files_list_cid}")
|
566
|
+
finally:
|
567
|
+
# Clean up the temporary file
|
568
|
+
if os.path.exists(temp_file_path):
|
569
|
+
os.remove(temp_file_path)
|
570
|
+
|
571
|
+
# Step 3: Submit the CID of the JSON file to the chain
|
572
|
+
# Create call parameters with the CID of the JSON file
|
573
|
+
call_params = {
|
574
|
+
"files_input": [
|
575
|
+
{
|
576
|
+
"file_hash": files_list_cid,
|
577
|
+
"file_name": f"files_list_{uuid.uuid4()}", # Generate a unique ID
|
624
578
|
}
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
call_params=alt_call_params,
|
629
|
-
)
|
579
|
+
],
|
580
|
+
"miner_ids": miner_ids if miner_ids else [],
|
581
|
+
}
|
630
582
|
|
631
|
-
|
632
|
-
|
633
|
-
|
583
|
+
# Create the call to the marketplace
|
584
|
+
try:
|
585
|
+
call = self._substrate.compose_call(
|
586
|
+
call_module="Marketplace",
|
587
|
+
call_function="storage_request",
|
588
|
+
call_params=call_params,
|
634
589
|
)
|
590
|
+
except Exception as e:
|
591
|
+
print(f"Warning: Error composing call: {e}")
|
592
|
+
print("Attempting to use IpfsPallet.storeFile instead...")
|
635
593
|
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
594
|
+
# Try with IpfsPallet.storeFile as an alternative
|
595
|
+
alt_call_params = {
|
596
|
+
"fileHash": files_list_cid,
|
597
|
+
"fileName": f"files_list_{uuid.uuid4()}", # Generate a unique ID
|
598
|
+
}
|
599
|
+
call = self._substrate.compose_call(
|
600
|
+
call_module="IpfsPallet",
|
601
|
+
call_function="storeFile",
|
602
|
+
call_params=alt_call_params,
|
645
603
|
)
|
646
604
|
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
605
|
+
# Get payment info to estimate the fee
|
606
|
+
payment_info = self._substrate.get_payment_info(
|
607
|
+
call=call, keypair=self._keypair
|
608
|
+
)
|
651
609
|
|
652
|
-
|
653
|
-
f"Submitting transaction to store {len(file_list)} files via file list CID..."
|
654
|
-
)
|
610
|
+
print(f"Payment info: {json.dumps(payment_info, indent=2)}")
|
655
611
|
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
)
|
612
|
+
# Convert partialFee from Substrate (10^18 units) to a more readable format
|
613
|
+
estimated_fee = payment_info.get("partialFee", 0)
|
614
|
+
estimated_fee_formatted = (
|
615
|
+
float(estimated_fee) / 1_000_000_000_000_000_000 if estimated_fee else 0
|
616
|
+
)
|
617
|
+
print(
|
618
|
+
f"Estimated transaction fee: {estimated_fee} ({estimated_fee_formatted:.10f} tokens)"
|
619
|
+
)
|
660
620
|
|
661
|
-
|
662
|
-
|
621
|
+
# Create a signed extrinsic
|
622
|
+
extrinsic = self._substrate.create_signed_extrinsic(
|
623
|
+
call=call, keypair=self._keypair
|
624
|
+
)
|
663
625
|
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
print(f"All {len(file_list)} files will be stored through this request")
|
626
|
+
print(
|
627
|
+
f"Submitting transaction to store {len(file_list)} files via file list CID..."
|
628
|
+
)
|
668
629
|
|
669
|
-
|
630
|
+
# Submit the transaction
|
631
|
+
response = self._substrate.submit_extrinsic(
|
632
|
+
extrinsic=extrinsic, wait_for_inclusion=True
|
633
|
+
)
|
670
634
|
|
671
|
-
|
672
|
-
|
673
|
-
print(f"Error: {e}")
|
674
|
-
raise
|
675
|
-
except Exception as e:
|
676
|
-
print(f"Error interacting with Substrate: {e}")
|
677
|
-
raise
|
635
|
+
# Get the transaction hash
|
636
|
+
tx_hash = response.extrinsic_hash
|
678
637
|
|
679
|
-
return
|
638
|
+
return tx_hash
|
680
639
|
|
681
640
|
async def store_cid(
|
682
|
-
self,
|
641
|
+
self,
|
642
|
+
cid: str,
|
643
|
+
filename: str = None,
|
683
644
|
) -> str:
|
684
645
|
"""
|
685
646
|
Store a CID on the blockchain.
|
@@ -687,7 +648,6 @@ class SubstrateClient:
|
|
687
648
|
Args:
|
688
649
|
cid: Content Identifier (CID) to store
|
689
650
|
filename: Original filename (optional)
|
690
|
-
metadata: Additional metadata to store with the CID
|
691
651
|
|
692
652
|
Returns:
|
693
653
|
str: Transaction hash
|
@@ -695,58 +655,6 @@ class SubstrateClient:
|
|
695
655
|
file_input = FileInput(file_hash=cid, file_name=filename or "unnamed_file")
|
696
656
|
return await self.storage_request([file_input])
|
697
657
|
|
698
|
-
def get_cid_metadata(self, cid: str) -> Dict[str, Any]:
|
699
|
-
"""
|
700
|
-
Retrieve metadata for a CID from the blockchain.
|
701
|
-
|
702
|
-
Args:
|
703
|
-
cid: Content Identifier (CID) to query
|
704
|
-
|
705
|
-
Returns:
|
706
|
-
Dict[str, Any]: Metadata associated with the CID
|
707
|
-
"""
|
708
|
-
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
709
|
-
|
710
|
-
def get_account_cids(self, account_address: str) -> List[str]:
|
711
|
-
"""
|
712
|
-
Get all CIDs associated with an account.
|
713
|
-
|
714
|
-
Args:
|
715
|
-
account_address: Substrate account address
|
716
|
-
|
717
|
-
Returns:
|
718
|
-
List[str]: List of CIDs owned by the account
|
719
|
-
"""
|
720
|
-
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
721
|
-
|
722
|
-
def delete_cid(self, cid: str) -> str:
|
723
|
-
"""
|
724
|
-
Delete a CID from the blockchain (mark as removed).
|
725
|
-
|
726
|
-
Args:
|
727
|
-
cid: Content Identifier (CID) to delete
|
728
|
-
|
729
|
-
Returns:
|
730
|
-
str: Transaction hash
|
731
|
-
"""
|
732
|
-
# This requires a keypair for signing
|
733
|
-
if not self._ensure_keypair():
|
734
|
-
raise ValueError("Seed phrase must be set before making transactions")
|
735
|
-
|
736
|
-
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
737
|
-
|
738
|
-
def get_storage_fee(self, file_size_mb: float) -> float:
|
739
|
-
"""
|
740
|
-
Get the estimated storage fee for a file of given size.
|
741
|
-
|
742
|
-
Args:
|
743
|
-
file_size_mb: File size in megabytes
|
744
|
-
|
745
|
-
Returns:
|
746
|
-
float: Estimated fee in native tokens
|
747
|
-
"""
|
748
|
-
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
749
|
-
|
750
658
|
async def get_account_balance(
|
751
659
|
self, account_address: Optional[str] = None
|
752
660
|
) -> Dict[str, float]:
|
@@ -760,30 +668,17 @@ class SubstrateClient:
|
|
760
668
|
Dict[str, float]: Account balances (free, reserved, total)
|
761
669
|
"""
|
762
670
|
try:
|
763
|
-
# Initialize Substrate connection
|
764
|
-
|
765
|
-
print("Initializing Substrate connection...")
|
766
|
-
self._substrate = SubstrateInterface(
|
767
|
-
url=self.url,
|
768
|
-
ss58_format=42, # Substrate default
|
769
|
-
type_registry_preset="substrate-node-template",
|
770
|
-
)
|
771
|
-
print(f"Connected to Substrate node at {self.url}")
|
671
|
+
# Initialize Substrate connection and get account address
|
672
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
772
673
|
|
773
|
-
# Use provided account address or
|
674
|
+
# Use provided account address or the one derived from initialization
|
774
675
|
if not account_address:
|
775
|
-
if
|
776
|
-
account_address =
|
777
|
-
print(f"Using account address: {account_address}")
|
676
|
+
if derived_address:
|
677
|
+
account_address = derived_address
|
778
678
|
else:
|
779
|
-
|
780
|
-
if not self._ensure_keypair():
|
781
|
-
raise ValueError("No account address available")
|
782
|
-
account_address = self._keypair.ss58_address
|
783
|
-
print(f"Using keypair address: {account_address}")
|
679
|
+
raise ValueError("No account address available")
|
784
680
|
|
785
681
|
# Query the blockchain for account balance
|
786
|
-
print(f"Querying balance for account: {account_address}")
|
787
682
|
result = self._substrate.query(
|
788
683
|
module="System",
|
789
684
|
storage_function="Account",
|
@@ -793,7 +688,6 @@ class SubstrateClient:
|
|
793
688
|
# If account exists, extract the balance information
|
794
689
|
if result.value:
|
795
690
|
data = result.value
|
796
|
-
print(data)
|
797
691
|
# Extract balance components
|
798
692
|
free_balance = data.get("data", {}).get("free", 0)
|
799
693
|
reserved_balance = data.get("data", {}).get("reserved", 0)
|
@@ -821,7 +715,6 @@ class SubstrateClient:
|
|
821
715
|
},
|
822
716
|
}
|
823
717
|
else:
|
824
|
-
print(f"No account data found for: {account_address}")
|
825
718
|
return {
|
826
719
|
"free": 0.0,
|
827
720
|
"reserved": 0.0,
|
@@ -831,9 +724,7 @@ class SubstrateClient:
|
|
831
724
|
}
|
832
725
|
|
833
726
|
except Exception as e:
|
834
|
-
|
835
|
-
print(error_msg)
|
836
|
-
raise ValueError(error_msg)
|
727
|
+
raise ValueError(f"Error querying account balance: {str(e)}")
|
837
728
|
|
838
729
|
async def watch_account_balance(
|
839
730
|
self, account_address: Optional[str] = None, interval: int = 5
|
@@ -858,10 +749,6 @@ class SubstrateClient:
|
|
858
749
|
raise ValueError("No account address available")
|
859
750
|
account_address = self._keypair.ss58_address
|
860
751
|
|
861
|
-
print(f"Watching balance for account: {account_address}")
|
862
|
-
print(f"Updates every {interval} seconds. Press Ctrl+C to stop.")
|
863
|
-
print("-" * 80)
|
864
|
-
|
865
752
|
# Keep track of previous balance to show changes
|
866
753
|
previous_balance = None
|
867
754
|
|
@@ -942,27 +829,15 @@ class SubstrateClient:
|
|
942
829
|
ValueError: If account has no credits
|
943
830
|
"""
|
944
831
|
try:
|
945
|
-
# Initialize Substrate connection
|
946
|
-
|
947
|
-
print("Initializing Substrate connection...")
|
948
|
-
self._substrate = SubstrateInterface(
|
949
|
-
url=self.url,
|
950
|
-
ss58_format=42, # Substrate default
|
951
|
-
type_registry_preset="substrate-node-template",
|
952
|
-
)
|
953
|
-
print(f"Connected to Substrate node at {self.url}")
|
832
|
+
# Initialize Substrate connection and get account address
|
833
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
954
834
|
|
955
|
-
# Use provided account address or
|
835
|
+
# Use provided account address or the one derived from initialization
|
956
836
|
if not account_address:
|
957
|
-
if
|
958
|
-
account_address =
|
959
|
-
print(f"Using account address: {account_address}")
|
837
|
+
if derived_address:
|
838
|
+
account_address = derived_address
|
960
839
|
else:
|
961
|
-
|
962
|
-
if not self._ensure_keypair():
|
963
|
-
raise ValueError("No account address available")
|
964
|
-
account_address = self._keypair.ss58_address
|
965
|
-
print(f"Using keypair address: {account_address}")
|
840
|
+
raise ValueError("No account address available")
|
966
841
|
|
967
842
|
# Query the blockchain for free credits
|
968
843
|
print(f"Querying free credits for account: {account_address}")
|
@@ -1006,30 +881,17 @@ class SubstrateClient:
|
|
1006
881
|
ValueError: If query fails or no files found
|
1007
882
|
"""
|
1008
883
|
try:
|
1009
|
-
# Initialize Substrate connection
|
1010
|
-
|
1011
|
-
print("Initializing Substrate connection...")
|
1012
|
-
self._substrate = SubstrateInterface(
|
1013
|
-
url=self.url,
|
1014
|
-
ss58_format=42, # Substrate default
|
1015
|
-
type_registry_preset="substrate-node-template",
|
1016
|
-
)
|
1017
|
-
print(f"Connected to Substrate node at {self.url}")
|
884
|
+
# Initialize Substrate connection and get account address
|
885
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
1018
886
|
|
1019
|
-
# Use provided account address or
|
887
|
+
# Use provided account address or the one derived from initialization
|
1020
888
|
if not account_address:
|
1021
|
-
if
|
1022
|
-
account_address =
|
1023
|
-
print(f"Using account address: {account_address}")
|
889
|
+
if derived_address:
|
890
|
+
account_address = derived_address
|
1024
891
|
else:
|
1025
|
-
|
1026
|
-
if not self._ensure_keypair():
|
1027
|
-
raise ValueError("No account address available")
|
1028
|
-
account_address = self._keypair.ss58_address
|
1029
|
-
print(f"Using keypair address: {account_address}")
|
892
|
+
raise ValueError("No account address available")
|
1030
893
|
|
1031
894
|
# Query the blockchain for user file hashes
|
1032
|
-
print(f"Querying file hashes for account: {account_address}")
|
1033
895
|
result = self._substrate.query(
|
1034
896
|
module="Marketplace",
|
1035
897
|
storage_function="UserFileHashes",
|
@@ -1040,16 +902,12 @@ class SubstrateClient:
|
|
1040
902
|
if result.value:
|
1041
903
|
# The result is already a list of bytes, convert each to string
|
1042
904
|
file_hashes = [cid.hex() for cid in result.value]
|
1043
|
-
print(f"Found {len(file_hashes)} files stored by this account")
|
1044
905
|
return file_hashes
|
1045
906
|
else:
|
1046
|
-
print(f"No files found for account: {account_address}")
|
1047
907
|
return []
|
1048
908
|
|
1049
909
|
except Exception as e:
|
1050
|
-
|
1051
|
-
print(error_msg)
|
1052
|
-
raise ValueError(error_msg)
|
910
|
+
raise ValueError(f"Error querying user file hashes: {str(e)}")
|
1053
911
|
|
1054
912
|
async def get_user_files(
|
1055
913
|
self,
|
@@ -1074,7 +932,6 @@ class SubstrateClient:
|
|
1074
932
|
"file_hash": str, # The IPFS CID of the file
|
1075
933
|
"file_name": str, # The name of the file
|
1076
934
|
"miner_ids": List[str], # List of miner IDs that have pinned the file
|
1077
|
-
"miner_ids_full": List[str], # Complete list of miner IDs (if truncated)
|
1078
935
|
"miner_count": int, # Total number of miners
|
1079
936
|
"file_size": int, # Size of the file in bytes
|
1080
937
|
"size_formatted": str # Human-readable file size
|
@@ -1108,90 +965,37 @@ class SubstrateClient:
|
|
1108
965
|
ValueError: If query fails or profile cannot be retrieved
|
1109
966
|
"""
|
1110
967
|
try:
|
1111
|
-
# Initialize Substrate connection
|
1112
|
-
|
1113
|
-
print("Initializing Substrate connection...")
|
1114
|
-
self._substrate = SubstrateInterface(
|
1115
|
-
url=self.url,
|
1116
|
-
ss58_format=42, # Substrate default
|
1117
|
-
type_registry_preset="substrate-node-template",
|
1118
|
-
)
|
1119
|
-
print(f"Connected to Substrate node at {self.url}")
|
968
|
+
# Initialize Substrate connection and get account address
|
969
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
1120
970
|
|
1121
|
-
# Use provided account address or
|
971
|
+
# Use provided account address or the one derived from initialization
|
1122
972
|
if not account_address:
|
1123
|
-
if
|
1124
|
-
account_address =
|
1125
|
-
print(f"Using account address: {account_address}")
|
973
|
+
if derived_address:
|
974
|
+
account_address = derived_address
|
1126
975
|
else:
|
1127
|
-
|
1128
|
-
if not self._ensure_keypair():
|
1129
|
-
raise ValueError("No account address available")
|
1130
|
-
account_address = self._keypair.ss58_address
|
1131
|
-
print(f"Using keypair address: {account_address}")
|
976
|
+
raise ValueError("No account address available")
|
1132
977
|
|
1133
978
|
# Query the blockchain for the user profile CID
|
1134
|
-
|
1135
|
-
result = self._substrate.query(
|
979
|
+
profile_hex_cid = self._substrate.query(
|
1136
980
|
module="IpfsPallet",
|
1137
981
|
storage_function="UserProfile",
|
1138
982
|
params=[account_address],
|
1139
|
-
)
|
983
|
+
).value
|
1140
984
|
|
1141
|
-
|
1142
|
-
if not result.value:
|
1143
|
-
print(f"No profile found for account: {account_address}")
|
985
|
+
if not profile_hex_cid:
|
1144
986
|
return []
|
1145
987
|
|
1146
|
-
|
1147
|
-
# Handle both cases: bytes (needs .hex()) and string (already hex)
|
1148
|
-
if isinstance(result.value, bytes):
|
1149
|
-
hex_cid = result.value.hex()
|
1150
|
-
else:
|
1151
|
-
# If it's already a string, use it directly
|
1152
|
-
hex_cid = result.value
|
1153
|
-
|
1154
|
-
# Remove '0x' prefix if present
|
1155
|
-
if hex_cid.startswith("0x"):
|
1156
|
-
hex_cid = hex_cid[2:]
|
1157
|
-
|
1158
|
-
print(f"Found user profile CID (hex): {hex_cid}")
|
1159
|
-
|
1160
|
-
# Convert the hex CID to a readable IPFS CID
|
1161
|
-
profile_cid = self._hex_to_ipfs_cid(hex_cid)
|
1162
|
-
print(f"Decoded IPFS CID: {profile_cid}")
|
988
|
+
profile_cid = self._hex_to_ipfs_cid(profile_hex_cid)
|
1163
989
|
|
1164
990
|
# Fetch the profile JSON from IPFS
|
1165
991
|
# Defer import to avoid circular imports
|
1166
992
|
from hippius_sdk.ipfs import IPFSClient
|
1167
993
|
|
1168
994
|
ipfs_client = IPFSClient()
|
1169
|
-
|
1170
|
-
|
1171
|
-
profile_data = await ipfs_client.cat(profile_cid)
|
1172
|
-
|
1173
|
-
# Parse the JSON content
|
1174
|
-
if not profile_data.get("is_text", False):
|
1175
|
-
raise ValueError("User profile is not in text format")
|
1176
|
-
|
1177
|
-
profile_json = json.loads(profile_data.get("content", "{}"))
|
1178
|
-
print(f"Successfully retrieved user profile")
|
1179
|
-
|
1180
|
-
# Extract the file list from the profile
|
1181
|
-
# The profile might be either a dictionary with a 'files' key or a direct list of files
|
1182
|
-
files = []
|
1183
|
-
if isinstance(profile_json, dict):
|
1184
|
-
files = profile_json.get("files", [])
|
1185
|
-
elif isinstance(profile_json, list):
|
1186
|
-
# The profile itself might be a list of files
|
1187
|
-
files = profile_json
|
1188
|
-
else:
|
1189
|
-
print(f"Warning: Unexpected profile structure: {type(profile_json)}")
|
1190
|
-
|
1191
|
-
print(f"Found {len(files)} files in user profile")
|
1192
|
-
|
1193
|
-
# Process the files to match the expected format
|
995
|
+
profile_content = (await ipfs_client.cat(profile_cid))["content"]
|
996
|
+
files = json.loads(profile_content)
|
1194
997
|
processed_files = []
|
998
|
+
|
1195
999
|
for file in files:
|
1196
1000
|
# Make sure file is a dictionary
|
1197
1001
|
if not isinstance(file, dict):
|
@@ -1208,37 +1012,10 @@ class SubstrateClient:
|
|
1208
1012
|
try:
|
1209
1013
|
# Convert array of numbers to bytes, then to a string
|
1210
1014
|
file_hash = bytes(raw_file_hash).decode("utf-8")
|
1211
|
-
except Exception:
|
1212
|
-
|
1213
|
-
else:
|
1214
|
-
# Try different field names for the CID that might be in the profile
|
1215
|
-
file_hash = (
|
1216
|
-
file.get("cid")
|
1217
|
-
or file.get("hash")
|
1218
|
-
or file.get("fileHash")
|
1219
|
-
or raw_file_hash
|
1220
|
-
)
|
1015
|
+
except Exception as e:
|
1016
|
+
print(e)
|
1221
1017
|
|
1222
1018
|
# Handle file_name: could be an array of ASCII/UTF-8 code points
|
1223
|
-
file_name = None
|
1224
|
-
raw_file_name = file.get("file_name")
|
1225
|
-
if isinstance(raw_file_name, list) and all(
|
1226
|
-
isinstance(n, int) for n in raw_file_name
|
1227
|
-
):
|
1228
|
-
try:
|
1229
|
-
# Convert array of numbers to bytes, then to a string
|
1230
|
-
file_name = bytes(raw_file_name).decode("utf-8")
|
1231
|
-
except Exception:
|
1232
|
-
pass
|
1233
|
-
else:
|
1234
|
-
# Try different field names for the filename
|
1235
|
-
file_name = (
|
1236
|
-
file.get("filename")
|
1237
|
-
or file.get("name")
|
1238
|
-
or file.get("fileName")
|
1239
|
-
or raw_file_name
|
1240
|
-
)
|
1241
|
-
|
1242
1019
|
# Try different field names for the size
|
1243
1020
|
file_size = (
|
1244
1021
|
file.get("size")
|
@@ -1249,25 +1026,18 @@ class SubstrateClient:
|
|
1249
1026
|
)
|
1250
1027
|
|
1251
1028
|
processed_file = {
|
1029
|
+
"cid": self._hex_to_ipfs_cid(file_hash),
|
1252
1030
|
"file_hash": file_hash,
|
1253
|
-
"file_name": file_name,
|
1254
|
-
|
1255
|
-
"miner_ids": file.get(
|
1256
|
-
"miner_ids", []
|
1257
|
-
), # Try to get miners if available
|
1031
|
+
"file_name": file.get("file_name"),
|
1032
|
+
"miner_ids": file.get("miner_ids", []),
|
1258
1033
|
"miner_count": len(file.get("miner_ids", [])), # Count the miners
|
1259
1034
|
"file_size": file_size or 0,
|
1035
|
+
"selected_validator": file["selected_validator"],
|
1260
1036
|
}
|
1261
1037
|
|
1262
1038
|
# Add formatted file size if available
|
1263
1039
|
if file_size:
|
1264
|
-
|
1265
|
-
if size_bytes >= 1024 * 1024:
|
1266
|
-
processed_file[
|
1267
|
-
"size_formatted"
|
1268
|
-
] = f"{size_bytes / (1024 * 1024):.2f} MB"
|
1269
|
-
else:
|
1270
|
-
processed_file["size_formatted"] = f"{size_bytes / 1024:.2f} KB"
|
1040
|
+
processed_file["size_formatted"] = format_size(file_size)
|
1271
1041
|
else:
|
1272
1042
|
processed_file["size_formatted"] = "Unknown"
|
1273
1043
|
|
@@ -1276,9 +1046,7 @@ class SubstrateClient:
|
|
1276
1046
|
return processed_files
|
1277
1047
|
|
1278
1048
|
except Exception as e:
|
1279
|
-
|
1280
|
-
print(error_msg)
|
1281
|
-
raise ValueError(error_msg)
|
1049
|
+
raise ValueError(f"Error retrieving user files from profile: {str(e)}")
|
1282
1050
|
|
1283
1051
|
def get_pinning_status(
|
1284
1052
|
self, account_address: Optional[str] = None
|
@@ -1311,165 +1079,48 @@ class SubstrateClient:
|
|
1311
1079
|
ConnectionError: If connection to Substrate fails
|
1312
1080
|
ValueError: If query fails or no requests found
|
1313
1081
|
"""
|
1314
|
-
|
1315
|
-
|
1316
|
-
if not hasattr(self, "_substrate") or self._substrate is None:
|
1317
|
-
print("Initializing Substrate connection...")
|
1318
|
-
self._substrate = SubstrateInterface(
|
1319
|
-
url=self.url,
|
1320
|
-
ss58_format=42, # Substrate default
|
1321
|
-
type_registry_preset="substrate-node-template",
|
1322
|
-
)
|
1323
|
-
print(f"Connected to Substrate node at {self.url}")
|
1082
|
+
# Initialize Substrate connection and get account address
|
1083
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
1324
1084
|
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1085
|
+
# Use provided account address or the one derived from initialization
|
1086
|
+
if not account_address:
|
1087
|
+
if derived_address:
|
1088
|
+
account_address = derived_address
|
1089
|
+
else:
|
1090
|
+
raise ValueError("No account address available")
|
1091
|
+
|
1092
|
+
# Query the blockchain for storage requests
|
1093
|
+
print(f"Querying storage requests for account: {account_address}")
|
1094
|
+
storage_requests = []
|
1095
|
+
|
1096
|
+
# First, try with query_map which is more suitable for iterating over collections
|
1097
|
+
result = self._substrate.query_map(
|
1098
|
+
module="IpfsPallet",
|
1099
|
+
storage_function="UserStorageRequests",
|
1100
|
+
params=[account_address],
|
1101
|
+
)
|
1102
|
+
|
1103
|
+
results_list = list(result)
|
1104
|
+
for key, substrate_result in results_list:
|
1105
|
+
# Extract file hash from key if possible
|
1106
|
+
file_hash_hex = None
|
1107
|
+
if key is not None:
|
1108
|
+
if hasattr(key, "hex"):
|
1109
|
+
file_hash_hex = key.hex()
|
1110
|
+
elif isinstance(key, bytes):
|
1111
|
+
file_hash_hex = key.hex()
|
1112
|
+
elif isinstance(key, str) and key.startswith("0x"):
|
1113
|
+
file_hash_hex = key[2:]
|
1330
1114
|
else:
|
1331
|
-
|
1332
|
-
if not self._ensure_keypair():
|
1333
|
-
raise ValueError("No account address available")
|
1334
|
-
account_address = self._keypair.ss58_address
|
1335
|
-
print(f"Using keypair address: {account_address}")
|
1336
|
-
|
1337
|
-
# Query the blockchain for storage requests
|
1338
|
-
print(f"Querying storage requests for account: {account_address}")
|
1339
|
-
try:
|
1340
|
-
# First, try with query_map which is more suitable for iterating over collections
|
1341
|
-
result = self._substrate.query_map(
|
1342
|
-
module="IpfsPallet",
|
1343
|
-
storage_function="UserStorageRequests",
|
1344
|
-
params=[account_address],
|
1345
|
-
)
|
1346
|
-
results_list = list(result)
|
1347
|
-
except Exception as e:
|
1348
|
-
print(f"Error with query_map: {e}")
|
1349
|
-
try:
|
1350
|
-
# Try again with query to double check storage function requirements
|
1351
|
-
result = self._substrate.query(
|
1352
|
-
module="IpfsPallet",
|
1353
|
-
storage_function="UserStorageRequests",
|
1354
|
-
params=[
|
1355
|
-
account_address,
|
1356
|
-
None,
|
1357
|
-
], # Try with a None second parameter
|
1358
|
-
)
|
1359
|
-
|
1360
|
-
# If the query returns a nested structure, extract it
|
1361
|
-
if result.value and isinstance(result.value, list):
|
1362
|
-
# Convert to a list format similar to query_map for processing
|
1363
|
-
results_list = []
|
1364
|
-
for item in result.value:
|
1365
|
-
if isinstance(item, list) and len(item) >= 2:
|
1366
|
-
key = item[0]
|
1367
|
-
value = item[1]
|
1368
|
-
results_list.append((key, value))
|
1369
|
-
else:
|
1370
|
-
# If it's not a nested structure, use a simpler format
|
1371
|
-
results_list = [(None, result.value)] if result.value else []
|
1372
|
-
except Exception as e_inner:
|
1373
|
-
print(f"Error with fallback query: {e_inner}")
|
1374
|
-
# If both methods fail, return an empty list
|
1375
|
-
results_list = []
|
1376
|
-
|
1377
|
-
# Process the storage requests
|
1378
|
-
storage_requests = []
|
1379
|
-
|
1380
|
-
if not results_list:
|
1381
|
-
print(f"No storage requests found for account: {account_address}")
|
1382
|
-
return []
|
1383
|
-
|
1384
|
-
print(f"Found {len(results_list)} storage request entries")
|
1115
|
+
file_hash_hex = str(key)
|
1385
1116
|
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
print(f"Entry {i+1}:")
|
1390
|
-
print(f" Raw key: {key}, type: {type(key)}")
|
1391
|
-
print(f" Raw value: {value}, type: {type(value)}")
|
1392
|
-
|
1393
|
-
# Extract file hash from key if possible
|
1394
|
-
file_hash_hex = None
|
1395
|
-
if key is not None:
|
1396
|
-
if hasattr(key, "hex"):
|
1397
|
-
file_hash_hex = key.hex()
|
1398
|
-
elif isinstance(key, bytes):
|
1399
|
-
file_hash_hex = key.hex()
|
1400
|
-
elif isinstance(key, str) and key.startswith("0x"):
|
1401
|
-
file_hash_hex = key[2:]
|
1402
|
-
else:
|
1403
|
-
file_hash_hex = str(key)
|
1404
|
-
|
1405
|
-
# Try to extract value data
|
1406
|
-
request_data = None
|
1407
|
-
if isinstance(value, dict):
|
1408
|
-
request_data = value
|
1409
|
-
elif hasattr(value, "get"):
|
1410
|
-
request_data = value
|
1411
|
-
elif hasattr(value, "__dict__"):
|
1412
|
-
# Convert object to dict
|
1413
|
-
request_data = {
|
1414
|
-
k: getattr(value, k)
|
1415
|
-
for k in dir(value)
|
1416
|
-
if not k.startswith("_") and not callable(getattr(value, k))
|
1417
|
-
}
|
1418
|
-
|
1419
|
-
# If we can't extract data, just use value as string for debugging
|
1420
|
-
if request_data is None:
|
1421
|
-
request_data = {"raw_value": str(value)}
|
1422
|
-
|
1423
|
-
# Create formatted request with available data
|
1424
|
-
formatted_request = {"raw_key": str(key), "raw_value": str(value)}
|
1425
|
-
|
1426
|
-
# Directly extract file_name from the value if it's a dict-like object
|
1427
|
-
if hasattr(value, "get"):
|
1428
|
-
if value.get("file_name"):
|
1429
|
-
formatted_request["file_name"] = value.get("file_name")
|
1430
|
-
elif value.get("fileName"):
|
1431
|
-
formatted_request["file_name"] = value.get("fileName")
|
1432
|
-
|
1433
|
-
# Add CID if we have it
|
1434
|
-
if file_hash_hex:
|
1435
|
-
file_cid = self._hex_to_ipfs_cid(file_hash_hex)
|
1436
|
-
formatted_request["cid"] = file_cid
|
1437
|
-
|
1438
|
-
# Add other fields from request_data if available
|
1439
|
-
for source_field, target_field in [
|
1440
|
-
("fileName", "file_name"),
|
1441
|
-
("totalReplicas", "total_replicas"),
|
1442
|
-
("owner", "owner"),
|
1443
|
-
("createdAt", "created_at"),
|
1444
|
-
("lastChargedAt", "last_charged_at"),
|
1445
|
-
("minerIds", "miner_ids"),
|
1446
|
-
("selectedValidator", "selected_validator"),
|
1447
|
-
("isAssigned", "is_assigned"),
|
1448
|
-
# Add variants that might appear differently in the chain storage
|
1449
|
-
("file_name", "file_name"),
|
1450
|
-
("file_hash", "file_hash"),
|
1451
|
-
("total_replicas", "total_replicas"),
|
1452
|
-
]:
|
1453
|
-
if source_field in request_data:
|
1454
|
-
formatted_request[target_field] = request_data[source_field]
|
1455
|
-
# Fallback to attribute access for different types of objects
|
1456
|
-
elif hasattr(value, source_field):
|
1457
|
-
formatted_request[target_field] = getattr(
|
1458
|
-
value, source_field
|
1459
|
-
)
|
1460
|
-
|
1461
|
-
storage_requests.append(formatted_request)
|
1117
|
+
if file_hash_hex:
|
1118
|
+
file_cid = self._hex_to_ipfs_cid(file_hash_hex)
|
1119
|
+
substrate_result.value["cid"] = file_cid
|
1462
1120
|
|
1463
|
-
|
1464
|
-
print(f"Error processing request entry {i+1}: {e}")
|
1121
|
+
storage_requests.append(substrate_result.value)
|
1465
1122
|
|
1466
|
-
|
1467
|
-
return storage_requests
|
1468
|
-
|
1469
|
-
except Exception as e:
|
1470
|
-
error_msg = f"Error querying storage requests: {str(e)}"
|
1471
|
-
print(error_msg)
|
1472
|
-
raise ValueError(error_msg)
|
1123
|
+
return storage_requests
|
1473
1124
|
|
1474
1125
|
def _hex_to_ipfs_cid(self, hex_string: str) -> str:
|
1475
1126
|
"""
|
@@ -1482,3 +1133,42 @@ class SubstrateClient:
|
|
1482
1133
|
str: Regular IPFS CID
|
1483
1134
|
"""
|
1484
1135
|
return hex_to_ipfs_cid(hex_string)
|
1136
|
+
|
1137
|
+
async def cancel_storage_request(self, cid: str) -> str:
|
1138
|
+
"""
|
1139
|
+
Cancel a storage request by CID from the Hippius blockchain.
|
1140
|
+
|
1141
|
+
Args:
|
1142
|
+
cid: Content Identifier (CID) of the file to cancel
|
1143
|
+
|
1144
|
+
Returns:
|
1145
|
+
str: Transaction hash
|
1146
|
+
"""
|
1147
|
+
if not self._ensure_keypair():
|
1148
|
+
raise ValueError("Seed phrase must be set before making transactions")
|
1149
|
+
|
1150
|
+
substrate, _ = initialize_substrate_connection(self)
|
1151
|
+
|
1152
|
+
call = self._substrate.compose_call(
|
1153
|
+
call_module="Marketplace",
|
1154
|
+
call_function="storage_unpin_request",
|
1155
|
+
call_params={
|
1156
|
+
"file_hash": cid,
|
1157
|
+
},
|
1158
|
+
)
|
1159
|
+
|
1160
|
+
# Get payment info and show estimated transaction fee
|
1161
|
+
payment_info = self._substrate.get_payment_info(
|
1162
|
+
call=call, keypair=self._keypair
|
1163
|
+
)
|
1164
|
+
print(f"Payment info: {json.dumps(payment_info, indent=2)}")
|
1165
|
+
fee = payment_info.get("partialFee", 0)
|
1166
|
+
fee_tokens = fee / 10**12 if fee > 0 else 0
|
1167
|
+
print(f"Estimated transaction fee: {fee} ({fee_tokens:.10f} tokens)")
|
1168
|
+
|
1169
|
+
extrinsic = self._substrate.create_signed_extrinsic(
|
1170
|
+
call=call, keypair=self._keypair
|
1171
|
+
)
|
1172
|
+
response = self._substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
|
1173
|
+
print(f"Transaction hash: {response.extrinsic_hash}")
|
1174
|
+
return response.extrinsic_hash
|