hippius 0.2.2__py3-none-any.whl → 0.2.4__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.2.dist-info → hippius-0.2.4.dist-info}/METADATA +213 -156
- hippius-0.2.4.dist-info/RECORD +16 -0
- hippius_sdk/__init__.py +10 -21
- hippius_sdk/cli.py +282 -2627
- hippius_sdk/cli_assets.py +10 -0
- hippius_sdk/cli_handlers.py +2773 -0
- hippius_sdk/cli_parser.py +607 -0
- hippius_sdk/cli_rich.py +247 -0
- hippius_sdk/client.py +70 -22
- hippius_sdk/config.py +109 -142
- hippius_sdk/ipfs.py +435 -58
- hippius_sdk/ipfs_core.py +22 -1
- hippius_sdk/substrate.py +234 -553
- hippius_sdk/utils.py +84 -2
- hippius-0.2.2.dist-info/RECORD +0 -12
- {hippius-0.2.2.dist-info → hippius-0.2.4.dist-info}/WHEEL +0 -0
- {hippius-0.2.2.dist-info → hippius-0.2.4.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
|
@@ -10,16 +11,12 @@ from dotenv import load_dotenv
|
|
10
11
|
from mnemonic import Mnemonic
|
11
12
|
from substrateinterface import Keypair, SubstrateInterface
|
12
13
|
|
13
|
-
from hippius_sdk.config import (
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
set_active_account,
|
20
|
-
set_seed_phrase,
|
21
|
-
)
|
22
|
-
from hippius_sdk.utils import hex_to_ipfs_cid
|
14
|
+
from hippius_sdk.config import (get_account_address, get_active_account,
|
15
|
+
get_all_config, get_config_value,
|
16
|
+
get_seed_phrase, set_active_account,
|
17
|
+
set_seed_phrase)
|
18
|
+
from hippius_sdk.utils import (format_size, hex_to_ipfs_cid,
|
19
|
+
initialize_substrate_connection)
|
23
20
|
|
24
21
|
# Load environment variables
|
25
22
|
load_dotenv()
|
@@ -128,16 +125,12 @@ class SubstrateClient:
|
|
128
125
|
print("Connected successfully (read-only mode, no account)")
|
129
126
|
self._read_only = True
|
130
127
|
|
131
|
-
return True
|
132
|
-
|
133
128
|
except Exception as e:
|
134
129
|
print(f"Failed to connect to Substrate node: {e}")
|
135
130
|
raise ConnectionError(
|
136
131
|
f"Could not connect to Substrate node at {self.url}: {e}"
|
137
132
|
)
|
138
133
|
|
139
|
-
return False
|
140
|
-
|
141
134
|
def _ensure_keypair(self) -> bool:
|
142
135
|
"""
|
143
136
|
Ensure we have a keypair for signing transactions.
|
@@ -154,7 +147,6 @@ class SubstrateClient:
|
|
154
147
|
try:
|
155
148
|
self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
|
156
149
|
self._account_address = self._keypair.ss58_address
|
157
|
-
print(f"Keypair created for account: {self._keypair.ss58_address}")
|
158
150
|
self._read_only = False
|
159
151
|
return True
|
160
152
|
except Exception as e:
|
@@ -162,22 +154,14 @@ class SubstrateClient:
|
|
162
154
|
return False
|
163
155
|
|
164
156
|
# Otherwise, try to get the seed phrase from config
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
)
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
print(f"Keypair created for account: {self._keypair.ss58_address}")
|
174
|
-
self._read_only = False
|
175
|
-
return True
|
176
|
-
else:
|
177
|
-
print("No seed phrase available. Cannot sign transactions.")
|
178
|
-
return False
|
179
|
-
except Exception as e:
|
180
|
-
print(f"Warning: Could not get seed phrase from config: {e}")
|
157
|
+
config_seed = get_seed_phrase(self._seed_phrase_password, self._account_name)
|
158
|
+
if config_seed:
|
159
|
+
self._seed_phrase = config_seed
|
160
|
+
self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
|
161
|
+
self._account_address = self._keypair.ss58_address
|
162
|
+
self._read_only = False
|
163
|
+
return True
|
164
|
+
else:
|
181
165
|
return False
|
182
166
|
|
183
167
|
def generate_mnemonic(self) -> str:
|
@@ -193,6 +177,15 @@ class SubstrateClient:
|
|
193
177
|
except Exception as e:
|
194
178
|
raise ValueError(f"Error generating mnemonic: {e}")
|
195
179
|
|
180
|
+
def generate_seed_phrase(self) -> str:
|
181
|
+
"""
|
182
|
+
Generate a new random seed phrase (alias for generate_mnemonic).
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
str: A 12-word mnemonic seed phrase
|
186
|
+
"""
|
187
|
+
return self.generate_mnemonic()
|
188
|
+
|
196
189
|
def create_account(
|
197
190
|
self, name: str, encode: bool = False, password: Optional[str] = None
|
198
191
|
) -> Dict[str, Any]:
|
@@ -201,7 +194,7 @@ class SubstrateClient:
|
|
201
194
|
|
202
195
|
Args:
|
203
196
|
name: Name for the new account
|
204
|
-
encode: Whether to encrypt the seed phrase with a password
|
197
|
+
encode: Whether to encrypt the seed phrase with a password.
|
205
198
|
password: Optional password for encryption (will prompt if not provided and encode=True)
|
206
199
|
|
207
200
|
Returns:
|
@@ -431,9 +424,9 @@ class SubstrateClient:
|
|
431
424
|
account_info["storage_stats"] = {
|
432
425
|
"files": total_files,
|
433
426
|
"bytes_used": total_size_bytes,
|
434
|
-
"size_formatted":
|
427
|
+
"size_formatted": format_size(total_size_bytes)
|
435
428
|
if total_size_bytes
|
436
|
-
else "0
|
429
|
+
else "0 bytes",
|
437
430
|
}
|
438
431
|
|
439
432
|
# Include file list if requested
|
@@ -463,17 +456,6 @@ class SubstrateClient:
|
|
463
456
|
|
464
457
|
return account_info
|
465
458
|
|
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
459
|
def set_seed_phrase(self, seed_phrase: str) -> None:
|
478
460
|
"""
|
479
461
|
Set or update the seed phrase used for signing transactions.
|
@@ -492,10 +474,8 @@ class SubstrateClient:
|
|
492
474
|
try:
|
493
475
|
self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
|
494
476
|
self._account_address = self._keypair.ss58_address
|
495
|
-
print(f"Keypair created for account: {self._keypair.ss58_address}")
|
496
477
|
except Exception as e:
|
497
|
-
|
498
|
-
print(f"Keypair will be created when needed")
|
478
|
+
raise ValueError(f"Could not create keypair from seed phrase: {e}")
|
499
479
|
|
500
480
|
async def storage_request(
|
501
481
|
self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
|
@@ -539,147 +519,119 @@ class SubstrateClient:
|
|
539
519
|
file_inputs.append(file)
|
540
520
|
|
541
521
|
# Print what is being submitted
|
542
|
-
print(f"Preparing storage request for {len(file_inputs)} files:")
|
543
522
|
for file in file_inputs:
|
544
523
|
print(f" - {file.file_name}: {file.file_hash}")
|
545
524
|
|
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
|
-
)
|
568
|
-
|
569
|
-
# Convert to JSON
|
570
|
-
files_json = json.dumps(file_list, indent=2)
|
571
|
-
print(f"Created file list with {len(file_list)} entries")
|
525
|
+
# Initialize Substrate connection
|
526
|
+
substrate, _ = initialize_substrate_connection(self)
|
572
527
|
|
573
|
-
|
574
|
-
|
575
|
-
|
528
|
+
# Step 1: Create a JSON file with the list of files to pin
|
529
|
+
file_list = []
|
530
|
+
for file_input in file_inputs:
|
531
|
+
file_list.append(
|
532
|
+
{"filename": file_input.file_name, "cid": file_input.file_hash}
|
533
|
+
)
|
576
534
|
|
577
|
-
|
535
|
+
# Convert to JSON
|
536
|
+
files_json = json.dumps(file_list, indent=2)
|
537
|
+
print(f"Created file list with {len(file_list)} entries")
|
578
538
|
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
) as temp_file:
|
583
|
-
temp_file_path = temp_file.name
|
584
|
-
temp_file.write(files_json)
|
539
|
+
# Step 2: Upload the JSON file to IPFS
|
540
|
+
# Defer import to avoid circular imports
|
541
|
+
from hippius_sdk.ipfs import IPFSClient
|
585
542
|
|
586
|
-
|
587
|
-
print("Uploading file list to IPFS...")
|
588
|
-
upload_result = await ipfs_client.upload_file(temp_file_path)
|
589
|
-
files_list_cid = upload_result["cid"]
|
590
|
-
print(f"File list uploaded to IPFS with CID: {files_list_cid}")
|
591
|
-
finally:
|
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
|
-
}
|
543
|
+
ipfs_client = IPFSClient()
|
607
544
|
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
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...")
|
545
|
+
# Create a temporary file with the JSON content
|
546
|
+
with tempfile.NamedTemporaryFile(
|
547
|
+
mode="w+", suffix=".json", delete=False
|
548
|
+
) as temp_file:
|
549
|
+
temp_file_path = temp_file.name
|
550
|
+
temp_file.write(files_json)
|
619
551
|
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
552
|
+
try:
|
553
|
+
print("Uploading file list to IPFS...")
|
554
|
+
upload_result = await ipfs_client.upload_file(temp_file_path)
|
555
|
+
files_list_cid = upload_result["cid"]
|
556
|
+
print(f"File list uploaded to IPFS with CID: {files_list_cid}")
|
557
|
+
finally:
|
558
|
+
# Clean up the temporary file
|
559
|
+
if os.path.exists(temp_file_path):
|
560
|
+
os.remove(temp_file_path)
|
561
|
+
|
562
|
+
# Step 3: Submit the CID of the JSON file to the chain
|
563
|
+
# Create call parameters with the CID of the JSON file
|
564
|
+
call_params = {
|
565
|
+
"files_input": [
|
566
|
+
{
|
567
|
+
"file_hash": files_list_cid,
|
568
|
+
"file_name": f"files_list_{uuid.uuid4()}", # Generate a unique ID
|
624
569
|
}
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
call_params=alt_call_params,
|
629
|
-
)
|
570
|
+
],
|
571
|
+
"miner_ids": miner_ids if miner_ids else [],
|
572
|
+
}
|
630
573
|
|
631
|
-
|
632
|
-
|
633
|
-
|
574
|
+
# Create the call to the marketplace
|
575
|
+
try:
|
576
|
+
call = self._substrate.compose_call(
|
577
|
+
call_module="Marketplace",
|
578
|
+
call_function="storage_request",
|
579
|
+
call_params=call_params,
|
634
580
|
)
|
581
|
+
except Exception as e:
|
582
|
+
print(f"Warning: Error composing call: {e}")
|
583
|
+
print("Attempting to use IpfsPallet.storeFile instead...")
|
635
584
|
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
585
|
+
# Try with IpfsPallet.storeFile as an alternative
|
586
|
+
alt_call_params = {
|
587
|
+
"fileHash": files_list_cid,
|
588
|
+
"fileName": f"files_list_{uuid.uuid4()}", # Generate a unique ID
|
589
|
+
}
|
590
|
+
call = self._substrate.compose_call(
|
591
|
+
call_module="IpfsPallet",
|
592
|
+
call_function="storeFile",
|
593
|
+
call_params=alt_call_params,
|
645
594
|
)
|
646
595
|
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
596
|
+
# Get payment info to estimate the fee
|
597
|
+
payment_info = self._substrate.get_payment_info(
|
598
|
+
call=call, keypair=self._keypair
|
599
|
+
)
|
651
600
|
|
652
|
-
|
653
|
-
f"Submitting transaction to store {len(file_list)} files via file list CID..."
|
654
|
-
)
|
601
|
+
print(f"Payment info: {json.dumps(payment_info, indent=2)}")
|
655
602
|
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
)
|
603
|
+
# Convert partialFee from Substrate (10^18 units) to a more readable format
|
604
|
+
estimated_fee = payment_info.get("partialFee", 0)
|
605
|
+
estimated_fee_formatted = (
|
606
|
+
float(estimated_fee) / 1_000_000_000_000_000_000 if estimated_fee else 0
|
607
|
+
)
|
608
|
+
print(
|
609
|
+
f"Estimated transaction fee: {estimated_fee} ({estimated_fee_formatted:.10f} tokens)"
|
610
|
+
)
|
660
611
|
|
661
|
-
|
662
|
-
|
612
|
+
# Create a signed extrinsic
|
613
|
+
extrinsic = self._substrate.create_signed_extrinsic(
|
614
|
+
call=call, keypair=self._keypair
|
615
|
+
)
|
663
616
|
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
print(f"All {len(file_list)} files will be stored through this request")
|
617
|
+
print(
|
618
|
+
f"Submitting transaction to store {len(file_list)} files via file list CID..."
|
619
|
+
)
|
668
620
|
|
669
|
-
|
621
|
+
# Submit the transaction
|
622
|
+
response = self._substrate.submit_extrinsic(
|
623
|
+
extrinsic=extrinsic, wait_for_inclusion=True
|
624
|
+
)
|
670
625
|
|
671
|
-
|
672
|
-
|
673
|
-
print(f"Error: {e}")
|
674
|
-
raise
|
675
|
-
except Exception as e:
|
676
|
-
print(f"Error interacting with Substrate: {e}")
|
677
|
-
raise
|
626
|
+
# Get the transaction hash
|
627
|
+
tx_hash = response.extrinsic_hash
|
678
628
|
|
679
|
-
return
|
629
|
+
return tx_hash
|
680
630
|
|
681
631
|
async def store_cid(
|
682
|
-
self,
|
632
|
+
self,
|
633
|
+
cid: str,
|
634
|
+
filename: str = None,
|
683
635
|
) -> str:
|
684
636
|
"""
|
685
637
|
Store a CID on the blockchain.
|
@@ -687,7 +639,6 @@ class SubstrateClient:
|
|
687
639
|
Args:
|
688
640
|
cid: Content Identifier (CID) to store
|
689
641
|
filename: Original filename (optional)
|
690
|
-
metadata: Additional metadata to store with the CID
|
691
642
|
|
692
643
|
Returns:
|
693
644
|
str: Transaction hash
|
@@ -695,58 +646,6 @@ class SubstrateClient:
|
|
695
646
|
file_input = FileInput(file_hash=cid, file_name=filename or "unnamed_file")
|
696
647
|
return await self.storage_request([file_input])
|
697
648
|
|
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
649
|
async def get_account_balance(
|
751
650
|
self, account_address: Optional[str] = None
|
752
651
|
) -> Dict[str, float]:
|
@@ -760,30 +659,17 @@ class SubstrateClient:
|
|
760
659
|
Dict[str, float]: Account balances (free, reserved, total)
|
761
660
|
"""
|
762
661
|
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}")
|
662
|
+
# Initialize Substrate connection and get account address
|
663
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
772
664
|
|
773
|
-
# Use provided account address or
|
665
|
+
# Use provided account address or the one derived from initialization
|
774
666
|
if not account_address:
|
775
|
-
if
|
776
|
-
account_address =
|
777
|
-
print(f"Using account address: {account_address}")
|
667
|
+
if derived_address:
|
668
|
+
account_address = derived_address
|
778
669
|
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}")
|
670
|
+
raise ValueError("No account address available")
|
784
671
|
|
785
672
|
# Query the blockchain for account balance
|
786
|
-
print(f"Querying balance for account: {account_address}")
|
787
673
|
result = self._substrate.query(
|
788
674
|
module="System",
|
789
675
|
storage_function="Account",
|
@@ -793,7 +679,6 @@ class SubstrateClient:
|
|
793
679
|
# If account exists, extract the balance information
|
794
680
|
if result.value:
|
795
681
|
data = result.value
|
796
|
-
print(data)
|
797
682
|
# Extract balance components
|
798
683
|
free_balance = data.get("data", {}).get("free", 0)
|
799
684
|
reserved_balance = data.get("data", {}).get("reserved", 0)
|
@@ -821,7 +706,6 @@ class SubstrateClient:
|
|
821
706
|
},
|
822
707
|
}
|
823
708
|
else:
|
824
|
-
print(f"No account data found for: {account_address}")
|
825
709
|
return {
|
826
710
|
"free": 0.0,
|
827
711
|
"reserved": 0.0,
|
@@ -831,9 +715,7 @@ class SubstrateClient:
|
|
831
715
|
}
|
832
716
|
|
833
717
|
except Exception as e:
|
834
|
-
|
835
|
-
print(error_msg)
|
836
|
-
raise ValueError(error_msg)
|
718
|
+
raise ValueError(f"Error querying account balance: {str(e)}")
|
837
719
|
|
838
720
|
async def watch_account_balance(
|
839
721
|
self, account_address: Optional[str] = None, interval: int = 5
|
@@ -858,10 +740,6 @@ class SubstrateClient:
|
|
858
740
|
raise ValueError("No account address available")
|
859
741
|
account_address = self._keypair.ss58_address
|
860
742
|
|
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
743
|
# Keep track of previous balance to show changes
|
866
744
|
previous_balance = None
|
867
745
|
|
@@ -942,27 +820,15 @@ class SubstrateClient:
|
|
942
820
|
ValueError: If account has no credits
|
943
821
|
"""
|
944
822
|
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}")
|
823
|
+
# Initialize Substrate connection and get account address
|
824
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
954
825
|
|
955
|
-
# Use provided account address or
|
826
|
+
# Use provided account address or the one derived from initialization
|
956
827
|
if not account_address:
|
957
|
-
if
|
958
|
-
account_address =
|
959
|
-
print(f"Using account address: {account_address}")
|
828
|
+
if derived_address:
|
829
|
+
account_address = derived_address
|
960
830
|
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}")
|
831
|
+
raise ValueError("No account address available")
|
966
832
|
|
967
833
|
# Query the blockchain for free credits
|
968
834
|
print(f"Querying free credits for account: {account_address}")
|
@@ -1006,30 +872,17 @@ class SubstrateClient:
|
|
1006
872
|
ValueError: If query fails or no files found
|
1007
873
|
"""
|
1008
874
|
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}")
|
875
|
+
# Initialize Substrate connection and get account address
|
876
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
1018
877
|
|
1019
|
-
# Use provided account address or
|
878
|
+
# Use provided account address or the one derived from initialization
|
1020
879
|
if not account_address:
|
1021
|
-
if
|
1022
|
-
account_address =
|
1023
|
-
print(f"Using account address: {account_address}")
|
880
|
+
if derived_address:
|
881
|
+
account_address = derived_address
|
1024
882
|
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}")
|
883
|
+
raise ValueError("No account address available")
|
1030
884
|
|
1031
885
|
# Query the blockchain for user file hashes
|
1032
|
-
print(f"Querying file hashes for account: {account_address}")
|
1033
886
|
result = self._substrate.query(
|
1034
887
|
module="Marketplace",
|
1035
888
|
storage_function="UserFileHashes",
|
@@ -1040,16 +893,12 @@ class SubstrateClient:
|
|
1040
893
|
if result.value:
|
1041
894
|
# The result is already a list of bytes, convert each to string
|
1042
895
|
file_hashes = [cid.hex() for cid in result.value]
|
1043
|
-
print(f"Found {len(file_hashes)} files stored by this account")
|
1044
896
|
return file_hashes
|
1045
897
|
else:
|
1046
|
-
print(f"No files found for account: {account_address}")
|
1047
898
|
return []
|
1048
899
|
|
1049
900
|
except Exception as e:
|
1050
|
-
|
1051
|
-
print(error_msg)
|
1052
|
-
raise ValueError(error_msg)
|
901
|
+
raise ValueError(f"Error querying user file hashes: {str(e)}")
|
1053
902
|
|
1054
903
|
async def get_user_files(
|
1055
904
|
self,
|
@@ -1074,7 +923,6 @@ class SubstrateClient:
|
|
1074
923
|
"file_hash": str, # The IPFS CID of the file
|
1075
924
|
"file_name": str, # The name of the file
|
1076
925
|
"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
926
|
"miner_count": int, # Total number of miners
|
1079
927
|
"file_size": int, # Size of the file in bytes
|
1080
928
|
"size_formatted": str # Human-readable file size
|
@@ -1108,90 +956,37 @@ class SubstrateClient:
|
|
1108
956
|
ValueError: If query fails or profile cannot be retrieved
|
1109
957
|
"""
|
1110
958
|
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}")
|
959
|
+
# Initialize Substrate connection and get account address
|
960
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
1120
961
|
|
1121
|
-
# Use provided account address or
|
962
|
+
# Use provided account address or the one derived from initialization
|
1122
963
|
if not account_address:
|
1123
|
-
if
|
1124
|
-
account_address =
|
1125
|
-
print(f"Using account address: {account_address}")
|
964
|
+
if derived_address:
|
965
|
+
account_address = derived_address
|
1126
966
|
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}")
|
967
|
+
raise ValueError("No account address available")
|
1132
968
|
|
1133
969
|
# Query the blockchain for the user profile CID
|
1134
|
-
|
1135
|
-
result = self._substrate.query(
|
970
|
+
profile_hex_cid = self._substrate.query(
|
1136
971
|
module="IpfsPallet",
|
1137
972
|
storage_function="UserProfile",
|
1138
973
|
params=[account_address],
|
1139
|
-
)
|
974
|
+
).value
|
1140
975
|
|
1141
|
-
|
1142
|
-
if not result.value:
|
1143
|
-
print(f"No profile found for account: {account_address}")
|
976
|
+
if not profile_hex_cid:
|
1144
977
|
return []
|
1145
978
|
|
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}")
|
979
|
+
profile_cid = self._hex_to_ipfs_cid(profile_hex_cid)
|
1163
980
|
|
1164
981
|
# Fetch the profile JSON from IPFS
|
1165
982
|
# Defer import to avoid circular imports
|
1166
983
|
from hippius_sdk.ipfs import IPFSClient
|
1167
984
|
|
1168
985
|
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
|
986
|
+
profile_content = (await ipfs_client.cat(profile_cid))["content"]
|
987
|
+
files = json.loads(profile_content)
|
1194
988
|
processed_files = []
|
989
|
+
|
1195
990
|
for file in files:
|
1196
991
|
# Make sure file is a dictionary
|
1197
992
|
if not isinstance(file, dict):
|
@@ -1208,37 +1003,10 @@ class SubstrateClient:
|
|
1208
1003
|
try:
|
1209
1004
|
# Convert array of numbers to bytes, then to a string
|
1210
1005
|
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
|
-
)
|
1006
|
+
except Exception as e:
|
1007
|
+
print(e)
|
1221
1008
|
|
1222
1009
|
# 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
1010
|
# Try different field names for the size
|
1243
1011
|
file_size = (
|
1244
1012
|
file.get("size")
|
@@ -1249,25 +1017,18 @@ class SubstrateClient:
|
|
1249
1017
|
)
|
1250
1018
|
|
1251
1019
|
processed_file = {
|
1020
|
+
"cid": self._hex_to_ipfs_cid(file_hash),
|
1252
1021
|
"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
|
1022
|
+
"file_name": file.get("file_name"),
|
1023
|
+
"miner_ids": file.get("miner_ids", []),
|
1258
1024
|
"miner_count": len(file.get("miner_ids", [])), # Count the miners
|
1259
1025
|
"file_size": file_size or 0,
|
1026
|
+
"selected_validator": file["selected_validator"],
|
1260
1027
|
}
|
1261
1028
|
|
1262
1029
|
# Add formatted file size if available
|
1263
1030
|
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"
|
1031
|
+
processed_file["size_formatted"] = format_size(file_size)
|
1271
1032
|
else:
|
1272
1033
|
processed_file["size_formatted"] = "Unknown"
|
1273
1034
|
|
@@ -1276,9 +1037,7 @@ class SubstrateClient:
|
|
1276
1037
|
return processed_files
|
1277
1038
|
|
1278
1039
|
except Exception as e:
|
1279
|
-
|
1280
|
-
print(error_msg)
|
1281
|
-
raise ValueError(error_msg)
|
1040
|
+
raise ValueError(f"Error retrieving user files from profile: {str(e)}")
|
1282
1041
|
|
1283
1042
|
def get_pinning_status(
|
1284
1043
|
self, account_address: Optional[str] = None
|
@@ -1311,165 +1070,48 @@ class SubstrateClient:
|
|
1311
1070
|
ConnectionError: If connection to Substrate fails
|
1312
1071
|
ValueError: If query fails or no requests found
|
1313
1072
|
"""
|
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}")
|
1073
|
+
# Initialize Substrate connection and get account address
|
1074
|
+
substrate, derived_address = initialize_substrate_connection(self)
|
1324
1075
|
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1076
|
+
# Use provided account address or the one derived from initialization
|
1077
|
+
if not account_address:
|
1078
|
+
if derived_address:
|
1079
|
+
account_address = derived_address
|
1080
|
+
else:
|
1081
|
+
raise ValueError("No account address available")
|
1082
|
+
|
1083
|
+
# Query the blockchain for storage requests
|
1084
|
+
print(f"Querying storage requests for account: {account_address}")
|
1085
|
+
storage_requests = []
|
1086
|
+
|
1087
|
+
# First, try with query_map which is more suitable for iterating over collections
|
1088
|
+
result = self._substrate.query_map(
|
1089
|
+
module="IpfsPallet",
|
1090
|
+
storage_function="UserStorageRequests",
|
1091
|
+
params=[account_address],
|
1092
|
+
)
|
1093
|
+
|
1094
|
+
results_list = list(result)
|
1095
|
+
for key, substrate_result in results_list:
|
1096
|
+
# Extract file hash from key if possible
|
1097
|
+
file_hash_hex = None
|
1098
|
+
if key is not None:
|
1099
|
+
if hasattr(key, "hex"):
|
1100
|
+
file_hash_hex = key.hex()
|
1101
|
+
elif isinstance(key, bytes):
|
1102
|
+
file_hash_hex = key.hex()
|
1103
|
+
elif isinstance(key, str) and key.startswith("0x"):
|
1104
|
+
file_hash_hex = key[2:]
|
1330
1105
|
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}")
|
1106
|
+
file_hash_hex = str(key)
|
1336
1107
|
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
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
|
-
)
|
1108
|
+
if file_hash_hex:
|
1109
|
+
file_cid = self._hex_to_ipfs_cid(file_hash_hex)
|
1110
|
+
substrate_result.value["cid"] = file_cid
|
1359
1111
|
|
1360
|
-
|
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 []
|
1112
|
+
storage_requests.append(substrate_result.value)
|
1383
1113
|
|
1384
|
-
|
1385
|
-
|
1386
|
-
for i, (key, value) in enumerate(results_list):
|
1387
|
-
try:
|
1388
|
-
# For debugging, print raw data
|
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)
|
1462
|
-
|
1463
|
-
except Exception as e:
|
1464
|
-
print(f"Error processing request entry {i+1}: {e}")
|
1465
|
-
|
1466
|
-
print(f"Successfully processed {len(storage_requests)} storage requests")
|
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)
|
1114
|
+
return storage_requests
|
1473
1115
|
|
1474
1116
|
def _hex_to_ipfs_cid(self, hex_string: str) -> str:
|
1475
1117
|
"""
|
@@ -1482,3 +1124,42 @@ class SubstrateClient:
|
|
1482
1124
|
str: Regular IPFS CID
|
1483
1125
|
"""
|
1484
1126
|
return hex_to_ipfs_cid(hex_string)
|
1127
|
+
|
1128
|
+
async def cancel_storage_request(self, cid: str) -> str:
|
1129
|
+
"""
|
1130
|
+
Cancel a storage request by CID from the Hippius blockchain.
|
1131
|
+
|
1132
|
+
Args:
|
1133
|
+
cid: Content Identifier (CID) of the file to cancel
|
1134
|
+
|
1135
|
+
Returns:
|
1136
|
+
str: Transaction hash
|
1137
|
+
"""
|
1138
|
+
if not self._ensure_keypair():
|
1139
|
+
raise ValueError("Seed phrase must be set before making transactions")
|
1140
|
+
|
1141
|
+
substrate, _ = initialize_substrate_connection(self)
|
1142
|
+
|
1143
|
+
call = self._substrate.compose_call(
|
1144
|
+
call_module="Marketplace",
|
1145
|
+
call_function="storage_unpin_request",
|
1146
|
+
call_params={
|
1147
|
+
"file_hash": cid,
|
1148
|
+
},
|
1149
|
+
)
|
1150
|
+
|
1151
|
+
# Get payment info and show estimated transaction fee
|
1152
|
+
payment_info = self._substrate.get_payment_info(
|
1153
|
+
call=call, keypair=self._keypair
|
1154
|
+
)
|
1155
|
+
print(f"Payment info: {json.dumps(payment_info, indent=2)}")
|
1156
|
+
fee = payment_info.get("partialFee", 0)
|
1157
|
+
fee_tokens = fee / 10**12 if fee > 0 else 0
|
1158
|
+
print(f"Estimated transaction fee: {fee} ({fee_tokens:.10f} tokens)")
|
1159
|
+
|
1160
|
+
extrinsic = self._substrate.create_signed_extrinsic(
|
1161
|
+
call=call, keypair=self._keypair
|
1162
|
+
)
|
1163
|
+
response = self._substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
|
1164
|
+
print(f"Transaction hash: {response.extrinsic_hash}")
|
1165
|
+
return response.extrinsic_hash
|