hippius 0.1.6__py3-none-any.whl → 0.1.9__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.6.dist-info → hippius-0.1.9.dist-info}/METADATA +365 -7
- hippius-0.1.9.dist-info/RECORD +10 -0
- hippius_sdk/__init__.py +45 -1
- hippius_sdk/cli.py +1269 -36
- hippius_sdk/client.py +53 -12
- hippius_sdk/config.py +744 -0
- hippius_sdk/ipfs.py +178 -87
- hippius_sdk/substrate.py +180 -88
- hippius-0.1.6.dist-info/RECORD +0 -9
- {hippius-0.1.6.dist-info → hippius-0.1.9.dist-info}/WHEEL +0 -0
- {hippius-0.1.6.dist-info → hippius-0.1.9.dist-info}/entry_points.txt +0 -0
hippius_sdk/config.py
ADDED
@@ -0,0 +1,744 @@
|
|
1
|
+
"""
|
2
|
+
Configuration management for Hippius SDK.
|
3
|
+
|
4
|
+
This module handles loading and saving configuration from the user's home directory,
|
5
|
+
specifically in ~/.hippius/config.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import json
|
10
|
+
import base64
|
11
|
+
import hashlib
|
12
|
+
import getpass
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Dict, Any, Optional, List, Union, Tuple
|
15
|
+
|
16
|
+
# Define constants
|
17
|
+
CONFIG_DIR = os.path.expanduser("~/.hippius")
|
18
|
+
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
19
|
+
DEFAULT_CONFIG = {
|
20
|
+
"ipfs": {
|
21
|
+
"gateway": "https://ipfs.io",
|
22
|
+
"api_url": "https://store.hippius.network",
|
23
|
+
"local_ipfs": False,
|
24
|
+
},
|
25
|
+
"substrate": {
|
26
|
+
"url": "wss://rpc.hippius.network",
|
27
|
+
"seed_phrase": None,
|
28
|
+
"seed_phrase_encoded": False,
|
29
|
+
"seed_phrase_salt": None, # Salt for password-based encryption
|
30
|
+
"default_miners": [],
|
31
|
+
"active_account": None, # Name of the active account
|
32
|
+
"accounts": {}, # Dictionary of accounts with names as keys
|
33
|
+
},
|
34
|
+
"encryption": {
|
35
|
+
"encrypt_by_default": False,
|
36
|
+
"encryption_key": None,
|
37
|
+
},
|
38
|
+
"erasure_coding": {
|
39
|
+
"default_k": 3,
|
40
|
+
"default_m": 5,
|
41
|
+
"default_chunk_size": 1024 * 1024, # 1MB
|
42
|
+
},
|
43
|
+
"cli": {
|
44
|
+
"verbose": False,
|
45
|
+
"max_retries": 3,
|
46
|
+
},
|
47
|
+
}
|
48
|
+
|
49
|
+
|
50
|
+
def ensure_config_dir() -> None:
|
51
|
+
"""Create configuration directory if it doesn't exist."""
|
52
|
+
if not os.path.exists(CONFIG_DIR):
|
53
|
+
try:
|
54
|
+
os.makedirs(CONFIG_DIR, exist_ok=True)
|
55
|
+
print(f"Created Hippius configuration directory: {CONFIG_DIR}")
|
56
|
+
except Exception as e:
|
57
|
+
print(f"Warning: Could not create configuration directory: {e}")
|
58
|
+
|
59
|
+
|
60
|
+
def load_config() -> Dict[str, Any]:
|
61
|
+
"""
|
62
|
+
Load configuration from the config file.
|
63
|
+
|
64
|
+
If the file doesn't exist, create it with default values.
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
Dict[str, Any]: The configuration dictionary
|
68
|
+
"""
|
69
|
+
ensure_config_dir()
|
70
|
+
|
71
|
+
if not os.path.exists(CONFIG_FILE):
|
72
|
+
save_config(DEFAULT_CONFIG)
|
73
|
+
return DEFAULT_CONFIG.copy()
|
74
|
+
|
75
|
+
try:
|
76
|
+
with open(CONFIG_FILE, "r") as f:
|
77
|
+
config = json.load(f)
|
78
|
+
|
79
|
+
# Ensure all config sections exist (for backward compatibility)
|
80
|
+
for section, defaults in DEFAULT_CONFIG.items():
|
81
|
+
if section not in config:
|
82
|
+
config[section] = defaults
|
83
|
+
|
84
|
+
return config
|
85
|
+
except Exception as e:
|
86
|
+
print(f"Warning: Could not load configuration file: {e}")
|
87
|
+
print(f"Using default configuration")
|
88
|
+
return DEFAULT_CONFIG.copy()
|
89
|
+
|
90
|
+
|
91
|
+
def save_config(config: Dict[str, Any]) -> bool:
|
92
|
+
"""
|
93
|
+
Save configuration to the config file.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
config: The configuration dictionary to save
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
bool: True if save was successful, False otherwise
|
100
|
+
"""
|
101
|
+
ensure_config_dir()
|
102
|
+
|
103
|
+
try:
|
104
|
+
with open(CONFIG_FILE, "w") as f:
|
105
|
+
json.dump(config, f, indent=2)
|
106
|
+
return True
|
107
|
+
except Exception as e:
|
108
|
+
print(f"Warning: Could not save configuration file: {e}")
|
109
|
+
return False
|
110
|
+
|
111
|
+
|
112
|
+
def get_config_value(section: str, key: str, default: Any = None) -> Any:
|
113
|
+
"""
|
114
|
+
Get a configuration value from a specific section.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
section: The configuration section
|
118
|
+
key: The configuration key
|
119
|
+
default: Default value if not found
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Any: The configuration value or default
|
123
|
+
"""
|
124
|
+
config = load_config()
|
125
|
+
return config.get(section, {}).get(key, default)
|
126
|
+
|
127
|
+
|
128
|
+
def set_config_value(section: str, key: str, value: Any) -> bool:
|
129
|
+
"""
|
130
|
+
Set a configuration value in a specific section.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
section: The configuration section
|
134
|
+
key: The configuration key
|
135
|
+
value: The value to set
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
bool: True if save was successful, False otherwise
|
139
|
+
"""
|
140
|
+
config = load_config()
|
141
|
+
|
142
|
+
if section not in config:
|
143
|
+
config[section] = {}
|
144
|
+
|
145
|
+
config[section][key] = value
|
146
|
+
return save_config(config)
|
147
|
+
|
148
|
+
|
149
|
+
def get_encryption_key() -> Optional[bytes]:
|
150
|
+
"""
|
151
|
+
Get the encryption key from the configuration.
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
Optional[bytes]: The encryption key or None if not set
|
155
|
+
"""
|
156
|
+
key_str = get_config_value("encryption", "encryption_key")
|
157
|
+
if not key_str:
|
158
|
+
return None
|
159
|
+
|
160
|
+
try:
|
161
|
+
return base64.b64decode(key_str)
|
162
|
+
except Exception as e:
|
163
|
+
print(f"Warning: Could not decode encryption key from config: {e}")
|
164
|
+
return None
|
165
|
+
|
166
|
+
|
167
|
+
def set_encryption_key(key: Union[bytes, str]) -> bool:
|
168
|
+
"""
|
169
|
+
Set the encryption key in the configuration.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
key: The encryption key (bytes or base64-encoded string)
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
bool: True if save was successful, False otherwise
|
176
|
+
"""
|
177
|
+
# Convert bytes to base64 string if needed
|
178
|
+
if isinstance(key, bytes):
|
179
|
+
key = base64.b64encode(key).decode()
|
180
|
+
|
181
|
+
return set_config_value("encryption", "encryption_key", key)
|
182
|
+
|
183
|
+
|
184
|
+
def _derive_key_from_password(
|
185
|
+
password: str, salt: Optional[bytes] = None
|
186
|
+
) -> Tuple[bytes, bytes]:
|
187
|
+
"""
|
188
|
+
Derive an encryption key from a password using PBKDF2.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
password: The user password
|
192
|
+
salt: Optional salt bytes. If None, a new random salt is generated
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
Tuple[bytes, bytes]: (derived_key, salt)
|
196
|
+
"""
|
197
|
+
# Import cryptography for PBKDF2
|
198
|
+
try:
|
199
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
200
|
+
from cryptography.hazmat.primitives import hashes
|
201
|
+
import os
|
202
|
+
except ImportError:
|
203
|
+
raise ImportError(
|
204
|
+
"cryptography is required for password-based encryption. Install it with: pip install cryptography"
|
205
|
+
)
|
206
|
+
|
207
|
+
# Generate a salt if not provided
|
208
|
+
if salt is None:
|
209
|
+
salt = os.urandom(16)
|
210
|
+
|
211
|
+
# Create a PBKDF2HMAC instance
|
212
|
+
kdf = PBKDF2HMAC(
|
213
|
+
algorithm=hashes.SHA256(),
|
214
|
+
length=32, # 32 bytes (256 bits) key
|
215
|
+
salt=salt,
|
216
|
+
iterations=100000, # Recommended minimum by NIST
|
217
|
+
)
|
218
|
+
|
219
|
+
# Derive the key
|
220
|
+
key = kdf.derive(password.encode("utf-8"))
|
221
|
+
|
222
|
+
return key, salt
|
223
|
+
|
224
|
+
|
225
|
+
def encrypt_with_password(data: str, password: str) -> Tuple[str, str]:
|
226
|
+
"""
|
227
|
+
Encrypt data using a password-derived key.
|
228
|
+
|
229
|
+
Args:
|
230
|
+
data: String data to encrypt
|
231
|
+
password: User password
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
Tuple[str, str]: (base64_encrypted_data, base64_salt)
|
235
|
+
"""
|
236
|
+
try:
|
237
|
+
# Derive key from password with a new salt
|
238
|
+
key, salt = _derive_key_from_password(password)
|
239
|
+
|
240
|
+
# Import NaCl for encryption
|
241
|
+
try:
|
242
|
+
import nacl.secret
|
243
|
+
import nacl.utils
|
244
|
+
except ImportError:
|
245
|
+
raise ValueError(
|
246
|
+
"PyNaCl is required for encryption. Install it with: pip install pynacl"
|
247
|
+
)
|
248
|
+
|
249
|
+
# Create a SecretBox with our derived key
|
250
|
+
box = nacl.secret.SecretBox(key)
|
251
|
+
|
252
|
+
# Encrypt the data
|
253
|
+
encrypted_data = box.encrypt(data.encode("utf-8"))
|
254
|
+
|
255
|
+
# Convert to base64 for storage
|
256
|
+
encoded_data = base64.b64encode(encrypted_data).decode("utf-8")
|
257
|
+
encoded_salt = base64.b64encode(salt).decode("utf-8")
|
258
|
+
|
259
|
+
return encoded_data, encoded_salt
|
260
|
+
|
261
|
+
except Exception as e:
|
262
|
+
raise ValueError(f"Error encrypting data with password: {e}")
|
263
|
+
|
264
|
+
|
265
|
+
def decrypt_with_password(encrypted_data: str, salt: str, password: str) -> str:
|
266
|
+
"""
|
267
|
+
Decrypt data using a password-derived key.
|
268
|
+
|
269
|
+
Args:
|
270
|
+
encrypted_data: Base64-encoded encrypted data
|
271
|
+
salt: Base64-encoded salt
|
272
|
+
password: User password
|
273
|
+
|
274
|
+
Returns:
|
275
|
+
str: Decrypted data
|
276
|
+
"""
|
277
|
+
try:
|
278
|
+
# Decode the encrypted data and salt
|
279
|
+
encrypted_bytes = base64.b64decode(encrypted_data)
|
280
|
+
salt_bytes = base64.b64decode(salt)
|
281
|
+
|
282
|
+
# Derive the key from the password and salt
|
283
|
+
key, _ = _derive_key_from_password(password, salt_bytes)
|
284
|
+
|
285
|
+
# Import NaCl for decryption
|
286
|
+
try:
|
287
|
+
import nacl.secret
|
288
|
+
import nacl.utils
|
289
|
+
except ImportError:
|
290
|
+
raise ValueError(
|
291
|
+
"PyNaCl is required for decryption. Install it with: pip install pynacl"
|
292
|
+
)
|
293
|
+
|
294
|
+
# Create a SecretBox with our derived key
|
295
|
+
box = nacl.secret.SecretBox(key)
|
296
|
+
|
297
|
+
# Decrypt the data
|
298
|
+
decrypted_data = box.decrypt(encrypted_bytes)
|
299
|
+
|
300
|
+
# Return the decrypted string
|
301
|
+
return decrypted_data.decode("utf-8")
|
302
|
+
|
303
|
+
except Exception as e:
|
304
|
+
raise ValueError(f"Error decrypting data with password: {e}")
|
305
|
+
|
306
|
+
|
307
|
+
def encrypt_seed_phrase(
|
308
|
+
seed_phrase: str, password: Optional[str] = None, account_name: Optional[str] = None
|
309
|
+
) -> bool:
|
310
|
+
"""
|
311
|
+
Encrypt the substrate seed phrase using password-based encryption.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
seed_phrase: The plain text seed phrase to encrypt
|
315
|
+
password: Optional password (if None, will prompt)
|
316
|
+
account_name: Optional name for the account (if None, uses legacy mode or active account)
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
bool: True if encryption and saving was successful, False otherwise
|
320
|
+
"""
|
321
|
+
try:
|
322
|
+
# Get password from user if not provided
|
323
|
+
if password is None:
|
324
|
+
password = getpass.getpass("Enter password to encrypt seed phrase: ")
|
325
|
+
password_confirm = getpass.getpass("Confirm password: ")
|
326
|
+
|
327
|
+
if password != password_confirm:
|
328
|
+
raise ValueError("Passwords do not match")
|
329
|
+
|
330
|
+
# Encrypt the seed phrase
|
331
|
+
encrypted_data, salt = encrypt_with_password(seed_phrase, password)
|
332
|
+
|
333
|
+
# Get the SS58 address from the seed phrase
|
334
|
+
ss58_address = None
|
335
|
+
try:
|
336
|
+
from substrateinterface import Keypair
|
337
|
+
|
338
|
+
keypair = Keypair.create_from_mnemonic(seed_phrase)
|
339
|
+
ss58_address = keypair.ss58_address
|
340
|
+
except Exception as e:
|
341
|
+
print(f"Warning: Could not derive SS58 address: {e}")
|
342
|
+
|
343
|
+
config = load_config()
|
344
|
+
|
345
|
+
# Check if we're using the new multi-account system
|
346
|
+
if account_name is not None:
|
347
|
+
# Multi-account mode
|
348
|
+
if "accounts" not in config["substrate"]:
|
349
|
+
config["substrate"]["accounts"] = {}
|
350
|
+
|
351
|
+
# Store the account data
|
352
|
+
config["substrate"]["accounts"][account_name] = {
|
353
|
+
"seed_phrase": encrypted_data,
|
354
|
+
"seed_phrase_encoded": True,
|
355
|
+
"seed_phrase_salt": salt,
|
356
|
+
"ss58_address": ss58_address,
|
357
|
+
}
|
358
|
+
|
359
|
+
# Set as active account if no active account exists
|
360
|
+
if not config["substrate"].get("active_account"):
|
361
|
+
config["substrate"]["active_account"] = account_name
|
362
|
+
|
363
|
+
else:
|
364
|
+
# Legacy mode - single account
|
365
|
+
config["substrate"]["seed_phrase"] = encrypted_data
|
366
|
+
config["substrate"]["seed_phrase_encoded"] = True
|
367
|
+
config["substrate"]["seed_phrase_salt"] = salt
|
368
|
+
config["substrate"]["ss58_address"] = ss58_address
|
369
|
+
|
370
|
+
return save_config(config)
|
371
|
+
|
372
|
+
except Exception as e:
|
373
|
+
print(f"Error encrypting seed phrase: {e}")
|
374
|
+
return False
|
375
|
+
|
376
|
+
|
377
|
+
def decrypt_seed_phrase(
|
378
|
+
password: Optional[str] = None, account_name: Optional[str] = None
|
379
|
+
) -> Optional[str]:
|
380
|
+
"""
|
381
|
+
Decrypt the substrate seed phrase using password-based decryption.
|
382
|
+
|
383
|
+
Args:
|
384
|
+
password: Optional password (if None, will prompt)
|
385
|
+
account_name: Optional account name (if None, uses active account or legacy mode)
|
386
|
+
|
387
|
+
Returns:
|
388
|
+
Optional[str]: The decrypted seed phrase, or None if decryption failed
|
389
|
+
"""
|
390
|
+
try:
|
391
|
+
config = load_config()
|
392
|
+
|
393
|
+
# Determine if we're using multi-account mode
|
394
|
+
if account_name is not None or config["substrate"].get("active_account"):
|
395
|
+
# Multi-account mode
|
396
|
+
name_to_use = account_name or config["substrate"].get("active_account")
|
397
|
+
|
398
|
+
if not name_to_use:
|
399
|
+
print("Error: No account specified and no active account")
|
400
|
+
return None
|
401
|
+
|
402
|
+
if name_to_use not in config["substrate"].get("accounts", {}):
|
403
|
+
print(f"Error: Account '{name_to_use}' not found")
|
404
|
+
return None
|
405
|
+
|
406
|
+
account_data = config["substrate"]["accounts"][name_to_use]
|
407
|
+
is_encoded = account_data.get("seed_phrase_encoded", False)
|
408
|
+
|
409
|
+
if not is_encoded:
|
410
|
+
return account_data.get("seed_phrase")
|
411
|
+
|
412
|
+
encrypted_data = account_data.get("seed_phrase")
|
413
|
+
salt = account_data.get("seed_phrase_salt")
|
414
|
+
|
415
|
+
else:
|
416
|
+
# Legacy mode - single account
|
417
|
+
is_encoded = config["substrate"].get("seed_phrase_encoded", False)
|
418
|
+
|
419
|
+
if not is_encoded:
|
420
|
+
return config["substrate"].get("seed_phrase")
|
421
|
+
|
422
|
+
encrypted_data = config["substrate"].get("seed_phrase")
|
423
|
+
salt = config["substrate"].get("seed_phrase_salt")
|
424
|
+
|
425
|
+
if not encrypted_data or not salt:
|
426
|
+
print("Error: No encrypted seed phrase found or missing salt")
|
427
|
+
return None
|
428
|
+
|
429
|
+
# Get password from user if not provided
|
430
|
+
if password is None:
|
431
|
+
password = getpass.getpass("Enter password to decrypt seed phrase: ")
|
432
|
+
|
433
|
+
# Decrypt the seed phrase
|
434
|
+
return decrypt_with_password(encrypted_data, salt, password)
|
435
|
+
|
436
|
+
except Exception as e:
|
437
|
+
print(f"Error decrypting seed phrase: {e}")
|
438
|
+
return None
|
439
|
+
|
440
|
+
|
441
|
+
def get_seed_phrase(
|
442
|
+
password: Optional[str] = None, account_name: Optional[str] = None
|
443
|
+
) -> Optional[str]:
|
444
|
+
"""
|
445
|
+
Get the substrate seed phrase from configuration, decrypting if necessary.
|
446
|
+
|
447
|
+
Args:
|
448
|
+
password: Optional password for decryption (if None and needed, will prompt)
|
449
|
+
account_name: Optional account name (if None, uses active account or legacy mode)
|
450
|
+
|
451
|
+
Returns:
|
452
|
+
Optional[str]: The seed phrase, or None if not available
|
453
|
+
"""
|
454
|
+
config = load_config()
|
455
|
+
|
456
|
+
# Determine if we're using multi-account mode
|
457
|
+
if account_name is not None or config["substrate"].get("active_account"):
|
458
|
+
# Multi-account mode
|
459
|
+
name_to_use = account_name or config["substrate"].get("active_account")
|
460
|
+
|
461
|
+
if not name_to_use:
|
462
|
+
print("Error: No account specified and no active account")
|
463
|
+
return None
|
464
|
+
|
465
|
+
if name_to_use not in config["substrate"].get("accounts", {}):
|
466
|
+
print(f"Error: Account '{name_to_use}' not found")
|
467
|
+
return None
|
468
|
+
|
469
|
+
account_data = config["substrate"]["accounts"][name_to_use]
|
470
|
+
is_encoded = account_data.get("seed_phrase_encoded", False)
|
471
|
+
|
472
|
+
else:
|
473
|
+
# Legacy mode - single account
|
474
|
+
is_encoded = config["substrate"].get("seed_phrase_encoded", False)
|
475
|
+
|
476
|
+
if is_encoded:
|
477
|
+
# If encoded, decrypt it
|
478
|
+
return decrypt_seed_phrase(password, account_name)
|
479
|
+
else:
|
480
|
+
# If not encoded, just return the plain text seed phrase
|
481
|
+
if account_name is not None or config["substrate"].get("active_account"):
|
482
|
+
# Multi-account mode
|
483
|
+
name_to_use = account_name or config["substrate"].get("active_account")
|
484
|
+
return config["substrate"]["accounts"][name_to_use].get("seed_phrase")
|
485
|
+
else:
|
486
|
+
# Legacy mode
|
487
|
+
return config["substrate"].get("seed_phrase")
|
488
|
+
|
489
|
+
|
490
|
+
def set_seed_phrase(
|
491
|
+
seed_phrase: str,
|
492
|
+
encode: bool = False,
|
493
|
+
password: Optional[str] = None,
|
494
|
+
account_name: Optional[str] = None,
|
495
|
+
) -> bool:
|
496
|
+
"""
|
497
|
+
Set the substrate seed phrase in configuration, with optional encryption.
|
498
|
+
|
499
|
+
Args:
|
500
|
+
seed_phrase: The seed phrase to store
|
501
|
+
encode: Whether to encrypt the seed phrase (requires password)
|
502
|
+
password: Optional password for encryption (if None and encode=True, will prompt)
|
503
|
+
account_name: Optional name for the account (if None, uses legacy mode or active account)
|
504
|
+
|
505
|
+
Returns:
|
506
|
+
bool: True if saving was successful, False otherwise
|
507
|
+
"""
|
508
|
+
if encode:
|
509
|
+
return encrypt_seed_phrase(seed_phrase, password, account_name)
|
510
|
+
else:
|
511
|
+
config = load_config()
|
512
|
+
|
513
|
+
# Get the SS58 address from the seed phrase
|
514
|
+
ss58_address = None
|
515
|
+
try:
|
516
|
+
from substrateinterface import Keypair
|
517
|
+
|
518
|
+
keypair = Keypair.create_from_mnemonic(seed_phrase)
|
519
|
+
ss58_address = keypair.ss58_address
|
520
|
+
except Exception as e:
|
521
|
+
print(f"Warning: Could not derive SS58 address: {e}")
|
522
|
+
|
523
|
+
# Determine if we're using multi-account mode
|
524
|
+
if account_name is not None:
|
525
|
+
# Multi-account mode
|
526
|
+
if "accounts" not in config["substrate"]:
|
527
|
+
config["substrate"]["accounts"] = {}
|
528
|
+
|
529
|
+
# Store the account data
|
530
|
+
config["substrate"]["accounts"][account_name] = {
|
531
|
+
"seed_phrase": seed_phrase,
|
532
|
+
"seed_phrase_encoded": False,
|
533
|
+
"seed_phrase_salt": None,
|
534
|
+
"ss58_address": ss58_address,
|
535
|
+
}
|
536
|
+
|
537
|
+
# Set as active account if no active account exists
|
538
|
+
if not config["substrate"].get("active_account"):
|
539
|
+
config["substrate"]["active_account"] = account_name
|
540
|
+
|
541
|
+
else:
|
542
|
+
# Legacy mode - single account
|
543
|
+
config["substrate"]["seed_phrase"] = seed_phrase
|
544
|
+
config["substrate"]["seed_phrase_encoded"] = False
|
545
|
+
config["substrate"]["seed_phrase_salt"] = None
|
546
|
+
config["substrate"]["ss58_address"] = ss58_address
|
547
|
+
|
548
|
+
return save_config(config)
|
549
|
+
|
550
|
+
|
551
|
+
def get_active_account() -> Optional[str]:
|
552
|
+
"""
|
553
|
+
Get the name of the currently active account.
|
554
|
+
|
555
|
+
Returns:
|
556
|
+
Optional[str]: The name of the active account, or None if not set
|
557
|
+
"""
|
558
|
+
return get_config_value("substrate", "active_account")
|
559
|
+
|
560
|
+
|
561
|
+
def set_active_account(account_name: str) -> bool:
|
562
|
+
"""
|
563
|
+
Set the active account by name.
|
564
|
+
|
565
|
+
Args:
|
566
|
+
account_name: Name of the account to set as active
|
567
|
+
|
568
|
+
Returns:
|
569
|
+
bool: True if successful, False otherwise
|
570
|
+
"""
|
571
|
+
config = load_config()
|
572
|
+
|
573
|
+
# Check if the account exists
|
574
|
+
if account_name not in config["substrate"].get("accounts", {}):
|
575
|
+
print(f"Error: Account '{account_name}' not found")
|
576
|
+
return False
|
577
|
+
|
578
|
+
# Set as active account
|
579
|
+
config["substrate"]["active_account"] = account_name
|
580
|
+
return save_config(config)
|
581
|
+
|
582
|
+
|
583
|
+
def list_accounts() -> Dict[str, Dict[str, Any]]:
|
584
|
+
"""
|
585
|
+
Get a list of all stored accounts.
|
586
|
+
|
587
|
+
Returns:
|
588
|
+
Dict[str, Dict[str, Any]]: Dictionary of account names to account data
|
589
|
+
"""
|
590
|
+
config = load_config()
|
591
|
+
accounts = config["substrate"].get("accounts", {})
|
592
|
+
|
593
|
+
# Mark the active account
|
594
|
+
active_account = config["substrate"].get("active_account")
|
595
|
+
if active_account and active_account in accounts:
|
596
|
+
accounts[active_account]["is_active"] = True
|
597
|
+
|
598
|
+
return accounts
|
599
|
+
|
600
|
+
|
601
|
+
def delete_account(account_name: str) -> bool:
|
602
|
+
"""
|
603
|
+
Delete an account by name.
|
604
|
+
|
605
|
+
Args:
|
606
|
+
account_name: Name of the account to delete
|
607
|
+
|
608
|
+
Returns:
|
609
|
+
bool: True if successful, False otherwise
|
610
|
+
"""
|
611
|
+
config = load_config()
|
612
|
+
|
613
|
+
# Check if the account exists
|
614
|
+
if account_name not in config["substrate"].get("accounts", {}):
|
615
|
+
print(f"Error: Account '{account_name}' not found")
|
616
|
+
return False
|
617
|
+
|
618
|
+
# Delete the account
|
619
|
+
del config["substrate"]["accounts"][account_name]
|
620
|
+
|
621
|
+
# Update active account if needed
|
622
|
+
if config["substrate"].get("active_account") == account_name:
|
623
|
+
if config["substrate"]["accounts"]:
|
624
|
+
# Set the first remaining account as active
|
625
|
+
config["substrate"]["active_account"] = next(
|
626
|
+
iter(config["substrate"]["accounts"])
|
627
|
+
)
|
628
|
+
else:
|
629
|
+
# No more accounts
|
630
|
+
config["substrate"]["active_account"] = None
|
631
|
+
|
632
|
+
return save_config(config)
|
633
|
+
|
634
|
+
|
635
|
+
def get_account_address(account_name: Optional[str] = None) -> Optional[str]:
|
636
|
+
"""
|
637
|
+
Get the SS58 address for an account.
|
638
|
+
|
639
|
+
Args:
|
640
|
+
account_name: Optional name of the account (if None, uses active account or legacy mode)
|
641
|
+
|
642
|
+
Returns:
|
643
|
+
Optional[str]: The SS58 address, or None if not available
|
644
|
+
"""
|
645
|
+
config = load_config()
|
646
|
+
|
647
|
+
# Determine if we're using multi-account mode
|
648
|
+
if account_name is not None or config["substrate"].get("active_account"):
|
649
|
+
# Multi-account mode
|
650
|
+
name_to_use = account_name or config["substrate"].get("active_account")
|
651
|
+
|
652
|
+
if not name_to_use:
|
653
|
+
print("Error: No account specified and no active account")
|
654
|
+
return None
|
655
|
+
|
656
|
+
if name_to_use not in config["substrate"].get("accounts", {}):
|
657
|
+
print(f"Error: Account '{name_to_use}' not found")
|
658
|
+
return None
|
659
|
+
|
660
|
+
return config["substrate"]["accounts"][name_to_use].get("ss58_address")
|
661
|
+
else:
|
662
|
+
# Legacy mode - single account
|
663
|
+
return config["substrate"].get("ss58_address")
|
664
|
+
|
665
|
+
|
666
|
+
def initialize_from_env() -> None:
|
667
|
+
"""
|
668
|
+
Initialize configuration from environment variables.
|
669
|
+
|
670
|
+
This is useful for maintaining backward compatibility with .env files.
|
671
|
+
"""
|
672
|
+
# Load dotenv first to get environment variables
|
673
|
+
try:
|
674
|
+
from dotenv import load_dotenv
|
675
|
+
|
676
|
+
load_dotenv()
|
677
|
+
except ImportError:
|
678
|
+
pass
|
679
|
+
|
680
|
+
config = load_config()
|
681
|
+
changed = False
|
682
|
+
|
683
|
+
# IPFS settings
|
684
|
+
if os.getenv("IPFS_GATEWAY"):
|
685
|
+
config["ipfs"]["gateway"] = os.getenv("IPFS_GATEWAY")
|
686
|
+
changed = True
|
687
|
+
|
688
|
+
if os.getenv("IPFS_API_URL"):
|
689
|
+
config["ipfs"]["api_url"] = os.getenv("IPFS_API_URL")
|
690
|
+
changed = True
|
691
|
+
|
692
|
+
# Substrate settings
|
693
|
+
if os.getenv("SUBSTRATE_URL"):
|
694
|
+
config["substrate"]["url"] = os.getenv("SUBSTRATE_URL")
|
695
|
+
changed = True
|
696
|
+
|
697
|
+
if os.getenv("SUBSTRATE_SEED_PHRASE"):
|
698
|
+
# Don't encrypt from env variables by default
|
699
|
+
config["substrate"]["seed_phrase"] = os.getenv("SUBSTRATE_SEED_PHRASE")
|
700
|
+
config["substrate"]["seed_phrase_encoded"] = False
|
701
|
+
config["substrate"]["seed_phrase_salt"] = None
|
702
|
+
changed = True
|
703
|
+
|
704
|
+
if os.getenv("SUBSTRATE_DEFAULT_MINERS"):
|
705
|
+
miners = os.getenv("SUBSTRATE_DEFAULT_MINERS").split(",")
|
706
|
+
config["substrate"]["default_miners"] = [m.strip() for m in miners if m.strip()]
|
707
|
+
changed = True
|
708
|
+
|
709
|
+
# Encryption settings
|
710
|
+
if os.getenv("HIPPIUS_ENCRYPTION_KEY"):
|
711
|
+
config["encryption"]["encryption_key"] = os.getenv("HIPPIUS_ENCRYPTION_KEY")
|
712
|
+
changed = True
|
713
|
+
|
714
|
+
if os.getenv("HIPPIUS_ENCRYPT_BY_DEFAULT"):
|
715
|
+
value = os.getenv("HIPPIUS_ENCRYPT_BY_DEFAULT").lower()
|
716
|
+
config["encryption"]["encrypt_by_default"] = value in ("true", "1", "yes")
|
717
|
+
changed = True
|
718
|
+
|
719
|
+
if changed:
|
720
|
+
save_config(config)
|
721
|
+
|
722
|
+
|
723
|
+
def get_all_config() -> Dict[str, Any]:
|
724
|
+
"""
|
725
|
+
Get the complete configuration.
|
726
|
+
|
727
|
+
Returns:
|
728
|
+
Dict[str, Any]: The full configuration dictionary
|
729
|
+
"""
|
730
|
+
return load_config()
|
731
|
+
|
732
|
+
|
733
|
+
def reset_config() -> bool:
|
734
|
+
"""
|
735
|
+
Reset configuration to default values.
|
736
|
+
|
737
|
+
Returns:
|
738
|
+
bool: True if reset was successful, False otherwise
|
739
|
+
"""
|
740
|
+
return save_config(DEFAULT_CONFIG.copy())
|
741
|
+
|
742
|
+
|
743
|
+
# Initialize configuration on module import
|
744
|
+
ensure_config_dir()
|