hippius 0.2.0__py3-none-any.whl → 0.2.2__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_sdk/substrate.py CHANGED
@@ -1,23 +1,22 @@
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
9
4
  import tempfile
5
+ import time
10
6
  import uuid
11
- from typing import Any, Dict, List, Optional, Type, Union
7
+ from typing import Any, Dict, List, Optional, Union
12
8
 
13
9
  from dotenv import load_dotenv
10
+ from mnemonic import Mnemonic
14
11
  from substrateinterface import Keypair, SubstrateInterface
15
12
 
16
13
  from hippius_sdk.config import (
17
14
  get_account_address,
18
15
  get_active_account,
16
+ get_all_config,
19
17
  get_config_value,
20
18
  get_seed_phrase,
19
+ set_active_account,
21
20
  set_seed_phrase,
22
21
  )
23
22
  from hippius_sdk.utils import hex_to_ipfs_cid
@@ -181,6 +180,300 @@ class SubstrateClient:
181
180
  print(f"Warning: Could not get seed phrase from config: {e}")
182
181
  return False
183
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
+
184
477
  def set_seed_phrase(self, seed_phrase: str) -> None:
185
478
  """
186
479
  Set or update the seed phrase used for signing transactions.
@@ -454,7 +747,7 @@ class SubstrateClient:
454
747
  """
455
748
  raise NotImplementedError("Substrate functionality is not implemented yet.")
456
749
 
457
- def get_account_balance(
750
+ async def get_account_balance(
458
751
  self, account_address: Optional[str] = None
459
752
  ) -> Dict[str, float]:
460
753
  """
@@ -466,9 +759,174 @@ class SubstrateClient:
466
759
  Returns:
467
760
  Dict[str, float]: Account balances (free, reserved, total)
468
761
  """
469
- raise NotImplementedError("Substrate functionality is not implemented yet.")
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
+ }
832
+
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}")
470
928
 
471
- def get_free_credits(self, account_address: Optional[str] = None) -> float:
929
+ async def get_free_credits(self, account_address: Optional[str] = None) -> float:
472
930
  """
473
931
  Get the free credits available for an account in the marketplace.
474
932
 
@@ -1,12 +0,0 @@
1
- hippius_sdk/__init__.py,sha256=KzTJTpSpFP1L1euDEkGnXh2pt1ReHGqyFn3atHA1Tfs,1391
2
- hippius_sdk/cli.py,sha256=L6_ZypipD9rqUYNqtmq4mB46eAcC4k170NNbnfx41ks,90051
3
- hippius_sdk/client.py,sha256=mMKX_m2ZwfbGVAU3zasHZQF0ddToqypkxGKTylruB3Y,14901
4
- hippius_sdk/config.py,sha256=WqocYwx-UomLeZ-iFUNDjg9vRcagOBA1Th68XELbuTs,22950
5
- hippius_sdk/ipfs.py,sha256=fFcDMR1-XUcSx1stwV_dD4Rr5fdlTGuSl1Zxag3eSSw,49437
6
- hippius_sdk/ipfs_core.py,sha256=w6ljgFdUzL-ffKxr4x_W-aYZTNpgyJg5HOFDrLKPILA,6690
7
- hippius_sdk/substrate.py,sha256=8Jq6IAGJR22WKQFFmafNOE85Q467JoVyQDK8udVU2d8,42021
8
- hippius_sdk/utils.py,sha256=-N0w0RfXhwxJgSkSroxqFMw-0zJQXvcmxM0OS5UtWEY,4145
9
- hippius-0.2.0.dist-info/METADATA,sha256=oHxH4kpLHWeDKInHLtKxA1HJWHOEJJWcPc5fMvl_orU,27993
10
- hippius-0.2.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
11
- hippius-0.2.0.dist-info/entry_points.txt,sha256=b1lo60zRXmv1ud-c5BC-cJcAfGE5FD4qM_nia6XeQtM,98
12
- hippius-0.2.0.dist-info/RECORD,,