import json
import logging
from enum import Enum
from pathlib import Path
from typing import Any, Optional, Union
from multiversx_sdk.wallet.crypto import (EncryptedData, Randomness, decryptor,
encryptor)
from multiversx_sdk.wallet.interfaces import IRandomness
from multiversx_sdk.wallet.mnemonic import Mnemonic
from multiversx_sdk.wallet.user_keys import UserPublicKey, UserSecretKey
[docs]
class UserWalletKind(str, Enum):
SECRET_KEY = "secretKey"
MNEMONIC = "mnemonic"
[docs]
class UserWallet:
def __init__(self, kind: str, encrypted_data: EncryptedData, public_key_when_kind_is_secret_key: Optional[UserPublicKey] = None) -> None:
"""
Do not use this constructor directly. Use the static methods from_secret_key() and from_mnemonic() instead.
"""
self.kind = kind
self.encrypted_data = encrypted_data
self.public_key_when_kind_is_secret_key = public_key_when_kind_is_secret_key
[docs]
@classmethod
def from_secret_key(cls, secret_key: UserSecretKey, password: str, randomness: Union[IRandomness, None] = None) -> 'UserWallet':
randomness = randomness or Randomness()
public_key = secret_key.generate_public_key()
data = secret_key.buffer + public_key.buffer
encrypted_data = encryptor.encrypt(data, password, randomness)
return cls(
kind=UserWalletKind.SECRET_KEY.value,
encrypted_data=encrypted_data,
public_key_when_kind_is_secret_key=public_key
)
[docs]
@classmethod
def from_mnemonic(cls, mnemonic: str, password: str, randomness: Union[IRandomness, None] = None) -> 'UserWallet':
randomness = randomness or Randomness()
Mnemonic.assert_text_is_valid(mnemonic)
data = mnemonic.encode()
encrypted_data = encryptor.encrypt(data, password, randomness)
return cls(
kind=UserWalletKind.MNEMONIC.value,
encrypted_data=encrypted_data
)
[docs]
@classmethod
def decrypt_secret_key(cls, keyfile_object: dict[str, Any], password: str) -> UserSecretKey:
# Here, we check the "kind" field only for files that have it. Older keystore files (holding only secret keys) do not have this field.
kind = keyfile_object.get("kind", None)
if kind and kind != UserWalletKind.SECRET_KEY.value:
raise Exception(f"Expected kind to be {UserWalletKind.SECRET_KEY.value}, but it was {kind}")
encrypted_data = EncryptedData.from_keyfile_object(keyfile_object)
buffer = decryptor.decrypt(encrypted_data, password)
buffer = buffer.rjust(32, b'\x00')
seed = buffer[:32]
return UserSecretKey(seed)
[docs]
@classmethod
def decrypt_mnemonic(cls, keyfile_object: dict[str, Any], password: str) -> Mnemonic:
if keyfile_object['kind'] != UserWalletKind.MNEMONIC.value:
raise Exception(f"Expected kind to be {UserWalletKind.MNEMONIC.value}, but it was {keyfile_object['kind']}")
encrypted_data = EncryptedData.from_keyfile_object(keyfile_object)
buffer = decryptor.decrypt(encrypted_data, password)
mnemonic = Mnemonic(buffer.decode())
return mnemonic
[docs]
@classmethod
def load_secret_key(cls, path: Path, password: str, address_index: Optional[int] = None) -> 'UserSecretKey':
"""
Loads a secret key from a keystore file.
:param path: The path to the keystore file.
:param password: The password to decrypt the keystore file.
:param address_index: The index of the address to load. This is only used when the keystore file contains a mnemonic, and the secret key has to be derived from this mnemonic.
"""
key_file_json = path.expanduser().resolve().read_text()
key_file_object = json.loads(key_file_json)
kind = key_file_object.get("kind", UserWalletKind.SECRET_KEY.value)
logging.debug(f"UserWallet.load_secret_key(), kind = {kind}")
if kind == UserWalletKind.SECRET_KEY.value:
if address_index is not None:
raise Exception("address_index must not be provided when kind == 'secretKey'")
secret_key = cls.decrypt_secret_key(key_file_object, password)
elif kind == UserWalletKind.MNEMONIC.value:
mnemonic = cls.decrypt_mnemonic(key_file_object, password)
secret_key = mnemonic.derive_key(address_index or 0)
else:
raise Exception(f"Unknown kind: {kind}")
return secret_key
[docs]
def save(self, path: Path, address_hrp: Optional[str] = None):
path = path.expanduser().resolve()
json_content = self.to_json(address_hrp)
path.write_text(json_content)
[docs]
def to_json(self, address_hrp: Optional[str] = None) -> str:
obj = self.to_dict(address_hrp)
return json.dumps(obj, indent=4)
[docs]
def to_dict(self, address_hrp: Optional[str] = None) -> dict[str, Any]:
if self.kind == UserWalletKind.SECRET_KEY.value:
return self._to_dict_when_kind_is_secret_key(address_hrp)
return self._to_dict_when_kind_is_mnemonic()
def _to_dict_when_kind_is_secret_key(self, address_hrp: Optional[str] = None) -> dict[str, Any]:
if self.public_key_when_kind_is_secret_key is None:
raise Exception("Public key isn't available")
crypto_section = self._get_crypto_section_as_dict()
envelope = {
"version": self.encrypted_data.version,
"kind": self.kind,
"id": self.encrypted_data.id,
"address": self.public_key_when_kind_is_secret_key.hex(),
"bech32": self.public_key_when_kind_is_secret_key.to_address(address_hrp).to_bech32(),
"crypto": crypto_section
}
return envelope
def _to_dict_when_kind_is_mnemonic(self) -> dict[str, Any]:
crypto_section = self._get_crypto_section_as_dict()
envelope = {
"version": self.encrypted_data.version,
"kind": self.kind,
"id": self.encrypted_data.id,
"crypto": crypto_section
}
return envelope
def _get_crypto_section_as_dict(self) -> dict[str, Any]:
return {
"ciphertext": self.encrypted_data.ciphertext,
"cipherparams": {"iv": self.encrypted_data.iv},
"cipher": self.encrypted_data.cipher,
"kdf": self.encrypted_data.kdf,
"kdfparams": {
"dklen": self.encrypted_data.kdfparams.dklen,
"salt": self.encrypted_data.salt,
"n": self.encrypted_data.kdfparams.n,
"r": self.encrypted_data.kdfparams.r,
"p": self.encrypted_data.kdfparams.p
},
"mac": self.encrypted_data.mac
}