hippius 0.1.11__py3-none-any.whl → 0.1.13__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/account.py DELETED
@@ -1,648 +0,0 @@
1
- from typing import Dict, List, Optional
2
- import json
3
- import os
4
- import uuid
5
- import time
6
-
7
- from substrateinterface import SubstrateInterface, Keypair
8
- from substrateinterface.exceptions import SubstrateRequestException
9
-
10
- from hippius_sdk.config import get_config_value, get_keypair
11
- from hippius_sdk.utils import ensure_directory_exists
12
-
13
-
14
- class AccountManager:
15
- """
16
- Manages Hippius blockchain accounts including coldkeys, hotkeys, and proxy relationships.
17
- """
18
-
19
- def __init__(self, substrate_interface=None, config_directory=None):
20
- """
21
- Initialize the AccountManager.
22
-
23
- Args:
24
- substrate_interface: Existing SubstrateInterface or None to create one
25
- config_directory: Custom config directory or None for default
26
- """
27
- # Initialize substrate interface if not provided
28
- if substrate_interface:
29
- self.substrate = substrate_interface
30
- else:
31
- node_url = get_config_value("substrate", "url")
32
- self.substrate = SubstrateInterface(url=node_url)
33
-
34
- # Set up config directory
35
- self.config_directory = config_directory
36
- if not self.config_directory:
37
- home_dir = os.path.expanduser("~")
38
- self.config_directory = os.path.join(home_dir, ".hippius")
39
-
40
- ensure_directory_exists(self.config_directory)
41
- self.accounts_file = os.path.join(self.config_directory, "accounts.json")
42
-
43
- # Initialize or load accounts data
44
- self._initialize_accounts_file()
45
-
46
- def _initialize_accounts_file(self):
47
- """Initialize the accounts file if it doesn't exist."""
48
- if not os.path.exists(self.accounts_file):
49
- with open(self.accounts_file, "w") as f:
50
- json.dump(
51
- {"coldkeys": {}, "hotkeys": {}, "relationships": []}, f, indent=2
52
- )
53
-
54
- def _load_accounts_data(self) -> Dict:
55
- """Load the accounts data from the file."""
56
- with open(self.accounts_file, "r") as f:
57
- return json.load(f)
58
-
59
- def _save_accounts_data(self, data: Dict):
60
- """Save the accounts data to the file."""
61
- with open(self.accounts_file, "w") as f:
62
- json.dump(data, f, indent=2)
63
-
64
- def create_coldkey(
65
- self,
66
- name: str = "hippius_coldkey",
67
- mnemonic: Optional[str] = None,
68
- encrypt: bool = True,
69
- password: Optional[str] = None,
70
- ) -> Dict:
71
- """
72
- Create a new coldkey or import one from a mnemonic.
73
-
74
- Args:
75
- name: Name for the coldkey (default: "hippius_coldkey")
76
- mnemonic: Optional mnemonic seed phrase to use
77
- encrypt: Whether to encrypt the mnemonic with a password
78
- password: Optional password (if None and encrypt=True, will prompt)
79
-
80
- Returns:
81
- Dict with coldkey information
82
- """
83
- # Create keypair
84
- if mnemonic:
85
- keypair = Keypair.create_from_mnemonic(mnemonic)
86
- else:
87
- keypair = Keypair.create_from_uri(f"//{name}_{uuid.uuid4()}")
88
- # Ensure we capture the mnemonic if generated
89
- mnemonic = keypair.mnemonic if hasattr(keypair, "mnemonic") else None
90
-
91
- # Save to accounts.json
92
- data = self._load_accounts_data()
93
-
94
- # Initialize coldkey info
95
- coldkey_info = {
96
- "name": name,
97
- "address": keypair.ss58_address,
98
- "created_at": time.time(),
99
- "public_key": keypair.public_key.hex(),
100
- "encrypted": encrypt,
101
- }
102
-
103
- # Handle encryption of mnemonic
104
- if mnemonic:
105
- if encrypt:
106
- # Import getpass here to avoid circular imports
107
- import getpass
108
-
109
- # Get password if not provided
110
- if password is None:
111
- password = getpass.getpass("Enter password to encrypt mnemonic: ")
112
- password_confirm = getpass.getpass("Confirm password: ")
113
-
114
- if password != password_confirm:
115
- raise ValueError("Passwords do not match")
116
-
117
- # Encrypt the mnemonic
118
- # We'll encrypt locally and store in our account.json file
119
- from hippius_sdk.config import encrypt_with_password
120
-
121
- encrypted_data, salt = encrypt_with_password(mnemonic, password)
122
-
123
- coldkey_info["mnemonic"] = encrypted_data
124
- coldkey_info["salt"] = salt
125
-
126
- # Also store in the main config for compatibility
127
- from hippius_sdk.config import set_seed_phrase
128
-
129
- set_seed_phrase(
130
- seed_phrase=mnemonic,
131
- encode=True,
132
- password=password,
133
- account_name=f"{name}_{keypair.ss58_address}",
134
- )
135
-
136
- print(f"Mnemonic encrypted and stored securely.")
137
- print(
138
- f"You will need your password to sign transactions with this account."
139
- )
140
- else:
141
- # Store without encryption (not recommended)
142
- coldkey_info["mnemonic"] = mnemonic
143
- print("WARNING: Mnemonic stored without encryption.")
144
- print("Consider using --encrypt for better security.")
145
-
146
- # Also store in the main config for compatibility
147
- from hippius_sdk.config import set_seed_phrase
148
-
149
- set_seed_phrase(
150
- seed_phrase=mnemonic,
151
- encode=False,
152
- account_name=f"{name}_{keypair.ss58_address}",
153
- )
154
-
155
- data["coldkeys"][keypair.ss58_address] = coldkey_info
156
- self._save_accounts_data(data)
157
-
158
- # Display information to the user
159
- print(f"Coldkey created successfully!")
160
- print(f"Name: {name}")
161
- print(f"Address: {keypair.ss58_address}")
162
-
163
- return coldkey_info
164
-
165
- def create_hotkey(
166
- self, name: Optional[str] = None, coldkey_address: Optional[str] = None
167
- ) -> Dict:
168
- """
169
- Create a new hotkey and associate it with a coldkey.
170
-
171
- Args:
172
- name: Optional custom name for the hotkey
173
- coldkey_address: SS58 address of the coldkey to associate with.
174
- If None, will attempt to use the only coldkey if only one exists,
175
- or will raise an error if multiple coldkeys exist.
176
-
177
- Returns:
178
- Dict with hotkey information
179
-
180
- Raises:
181
- ValueError: If no coldkey is provided and multiple coldkeys exist,
182
- or if the specified coldkey doesn't exist
183
- """
184
- # Load account data first to check coldkeys
185
- data = self._load_accounts_data()
186
-
187
- # Verify we have a valid coldkey to associate with
188
- if not coldkey_address:
189
- # If no coldkey specified, check if we have only one coldkey
190
- available_coldkeys = list(data["coldkeys"].keys())
191
-
192
- if not available_coldkeys:
193
- raise ValueError(
194
- "No coldkey provided and no coldkeys found. "
195
- "Please create a coldkey first with: hippius account coldkey create"
196
- )
197
- elif len(available_coldkeys) == 1:
198
- # If only one coldkey exists, use it automatically
199
- coldkey_address = available_coldkeys[0]
200
- coldkey_name = data["coldkeys"][coldkey_address]["name"]
201
- print(f"Automatically associating with the only available coldkey:")
202
- print(f" Name: {coldkey_name}")
203
- print(f" Address: {coldkey_address}")
204
- else:
205
- # If multiple coldkeys exist, we need the user to specify
206
- coldkey_list = "\n ".join(
207
- [f"{k} ({data['coldkeys'][k]['name']})" for k in available_coldkeys]
208
- )
209
- raise ValueError(
210
- f"No coldkey provided and multiple coldkeys exist. "
211
- f"Please specify which coldkey to associate with using --coldkey.\n"
212
- f"Available coldkeys:\n {coldkey_list}"
213
- )
214
- elif coldkey_address not in data["coldkeys"]:
215
- raise ValueError(f"Coldkey {coldkey_address} not found")
216
-
217
- # Generate a name if not provided
218
- if not name:
219
- # Get the next available hotkey number for this coldkey
220
- existing_relationships = [
221
- r["hotkey"]
222
- for r in data["relationships"]
223
- if r["coldkey"] == coldkey_address
224
- ]
225
-
226
- existing_hotkeys = [
227
- data["hotkeys"][addr]
228
- for addr in existing_relationships
229
- if addr in data["hotkeys"]
230
- ]
231
-
232
- # Use the coldkey's name as a prefix for better organization
233
- coldkey_name = data["coldkeys"][coldkey_address]["name"]
234
- hotkey_prefix = f"{coldkey_name}_hotkey_"
235
-
236
- # Find the next number
237
- next_number = 1
238
- if existing_hotkeys:
239
- numbers = []
240
- for k in existing_hotkeys:
241
- # Extract number from name if it follows our pattern
242
- if (
243
- k["name"].startswith(hotkey_prefix)
244
- and k["name"][len(hotkey_prefix) :].isdigit()
245
- ):
246
- numbers.append(int(k["name"][len(hotkey_prefix) :]))
247
-
248
- if numbers:
249
- next_number = max(numbers) + 1
250
-
251
- name = f"{hotkey_prefix}{next_number}"
252
-
253
- # Create keypair
254
- keypair = Keypair.create_from_uri(f"//{name}_{uuid.uuid4()}")
255
-
256
- # Save hotkey info
257
- hotkey_info = {
258
- "name": name,
259
- "address": keypair.ss58_address,
260
- "created_at": time.time(),
261
- "public_key": keypair.public_key.hex(),
262
- "mnemonic": keypair.mnemonic if hasattr(keypair, "mnemonic") else None,
263
- "coldkey": coldkey_address, # Store the associated coldkey for clarity
264
- }
265
-
266
- data["hotkeys"][keypair.ss58_address] = hotkey_info
267
-
268
- # Create relationship with coldkey
269
- relationship = {
270
- "coldkey": coldkey_address,
271
- "hotkey": keypair.ss58_address,
272
- "created_at": time.time(),
273
- }
274
- data["relationships"].append(relationship)
275
-
276
- # Save updated data
277
- self._save_accounts_data(data)
278
-
279
- # Display information to the user
280
- print(f"Hotkey created successfully!")
281
- print(f"Name: {name}")
282
- print(f"Address: {keypair.ss58_address}")
283
- print(
284
- f"Associated with coldkey: {coldkey_address} ({data['coldkeys'][coldkey_address]['name']})"
285
- )
286
- print(
287
- f"Note: This is only a local association. To create a blockchain proxy relationship,"
288
- )
289
- print(
290
- f"use: hippius account proxy create --coldkey {coldkey_address} --hotkey {keypair.ss58_address}"
291
- )
292
-
293
- return hotkey_info
294
-
295
- def list_coldkeys(self) -> List[Dict]:
296
- """List all stored coldkeys."""
297
- data = self._load_accounts_data()
298
- return list(data["coldkeys"].values())
299
-
300
- def list_hotkeys(self, coldkey_address: Optional[str] = None) -> List[Dict]:
301
- """
302
- List all stored hotkeys, optionally filtered by associated coldkey.
303
-
304
- Args:
305
- coldkey_address: If provided, only return hotkeys associated with this coldkey
306
-
307
- Returns:
308
- List of hotkey information dictionaries with coldkey association information
309
- """
310
- data = self._load_accounts_data()
311
-
312
- # Build a mapping of hotkey address to associated coldkey
313
- hotkey_to_coldkey = {}
314
- for relationship in data.get("relationships", []):
315
- hotkey = relationship.get("hotkey")
316
- coldkey = relationship.get("coldkey")
317
- if hotkey and coldkey:
318
- hotkey_to_coldkey[hotkey] = coldkey
319
-
320
- # Filter hotkeys based on coldkey_address if provided
321
- if coldkey_address:
322
- # Verify the coldkey exists
323
- if coldkey_address not in data["coldkeys"]:
324
- raise ValueError(f"Coldkey {coldkey_address} not found")
325
-
326
- # Get all hotkeys associated with this coldkey
327
- related_hotkey_addresses = [
328
- hotkey
329
- for hotkey, coldkey in hotkey_to_coldkey.items()
330
- if coldkey == coldkey_address
331
- ]
332
-
333
- # Return the hotkey information with added coldkey association
334
- result = []
335
- for addr in related_hotkey_addresses:
336
- if addr in data["hotkeys"]:
337
- hotkey_info = data["hotkeys"][addr].copy()
338
- # Make sure the coldkey association is included in the info
339
- hotkey_info["associated_coldkey"] = coldkey_address
340
- hotkey_info["coldkey_name"] = data["coldkeys"][coldkey_address][
341
- "name"
342
- ]
343
- result.append(hotkey_info)
344
-
345
- return result
346
- else:
347
- # Return all hotkeys with their coldkey associations
348
- result = []
349
- for addr, info in data["hotkeys"].items():
350
- hotkey_info = info.copy()
351
- # Add coldkey association information if available
352
- if addr in hotkey_to_coldkey:
353
- coldkey = hotkey_to_coldkey[addr]
354
- hotkey_info["associated_coldkey"] = coldkey
355
- if coldkey in data["coldkeys"]:
356
- hotkey_info["coldkey_name"] = data["coldkeys"][coldkey]["name"]
357
- result.append(hotkey_info)
358
-
359
- return result
360
-
361
- def create_proxy_relationship(
362
- self,
363
- coldkey_address: str,
364
- hotkey_address: str,
365
- proxy_type: str = "NonTransfer",
366
- delay: int = 0,
367
- password: Optional[str] = None,
368
- ) -> Dict:
369
- """
370
- Create a proxy relationship between coldkey and hotkey on the blockchain.
371
-
372
- Args:
373
- coldkey_address: The SS58 address of the coldkey (delegator)
374
- hotkey_address: The SS58 address of the hotkey (delegate)
375
- proxy_type: The proxy type (default: "NonTransfer")
376
- delay: Delay in blocks before the proxy becomes active
377
- password: Optional password for decrypting the coldkey mnemonic
378
-
379
- Returns:
380
- Dict with transaction details
381
- """
382
- # Verify the accounts exist
383
- data = self._load_accounts_data()
384
- if coldkey_address not in data["coldkeys"]:
385
- raise ValueError(f"Coldkey {coldkey_address} not found")
386
-
387
- if hotkey_address not in data["hotkeys"]:
388
- raise ValueError(f"Hotkey {hotkey_address} not found")
389
-
390
- # Get the mnemonic for the coldkey and create a keypair
391
- try:
392
- # Try first with our own method which handles our encrypted storage
393
- mnemonic = self.get_coldkey_mnemonic(coldkey_address, password)
394
- keypair = Keypair.create_from_mnemonic(mnemonic)
395
- except Exception as e:
396
- print(f"Could not retrieve coldkey mnemonic: {str(e)}")
397
- print("Falling back to global config keypair...")
398
-
399
- # Fall back to the config system's get_keypair as a backup
400
- from hippius_sdk.config import get_keypair
401
-
402
- keypair = get_keypair(ss58_address=coldkey_address)
403
-
404
- # Create proxy
405
- call = self.substrate.compose_call(
406
- call_module="Proxy",
407
- call_function="addProxy",
408
- call_params={
409
- "delegate": hotkey_address,
410
- "proxyType": proxy_type,
411
- "delay": delay,
412
- },
413
- )
414
-
415
- # Create and sign extrinsic
416
- extrinsic = self.substrate.create_signed_extrinsic(call=call, keypair=keypair)
417
-
418
- # Submit and get result
419
- try:
420
- response = self.substrate.submit_extrinsic(
421
- extrinsic=extrinsic, wait_for_inclusion=True
422
- )
423
-
424
- result = {
425
- "success": True,
426
- "transaction_hash": response.extrinsic_hash,
427
- "block_hash": response.block_hash,
428
- "coldkey": coldkey_address,
429
- "hotkey": hotkey_address,
430
- "proxy_type": proxy_type,
431
- "delay": delay,
432
- }
433
-
434
- # Update local records
435
- relationship = {
436
- "coldkey": coldkey_address,
437
- "hotkey": hotkey_address,
438
- "proxy_type": proxy_type,
439
- "delay": delay,
440
- "created_at": time.time(),
441
- "transaction_hash": response.extrinsic_hash,
442
- }
443
- data["relationships"].append(relationship)
444
- self._save_accounts_data(data)
445
-
446
- return result
447
-
448
- except SubstrateRequestException as e:
449
- return {
450
- "success": False,
451
- "error": str(e),
452
- "coldkey": coldkey_address,
453
- "hotkey": hotkey_address,
454
- }
455
-
456
- def list_proxies(self, coldkey_address: Optional[str] = None) -> List[Dict]:
457
- """
458
- List proxy relationships from the blockchain, optionally filtered by coldkey.
459
-
460
- Args:
461
- coldkey_address: If provided, only return proxies for this coldkey
462
-
463
- Returns:
464
- List of proxy relationship information dictionaries
465
- """
466
- # Query the chain for registered proxies
467
- chain_proxies = []
468
-
469
- # If coldkey specified, only query for that address
470
- addresses_to_query = [coldkey_address] if coldkey_address else []
471
-
472
- # If no specific coldkey, get all coldkeys from our records
473
- if not coldkey_address:
474
- data = self._load_accounts_data()
475
- addresses_to_query = list(data["coldkeys"].keys())
476
-
477
- # Query chain for each address
478
- for address in addresses_to_query:
479
- try:
480
- result = self.substrate.query(
481
- module="Proxy", storage_function="Proxies", params=[address]
482
- )
483
-
484
- # Format depends on the specific chain implementation
485
- if result and result.value:
486
- proxies_data = result.value[0] # Typically [proxies, deposit]
487
-
488
- for proxy in proxies_data:
489
- chain_proxies.append(
490
- {
491
- "coldkey": address,
492
- "hotkey": proxy["delegate"],
493
- "proxy_type": proxy["proxyType"],
494
- "delay": proxy["delay"],
495
- "source": "blockchain",
496
- }
497
- )
498
- except Exception as e:
499
- print(f"Error querying proxies for {address}: {str(e)}")
500
-
501
- return chain_proxies
502
-
503
- def remove_proxy(
504
- self,
505
- coldkey_address: str,
506
- hotkey_address: str,
507
- password: Optional[str] = None,
508
- proxy_type: str = "NonTransfer",
509
- delay: int = 0,
510
- ) -> Dict:
511
- """
512
- Remove a proxy relationship from the blockchain.
513
-
514
- Args:
515
- coldkey_address: The SS58 address of the coldkey (delegator)
516
- hotkey_address: The SS58 address of the hotkey (delegate)
517
- password: Optional password for decrypting the coldkey mnemonic
518
- proxy_type: The proxy type (default: "NonTransfer")
519
- delay: Delay value used when creating the proxy
520
-
521
- Returns:
522
- Dict with transaction details
523
- """
524
- # Get the mnemonic for the coldkey and create a keypair
525
- try:
526
- # Try first with our own method which handles our encrypted storage
527
- mnemonic = self.get_coldkey_mnemonic(coldkey_address, password)
528
- keypair = Keypair.create_from_mnemonic(mnemonic)
529
- except Exception as e:
530
- print(f"Could not retrieve coldkey mnemonic: {str(e)}")
531
- print("Falling back to global config keypair...")
532
-
533
- # Fall back to the config system's get_keypair as a backup
534
- from hippius_sdk.config import get_keypair
535
-
536
- keypair = get_keypair(ss58_address=coldkey_address)
537
-
538
- # Remove proxy
539
- call = self.substrate.compose_call(
540
- call_module="Proxy",
541
- call_function="removeProxy",
542
- call_params={
543
- "delegate": hotkey_address,
544
- "proxyType": proxy_type,
545
- "delay": delay,
546
- },
547
- )
548
-
549
- # Create and sign extrinsic
550
- extrinsic = self.substrate.create_signed_extrinsic(call=call, keypair=keypair)
551
-
552
- # Submit and get result
553
- try:
554
- response = self.substrate.submit_extrinsic(
555
- extrinsic=extrinsic, wait_for_inclusion=True
556
- )
557
-
558
- # Update local records
559
- data = self._load_accounts_data()
560
- data["relationships"] = [
561
- r
562
- for r in data["relationships"]
563
- if not (
564
- r["coldkey"] == coldkey_address and r["hotkey"] == hotkey_address
565
- )
566
- ]
567
- self._save_accounts_data(data)
568
-
569
- return {
570
- "success": True,
571
- "transaction_hash": response.extrinsic_hash,
572
- "block_hash": response.block_hash,
573
- "coldkey": coldkey_address,
574
- "hotkey": hotkey_address,
575
- }
576
-
577
- except SubstrateRequestException as e:
578
- return {
579
- "success": False,
580
- "error": str(e),
581
- "coldkey": coldkey_address,
582
- "hotkey": hotkey_address,
583
- }
584
-
585
- def get_coldkey_mnemonic(
586
- self, coldkey_address: str, password: Optional[str] = None
587
- ) -> str:
588
- """
589
- Get a coldkey's mnemonic, decrypting it if necessary.
590
-
591
- Args:
592
- coldkey_address: The SS58 address of the coldkey
593
- password: Optional password for decryption (if None and needed, will prompt)
594
-
595
- Returns:
596
- str: The mnemonic seed phrase
597
-
598
- Raises:
599
- ValueError: If the coldkey doesn't exist or the password is incorrect
600
- """
601
- # Load the coldkey data
602
- data = self._load_accounts_data()
603
-
604
- if coldkey_address not in data["coldkeys"]:
605
- raise ValueError(f"Coldkey {coldkey_address} not found")
606
-
607
- coldkey_info = data["coldkeys"][coldkey_address]
608
-
609
- # Check if the mnemonic is encrypted
610
- if coldkey_info.get("encrypted", False):
611
- # If encrypted, we need to decrypt it
612
- if "mnemonic" not in coldkey_info or "salt" not in coldkey_info:
613
- # Try to get from main config as fallback
614
- from hippius_sdk.config import get_seed_phrase
615
-
616
- account_name = f"{coldkey_info['name']}_{coldkey_address}"
617
-
618
- mnemonic = get_seed_phrase(password=password, account_name=account_name)
619
- if mnemonic:
620
- return mnemonic
621
-
622
- raise ValueError(
623
- f"Mnemonic for coldkey {coldkey_address} is marked as encrypted "
624
- f"but encryption data is missing"
625
- )
626
-
627
- # Import needed modules for decryption
628
- import getpass
629
- from hippius_sdk.config import decrypt_with_password
630
-
631
- # Get password if not provided
632
- if password is None:
633
- password = getpass.getpass("Enter password to decrypt mnemonic: ")
634
-
635
- # Decrypt the mnemonic
636
- try:
637
- encrypted_data = coldkey_info["mnemonic"]
638
- salt = coldkey_info["salt"]
639
- mnemonic = decrypt_with_password(encrypted_data, salt, password)
640
- return mnemonic
641
- except Exception as e:
642
- raise ValueError(f"Failed to decrypt mnemonic: {str(e)}")
643
- else:
644
- # If not encrypted, just return the mnemonic
645
- if "mnemonic" not in coldkey_info:
646
- raise ValueError(f"No mnemonic found for coldkey {coldkey_address}")
647
-
648
- return coldkey_info["mnemonic"]