hippius 0.1.14__py3-none-any.whl → 0.2.1__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.14.dist-info → hippius-0.2.1.dist-info}/METADATA +3 -2
- hippius-0.2.1.dist-info/RECORD +12 -0
- hippius_sdk/__init__.py +5 -1
- hippius_sdk/cli.py +810 -89
- hippius_sdk/client.py +20 -22
- hippius_sdk/config.py +19 -26
- hippius_sdk/ipfs.py +186 -428
- hippius_sdk/ipfs_core.py +216 -0
- hippius_sdk/substrate.py +699 -67
- hippius_sdk/utils.py +152 -0
- hippius-0.1.14.dist-info/RECORD +0 -10
- {hippius-0.1.14.dist-info → hippius-0.2.1.dist-info}/WHEEL +0 -0
- {hippius-0.1.14.dist-info → hippius-0.2.1.dist-info}/entry_points.txt +0 -0
hippius_sdk/substrate.py
CHANGED
@@ -1,24 +1,25 @@
|
|
1
|
-
|
2
|
-
Substrate operations for the Hippius SDK.
|
3
|
-
|
4
|
-
Note: This functionality is coming soon and not implemented yet.
|
5
|
-
"""
|
6
|
-
|
1
|
+
import datetime
|
7
2
|
import json
|
8
3
|
import os
|
4
|
+
import tempfile
|
5
|
+
import time
|
9
6
|
import uuid
|
10
7
|
from typing import Any, Dict, List, Optional, Union
|
11
8
|
|
12
9
|
from dotenv import load_dotenv
|
10
|
+
from mnemonic import Mnemonic
|
13
11
|
from substrateinterface import Keypair, SubstrateInterface
|
14
12
|
|
15
13
|
from hippius_sdk.config import (
|
16
14
|
get_account_address,
|
17
15
|
get_active_account,
|
16
|
+
get_all_config,
|
18
17
|
get_config_value,
|
19
18
|
get_seed_phrase,
|
19
|
+
set_active_account,
|
20
20
|
set_seed_phrase,
|
21
21
|
)
|
22
|
+
from hippius_sdk.utils import hex_to_ipfs_cid
|
22
23
|
|
23
24
|
# Load environment variables
|
24
25
|
load_dotenv()
|
@@ -179,6 +180,300 @@ class SubstrateClient:
|
|
179
180
|
print(f"Warning: Could not get seed phrase from config: {e}")
|
180
181
|
return False
|
181
182
|
|
183
|
+
def generate_mnemonic(self) -> str:
|
184
|
+
"""
|
185
|
+
Generate a new random 12-word mnemonic phrase.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
str: A 12-word mnemonic seed phrase
|
189
|
+
"""
|
190
|
+
try:
|
191
|
+
mnemo = Mnemonic("english")
|
192
|
+
return mnemo.generate(strength=128) # 128 bits = 12 words
|
193
|
+
except Exception as e:
|
194
|
+
raise ValueError(f"Error generating mnemonic: {e}")
|
195
|
+
|
196
|
+
def create_account(
|
197
|
+
self, name: str, encode: bool = False, password: Optional[str] = None
|
198
|
+
) -> Dict[str, Any]:
|
199
|
+
"""
|
200
|
+
Create a new account with a generated seed phrase.
|
201
|
+
|
202
|
+
Args:
|
203
|
+
name: Name for the new account
|
204
|
+
encode: Whether to encrypt the seed phrase with a password
|
205
|
+
password: Optional password for encryption (will prompt if not provided and encode=True)
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
Dict[str, Any]: Dictionary with new account details
|
209
|
+
"""
|
210
|
+
# Check if account name already exists
|
211
|
+
config = get_all_config()
|
212
|
+
if name in config["substrate"].get("accounts", {}):
|
213
|
+
raise ValueError(f"Account with name '{name}' already exists")
|
214
|
+
|
215
|
+
# Generate a new mnemonic seed phrase
|
216
|
+
mnemonic = self.generate_mnemonic()
|
217
|
+
|
218
|
+
# Create a keypair from the mnemonic
|
219
|
+
keypair = Keypair.create_from_mnemonic(mnemonic)
|
220
|
+
ss58_address = keypair.ss58_address
|
221
|
+
|
222
|
+
# Save the seed phrase to configuration
|
223
|
+
if encode:
|
224
|
+
result = set_seed_phrase(
|
225
|
+
mnemonic, encode=True, password=password, account_name=name
|
226
|
+
)
|
227
|
+
else:
|
228
|
+
result = set_seed_phrase(mnemonic, encode=False, account_name=name)
|
229
|
+
|
230
|
+
if not result:
|
231
|
+
raise RuntimeError("Failed to save account to configuration")
|
232
|
+
|
233
|
+
# Set this as the active account
|
234
|
+
set_active_account(name)
|
235
|
+
|
236
|
+
# Update the client's state to use this account
|
237
|
+
self._account_name = name
|
238
|
+
self._account_address = ss58_address
|
239
|
+
self._seed_phrase = mnemonic
|
240
|
+
self._keypair = keypair
|
241
|
+
self._read_only = False
|
242
|
+
|
243
|
+
# Return the new account details
|
244
|
+
return {
|
245
|
+
"name": name,
|
246
|
+
"address": ss58_address,
|
247
|
+
"mnemonic": mnemonic,
|
248
|
+
"is_active": True,
|
249
|
+
"creation_date": datetime.datetime.now().isoformat(),
|
250
|
+
}
|
251
|
+
|
252
|
+
def export_account(
|
253
|
+
self, account_name: Optional[str] = None, file_path: Optional[str] = None
|
254
|
+
) -> str:
|
255
|
+
"""
|
256
|
+
Export an account to a JSON file.
|
257
|
+
|
258
|
+
Args:
|
259
|
+
account_name: Name of the account to export (uses active account if None)
|
260
|
+
file_path: Path to save the exported account file (auto-generated if None)
|
261
|
+
|
262
|
+
Returns:
|
263
|
+
str: Path to the exported account file
|
264
|
+
"""
|
265
|
+
# Determine which account to export
|
266
|
+
name_to_use = account_name or self._account_name or get_active_account()
|
267
|
+
if not name_to_use:
|
268
|
+
raise ValueError("No account specified and no active account")
|
269
|
+
|
270
|
+
# Get the seed phrase and address
|
271
|
+
seed_phrase = get_seed_phrase(account_name=name_to_use)
|
272
|
+
if not seed_phrase:
|
273
|
+
raise ValueError(
|
274
|
+
f"Could not retrieve seed phrase for account '{name_to_use}'"
|
275
|
+
)
|
276
|
+
|
277
|
+
address = get_account_address(name_to_use)
|
278
|
+
if not address:
|
279
|
+
# Generate the address from the seed phrase
|
280
|
+
keypair = Keypair.create_from_mnemonic(seed_phrase)
|
281
|
+
address = keypair.ss58_address
|
282
|
+
|
283
|
+
# Create the export data structure
|
284
|
+
export_data = {
|
285
|
+
"name": name_to_use,
|
286
|
+
"address": address,
|
287
|
+
"mnemonic": seed_phrase,
|
288
|
+
"meta": {
|
289
|
+
"exported_at": datetime.datetime.now().isoformat(),
|
290
|
+
"description": "Hippius SDK exported account",
|
291
|
+
},
|
292
|
+
}
|
293
|
+
|
294
|
+
# Determine the file path if not provided
|
295
|
+
if not file_path:
|
296
|
+
file_path = f"{name_to_use}_account_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
297
|
+
|
298
|
+
# Write the export file
|
299
|
+
try:
|
300
|
+
with open(file_path, "w") as f:
|
301
|
+
json.dump(export_data, f, indent=2)
|
302
|
+
print(f"Account '{name_to_use}' exported to {file_path}")
|
303
|
+
return file_path
|
304
|
+
except Exception as e:
|
305
|
+
raise ValueError(f"Failed to export account: {e}")
|
306
|
+
|
307
|
+
def import_account(
|
308
|
+
self, file_path: str, password: Optional[str] = None
|
309
|
+
) -> Dict[str, Any]:
|
310
|
+
"""
|
311
|
+
Import an account from a JSON file.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
file_path: Path to the account export file
|
315
|
+
password: Optional password to use for encrypting the imported seed phrase
|
316
|
+
|
317
|
+
Returns:
|
318
|
+
Dict[str, Any]: Dictionary with imported account details
|
319
|
+
"""
|
320
|
+
try:
|
321
|
+
# Read the export file
|
322
|
+
with open(file_path, "r") as f:
|
323
|
+
import_data = json.load(f)
|
324
|
+
|
325
|
+
# Validate the import data structure
|
326
|
+
required_fields = ["name", "address", "mnemonic"]
|
327
|
+
for field in required_fields:
|
328
|
+
if field not in import_data:
|
329
|
+
raise ValueError(
|
330
|
+
f"Invalid account file format: missing '{field}' field"
|
331
|
+
)
|
332
|
+
|
333
|
+
# Extract account details
|
334
|
+
name = import_data["name"]
|
335
|
+
address = import_data["address"]
|
336
|
+
mnemonic = import_data["mnemonic"]
|
337
|
+
|
338
|
+
# Check if the account name already exists
|
339
|
+
config = get_all_config()
|
340
|
+
if name in config["substrate"].get("accounts", {}):
|
341
|
+
# Modify the name to avoid conflicts
|
342
|
+
original_name = name
|
343
|
+
name = f"{name}_imported_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
344
|
+
print(
|
345
|
+
f"Account name '{original_name}' already exists, using '{name}' instead"
|
346
|
+
)
|
347
|
+
|
348
|
+
# Save the account to configuration
|
349
|
+
if password:
|
350
|
+
# Encrypt the seed phrase with the provided password
|
351
|
+
result = set_seed_phrase(
|
352
|
+
mnemonic, encode=True, password=password, account_name=name
|
353
|
+
)
|
354
|
+
else:
|
355
|
+
# Store the seed phrase in plain text
|
356
|
+
result = set_seed_phrase(mnemonic, encode=False, account_name=name)
|
357
|
+
|
358
|
+
if not result:
|
359
|
+
raise RuntimeError("Failed to save imported account to configuration")
|
360
|
+
|
361
|
+
# Set this as the active account
|
362
|
+
set_active_account(name)
|
363
|
+
|
364
|
+
# Update the client's state to use this account
|
365
|
+
self._account_name = name
|
366
|
+
self._account_address = address
|
367
|
+
self._seed_phrase = mnemonic
|
368
|
+
self._keypair = Keypair.create_from_mnemonic(mnemonic)
|
369
|
+
self._read_only = False
|
370
|
+
|
371
|
+
# Return the imported account details
|
372
|
+
return {
|
373
|
+
"name": name,
|
374
|
+
"address": address,
|
375
|
+
"is_active": True,
|
376
|
+
"imported_at": datetime.datetime.now().isoformat(),
|
377
|
+
"original_name": import_data.get("name"),
|
378
|
+
}
|
379
|
+
except Exception as e:
|
380
|
+
raise ValueError(f"Failed to import account: {e}")
|
381
|
+
|
382
|
+
async def get_account_info(
|
383
|
+
self, account_name: Optional[str] = None, include_history: bool = False
|
384
|
+
) -> Dict[str, Any]:
|
385
|
+
"""
|
386
|
+
Get detailed information about an account.
|
387
|
+
|
388
|
+
Args:
|
389
|
+
account_name: Name of the account to get info for (uses active account if None)
|
390
|
+
include_history: Whether to include usage history in the results
|
391
|
+
|
392
|
+
Returns:
|
393
|
+
Dict[str, Any]: Detailed account information
|
394
|
+
"""
|
395
|
+
# Determine which account to get info for
|
396
|
+
name_to_use = account_name or self._account_name or get_active_account()
|
397
|
+
if not name_to_use:
|
398
|
+
raise ValueError("No account specified and no active account")
|
399
|
+
|
400
|
+
# Get the configuration to extract account data
|
401
|
+
config = get_all_config()
|
402
|
+
|
403
|
+
# Check if the account exists
|
404
|
+
if name_to_use not in config["substrate"].get("accounts", {}):
|
405
|
+
raise ValueError(f"Account '{name_to_use}' not found")
|
406
|
+
|
407
|
+
# Get account data from config
|
408
|
+
account_data = config["substrate"]["accounts"][name_to_use]
|
409
|
+
is_active = name_to_use == config["substrate"].get("active_account")
|
410
|
+
is_encoded = account_data.get("seed_phrase_encoded", False)
|
411
|
+
address = account_data.get("ss58_address")
|
412
|
+
|
413
|
+
# Create the account info object
|
414
|
+
account_info = {
|
415
|
+
"name": name_to_use,
|
416
|
+
"address": address,
|
417
|
+
"is_active": is_active,
|
418
|
+
"seed_phrase_encrypted": is_encoded,
|
419
|
+
}
|
420
|
+
|
421
|
+
# Query storage statistics for this account
|
422
|
+
try:
|
423
|
+
# Get files stored by this account - use await since this is an async method
|
424
|
+
files = await self.get_user_files_from_profile(address)
|
425
|
+
|
426
|
+
# Calculate storage statistics
|
427
|
+
total_files = len(files)
|
428
|
+
total_size_bytes = sum(file.get("file_size", 0) for file in files)
|
429
|
+
|
430
|
+
# Add storage stats to account info
|
431
|
+
account_info["storage_stats"] = {
|
432
|
+
"files": total_files,
|
433
|
+
"bytes_used": total_size_bytes,
|
434
|
+
"size_formatted": self._format_size(total_size_bytes)
|
435
|
+
if total_size_bytes
|
436
|
+
else "0 B",
|
437
|
+
}
|
438
|
+
|
439
|
+
# Include file list if requested
|
440
|
+
if include_history:
|
441
|
+
account_info["files"] = files
|
442
|
+
|
443
|
+
# Try to get account balance
|
444
|
+
try:
|
445
|
+
account_info["balance"] = await self.get_account_balance(address)
|
446
|
+
except Exception as e:
|
447
|
+
# Ignore balance errors, it's optional information
|
448
|
+
print(f"Could not fetch balance: {e}")
|
449
|
+
pass
|
450
|
+
|
451
|
+
# Try to get free credits
|
452
|
+
try:
|
453
|
+
account_info["free_credits"] = await self.get_free_credits(address)
|
454
|
+
except Exception as e:
|
455
|
+
# Ignore credits errors, it's optional information
|
456
|
+
print(f"Could not fetch free credits: {e}")
|
457
|
+
pass
|
458
|
+
except Exception as e:
|
459
|
+
# Add a note about the error but don't fail the whole operation
|
460
|
+
account_info["storage_stats"] = {
|
461
|
+
"error": f"Could not fetch storage statistics: {str(e)}"
|
462
|
+
}
|
463
|
+
|
464
|
+
return account_info
|
465
|
+
|
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
|
+
|
182
477
|
def set_seed_phrase(self, seed_phrase: str) -> None:
|
183
478
|
"""
|
184
479
|
Set or update the seed phrase used for signing transactions.
|
@@ -202,7 +497,7 @@ class SubstrateClient:
|
|
202
497
|
print(f"Warning: Could not create keypair from seed phrase: {e}")
|
203
498
|
print(f"Keypair will be created when needed")
|
204
499
|
|
205
|
-
def storage_request(
|
500
|
+
async def storage_request(
|
206
501
|
self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
|
207
502
|
) -> str:
|
208
503
|
"""
|
@@ -276,8 +571,7 @@ class SubstrateClient:
|
|
276
571
|
print(f"Created file list with {len(file_list)} entries")
|
277
572
|
|
278
573
|
# Step 2: Upload the JSON file to IPFS
|
279
|
-
import
|
280
|
-
|
574
|
+
# Defer import to avoid circular imports
|
281
575
|
from hippius_sdk.ipfs import IPFSClient
|
282
576
|
|
283
577
|
ipfs_client = IPFSClient()
|
@@ -291,7 +585,7 @@ class SubstrateClient:
|
|
291
585
|
|
292
586
|
try:
|
293
587
|
print("Uploading file list to IPFS...")
|
294
|
-
upload_result = ipfs_client.upload_file(temp_file_path)
|
588
|
+
upload_result = await ipfs_client.upload_file(temp_file_path)
|
295
589
|
files_list_cid = upload_result["cid"]
|
296
590
|
print(f"File list uploaded to IPFS with CID: {files_list_cid}")
|
297
591
|
finally:
|
@@ -313,19 +607,42 @@ class SubstrateClient:
|
|
313
607
|
|
314
608
|
# Create the call to the marketplace
|
315
609
|
print(f"Call parameters: {json.dumps(call_params, indent=2)}")
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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...")
|
619
|
+
|
620
|
+
# Try with IpfsPallet.storeFile as an alternative
|
621
|
+
alt_call_params = {
|
622
|
+
"fileHash": files_list_cid,
|
623
|
+
"fileName": f"files_list_{uuid.uuid4()}", # Generate a unique ID
|
624
|
+
}
|
625
|
+
call = self._substrate.compose_call(
|
626
|
+
call_module="IpfsPallet",
|
627
|
+
call_function="storeFile",
|
628
|
+
call_params=alt_call_params,
|
629
|
+
)
|
321
630
|
|
322
631
|
# Get payment info to estimate the fee
|
323
632
|
payment_info = self._substrate.get_payment_info(
|
324
633
|
call=call, keypair=self._keypair
|
325
634
|
)
|
326
635
|
|
636
|
+
print(f"Payment info: {json.dumps(payment_info, indent=2)}")
|
637
|
+
|
638
|
+
# Convert partialFee from Substrate (10^18 units) to a more readable format
|
327
639
|
estimated_fee = payment_info.get("partialFee", 0)
|
328
|
-
|
640
|
+
estimated_fee_formatted = (
|
641
|
+
float(estimated_fee) / 1_000_000_000_000_000_000 if estimated_fee else 0
|
642
|
+
)
|
643
|
+
print(
|
644
|
+
f"Estimated transaction fee: {estimated_fee} ({estimated_fee_formatted:.10f} tokens)"
|
645
|
+
)
|
329
646
|
|
330
647
|
# Create a signed extrinsic
|
331
648
|
extrinsic = self._substrate.create_signed_extrinsic(
|
@@ -361,7 +678,7 @@ class SubstrateClient:
|
|
361
678
|
|
362
679
|
return "simulated-tx-hash"
|
363
680
|
|
364
|
-
def store_cid(
|
681
|
+
async def store_cid(
|
365
682
|
self, cid: str, filename: str = None, metadata: Optional[Dict[str, Any]] = None
|
366
683
|
) -> str:
|
367
684
|
"""
|
@@ -376,7 +693,7 @@ class SubstrateClient:
|
|
376
693
|
str: Transaction hash
|
377
694
|
"""
|
378
695
|
file_input = FileInput(file_hash=cid, file_name=filename or "unnamed_file")
|
379
|
-
return self.storage_request([file_input])
|
696
|
+
return await self.storage_request([file_input])
|
380
697
|
|
381
698
|
def get_cid_metadata(self, cid: str) -> Dict[str, Any]:
|
382
699
|
"""
|
@@ -430,7 +747,7 @@ class SubstrateClient:
|
|
430
747
|
"""
|
431
748
|
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
432
749
|
|
433
|
-
def get_account_balance(
|
750
|
+
async def get_account_balance(
|
434
751
|
self, account_address: Optional[str] = None
|
435
752
|
) -> Dict[str, float]:
|
436
753
|
"""
|
@@ -442,9 +759,174 @@ class SubstrateClient:
|
|
442
759
|
Returns:
|
443
760
|
Dict[str, float]: Account balances (free, reserved, total)
|
444
761
|
"""
|
445
|
-
|
762
|
+
try:
|
763
|
+
# Initialize Substrate connection if not already connected
|
764
|
+
if not hasattr(self, "_substrate") or self._substrate is None:
|
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}")
|
772
|
+
|
773
|
+
# Use provided account address or default to keypair/configured address
|
774
|
+
if not account_address:
|
775
|
+
if self._account_address:
|
776
|
+
account_address = self._account_address
|
777
|
+
print(f"Using account address: {account_address}")
|
778
|
+
else:
|
779
|
+
# Try to get the address from the keypair (requires seed phrase)
|
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}")
|
784
|
+
|
785
|
+
# Query the blockchain for account balance
|
786
|
+
print(f"Querying balance for account: {account_address}")
|
787
|
+
result = self._substrate.query(
|
788
|
+
module="System",
|
789
|
+
storage_function="Account",
|
790
|
+
params=[account_address],
|
791
|
+
)
|
792
|
+
|
793
|
+
# If account exists, extract the balance information
|
794
|
+
if result.value:
|
795
|
+
data = result.value
|
796
|
+
print(data)
|
797
|
+
# Extract balance components
|
798
|
+
free_balance = data.get("data", {}).get("free", 0)
|
799
|
+
reserved_balance = data.get("data", {}).get("reserved", 0)
|
800
|
+
frozen_balance = data.get("data", {}).get("frozen", 0)
|
801
|
+
|
802
|
+
# Convert from blockchain units to float (divide by 10^18)
|
803
|
+
divisor = 1_000_000_000_000_000_000 # 18 zeros for decimals
|
804
|
+
|
805
|
+
free = float(free_balance) / divisor
|
806
|
+
reserved = float(reserved_balance) / divisor
|
807
|
+
frozen = float(frozen_balance) / divisor
|
808
|
+
|
809
|
+
# Calculate total (free + reserved - frozen)
|
810
|
+
total = free + reserved - frozen
|
811
|
+
|
812
|
+
return {
|
813
|
+
"free": free,
|
814
|
+
"reserved": reserved,
|
815
|
+
"frozen": frozen,
|
816
|
+
"total": total,
|
817
|
+
"raw": {
|
818
|
+
"free": free_balance,
|
819
|
+
"reserved": reserved_balance,
|
820
|
+
"frozen": frozen_balance,
|
821
|
+
},
|
822
|
+
}
|
823
|
+
else:
|
824
|
+
print(f"No account data found for: {account_address}")
|
825
|
+
return {
|
826
|
+
"free": 0.0,
|
827
|
+
"reserved": 0.0,
|
828
|
+
"frozen": 0.0,
|
829
|
+
"total": 0.0,
|
830
|
+
"raw": {"free": 0, "reserved": 0, "frozen": 0},
|
831
|
+
}
|
446
832
|
|
447
|
-
|
833
|
+
except Exception as e:
|
834
|
+
error_msg = f"Error querying account balance: {str(e)}"
|
835
|
+
print(error_msg)
|
836
|
+
raise ValueError(error_msg)
|
837
|
+
|
838
|
+
async def watch_account_balance(
|
839
|
+
self, account_address: Optional[str] = None, interval: int = 5
|
840
|
+
) -> None:
|
841
|
+
"""
|
842
|
+
Watch account balance in real-time, updating at specified intervals.
|
843
|
+
|
844
|
+
The function runs until interrupted with Ctrl+C.
|
845
|
+
|
846
|
+
Args:
|
847
|
+
account_address: Substrate account address (uses keypair address if not specified)
|
848
|
+
interval: Polling interval in seconds (default: 5)
|
849
|
+
"""
|
850
|
+
try:
|
851
|
+
# Use provided account address or default to keypair/configured address
|
852
|
+
if not account_address:
|
853
|
+
if self._account_address:
|
854
|
+
account_address = self._account_address
|
855
|
+
else:
|
856
|
+
# Try to get the address from the keypair (requires seed phrase)
|
857
|
+
if not self._ensure_keypair():
|
858
|
+
raise ValueError("No account address available")
|
859
|
+
account_address = self._keypair.ss58_address
|
860
|
+
|
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
|
+
# Keep track of previous balance to show changes
|
866
|
+
previous_balance = None
|
867
|
+
|
868
|
+
try:
|
869
|
+
while True:
|
870
|
+
# Get current time for display
|
871
|
+
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
872
|
+
|
873
|
+
# Get current balance
|
874
|
+
try:
|
875
|
+
balance = await self.get_account_balance(account_address)
|
876
|
+
|
877
|
+
# Clear screen (ANSI escape sequence)
|
878
|
+
print("\033c", end="")
|
879
|
+
|
880
|
+
# Display header
|
881
|
+
print(f"Account Balance Watch for: {account_address}")
|
882
|
+
print(f"Last update: {current_time}")
|
883
|
+
print("-" * 80)
|
884
|
+
|
885
|
+
# Display current balance
|
886
|
+
print(f"Free: {balance['free']:.6f}")
|
887
|
+
print(f"Reserved: {balance['reserved']:.6f}")
|
888
|
+
print(f"Frozen: {balance['frozen']:.6f}")
|
889
|
+
print(f"Total: {balance['total']:.6f}")
|
890
|
+
|
891
|
+
# Show changes since last update if available
|
892
|
+
if previous_balance:
|
893
|
+
print("\nChanges since last update:")
|
894
|
+
free_change = balance["free"] - previous_balance["free"]
|
895
|
+
reserved_change = (
|
896
|
+
balance["reserved"] - previous_balance["reserved"]
|
897
|
+
)
|
898
|
+
total_change = balance["total"] - previous_balance["total"]
|
899
|
+
|
900
|
+
# Format changes with + or - sign
|
901
|
+
print(f"Free: {free_change:+.6f}")
|
902
|
+
print(f"Reserved: {reserved_change:+.6f}")
|
903
|
+
print(f"Total: {total_change:+.6f}")
|
904
|
+
|
905
|
+
# Store current balance for next comparison
|
906
|
+
previous_balance = balance
|
907
|
+
|
908
|
+
# Show instructions at the bottom
|
909
|
+
print(
|
910
|
+
"\nUpdating every",
|
911
|
+
interval,
|
912
|
+
"seconds. Press Ctrl+C to stop.",
|
913
|
+
)
|
914
|
+
|
915
|
+
except Exception as e:
|
916
|
+
# Show error but continue watching
|
917
|
+
print(f"Error: {e}")
|
918
|
+
print(f"Will try again in {interval} seconds...")
|
919
|
+
|
920
|
+
# Wait for next update
|
921
|
+
time.sleep(interval)
|
922
|
+
|
923
|
+
except KeyboardInterrupt:
|
924
|
+
print("\nBalance watch stopped.")
|
925
|
+
|
926
|
+
except Exception as e:
|
927
|
+
print(f"Error in watch_account_balance: {e}")
|
928
|
+
|
929
|
+
async def get_free_credits(self, account_address: Optional[str] = None) -> float:
|
448
930
|
"""
|
449
931
|
Get the free credits available for an account in the marketplace.
|
450
932
|
|
@@ -569,7 +1051,7 @@ class SubstrateClient:
|
|
569
1051
|
print(error_msg)
|
570
1052
|
raise ValueError(error_msg)
|
571
1053
|
|
572
|
-
def get_user_files(
|
1054
|
+
async def get_user_files(
|
573
1055
|
self,
|
574
1056
|
account_address: Optional[str] = None,
|
575
1057
|
truncate_miners: bool = True,
|
@@ -604,9 +1086,9 @@ class SubstrateClient:
|
|
604
1086
|
"""
|
605
1087
|
# For backward compatibility, this method now calls get_user_files_from_profile
|
606
1088
|
# with appropriate conversions
|
607
|
-
return self.get_user_files_from_profile(account_address)
|
1089
|
+
return await self.get_user_files_from_profile(account_address)
|
608
1090
|
|
609
|
-
def get_user_files_from_profile(
|
1091
|
+
async def get_user_files_from_profile(
|
610
1092
|
self,
|
611
1093
|
account_address: Optional[str] = None,
|
612
1094
|
) -> List[Dict[str, Any]]:
|
@@ -680,12 +1162,13 @@ class SubstrateClient:
|
|
680
1162
|
print(f"Decoded IPFS CID: {profile_cid}")
|
681
1163
|
|
682
1164
|
# Fetch the profile JSON from IPFS
|
1165
|
+
# Defer import to avoid circular imports
|
683
1166
|
from hippius_sdk.ipfs import IPFSClient
|
684
1167
|
|
685
1168
|
ipfs_client = IPFSClient()
|
686
1169
|
|
687
1170
|
print(f"Fetching user profile from IPFS: {profile_cid}")
|
688
|
-
profile_data = ipfs_client.cat(profile_cid)
|
1171
|
+
profile_data = await ipfs_client.cat(profile_cid)
|
689
1172
|
|
690
1173
|
# Parse the JSON content
|
691
1174
|
if not profile_data.get("is_text", False):
|
@@ -797,56 +1280,205 @@ class SubstrateClient:
|
|
797
1280
|
print(error_msg)
|
798
1281
|
raise ValueError(error_msg)
|
799
1282
|
|
800
|
-
def
|
1283
|
+
def get_pinning_status(
|
1284
|
+
self, account_address: Optional[str] = None
|
1285
|
+
) -> List[Dict[str, Any]]:
|
801
1286
|
"""
|
802
|
-
|
1287
|
+
Get the status of file pinning requests for an account.
|
1288
|
+
|
1289
|
+
This method queries the blockchain for all storage requests made by the user
|
1290
|
+
to check their pinning status.
|
803
1291
|
|
804
1292
|
Args:
|
805
|
-
|
1293
|
+
account_address: Substrate account address (uses keypair address if not specified)
|
1294
|
+
Format: 5HoreGVb17XhY3wanDvzoAWS7yHYbc5uMteXqRNTiZ6Txkqq
|
806
1295
|
|
807
1296
|
Returns:
|
808
|
-
str:
|
1297
|
+
List[Dict[str, Any]]: List of storage requests with their status information:
|
1298
|
+
{
|
1299
|
+
"cid": str, # The IPFS CID of the file
|
1300
|
+
"file_name": str, # The name of the file
|
1301
|
+
"total_replicas": int, # Total number of replicas requested
|
1302
|
+
"owner": str, # Owner's address
|
1303
|
+
"created_at": int, # Block number when request was created
|
1304
|
+
"last_charged_at": int, # Block number when last charged
|
1305
|
+
"miner_ids": List[str], # List of miners assigned to pin the file
|
1306
|
+
"selected_validator": str, # Selected validator address
|
1307
|
+
"is_assigned": bool, # Whether request has been assigned to miners
|
1308
|
+
}
|
1309
|
+
|
1310
|
+
Raises:
|
1311
|
+
ConnectionError: If connection to Substrate fails
|
1312
|
+
ValueError: If query fails or no requests found
|
809
1313
|
"""
|
810
|
-
# First, try to decode as ASCII if it's a hex representation of ASCII characters
|
811
1314
|
try:
|
812
|
-
if
|
813
|
-
|
1315
|
+
# Initialize Substrate connection if not already connected
|
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}")
|
1324
|
+
|
1325
|
+
# Use provided account address or default to keypair/configured address
|
1326
|
+
if not account_address:
|
1327
|
+
if self._account_address:
|
1328
|
+
account_address = self._account_address
|
1329
|
+
print(f"Using account address: {account_address}")
|
1330
|
+
else:
|
1331
|
+
# Try to get the address from the keypair (requires seed phrase)
|
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
|
+
)
|
814
1359
|
|
815
|
-
|
816
|
-
|
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 []
|
817
1383
|
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
1384
|
+
print(f"Found {len(results_list)} storage request entries")
|
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
|
824
1468
|
|
825
|
-
# Try to decode as a binary CID
|
826
|
-
try:
|
827
|
-
import base58
|
828
|
-
|
829
|
-
if hex_string.startswith("0x"):
|
830
|
-
hex_string = hex_string[2:]
|
831
|
-
|
832
|
-
binary_data = bytes.fromhex(hex_string)
|
833
|
-
|
834
|
-
# Check if it matches CIDv0 pattern (starts with 0x12, 0x20)
|
835
|
-
if (
|
836
|
-
len(binary_data) > 2
|
837
|
-
and binary_data[0] == 0x12
|
838
|
-
and binary_data[1] == 0x20
|
839
|
-
):
|
840
|
-
# CIDv0 (Qm...)
|
841
|
-
return base58.b58encode(binary_data).decode("utf-8")
|
842
|
-
|
843
|
-
# If it doesn't match CIDv0, for CIDv1 just return the hex without 0x prefix
|
844
|
-
# since adding 0x breaks IPFS gateway URLs
|
845
|
-
return hex_string
|
846
|
-
except ImportError:
|
847
|
-
# If base58 is not available
|
848
|
-
print("Warning: base58 module not available for proper CID conversion")
|
849
|
-
return hex_string
|
850
1469
|
except Exception as e:
|
851
|
-
|
852
|
-
|
1470
|
+
error_msg = f"Error querying storage requests: {str(e)}"
|
1471
|
+
print(error_msg)
|
1472
|
+
raise ValueError(error_msg)
|
1473
|
+
|
1474
|
+
def _hex_to_ipfs_cid(self, hex_string: str) -> str:
|
1475
|
+
"""
|
1476
|
+
Convert a hex-encoded IPFS CID to a regular IPFS CID.
|
1477
|
+
|
1478
|
+
Args:
|
1479
|
+
hex_string: Hex string representation of an IPFS CID
|
1480
|
+
|
1481
|
+
Returns:
|
1482
|
+
str: Regular IPFS CID
|
1483
|
+
"""
|
1484
|
+
return hex_to_ipfs_cid(hex_string)
|