Module nempy.ui
Expand source code
import abc
import binascii
import configparser
import logging
import os
import random
from enum import Enum
from hashlib import blake2b
from typing import Optional, Dict, Tuple, Union, List, Type
import bcrypt
import inquirer
import stdiomask
from bip_utils import Bip39MnemonicGenerator, Bip39Languages
from nempy.config import C
from nempy.sym import network
from nempy.sym.constants import NetworkType, TransactionStatus
from nempy.sym.network import TransactionResponse
from nempy.user_data import AccountData, GenerationType, UserData
from nempy.user_data import ProfileData
from password_strength import PasswordPolicy
logger = logging.getLogger(__name__)
class PasswordPolicyError(Exception):
pass
class RepeatPasswordError(Exception):
pass
class UDTypes(Enum):
PROFILE = 'profile'
ACCOUNT = 'account'
def print_warning():
print(f""" {C.ORANGE}
!!! Important !!!
Save the mnemonic, it will be needed to restore access to the wallet in case of password loss
Where to store can be found here - https://en.bitcoinwiki.org/wiki/Mnemonic_phrase
!!!Do not share your secret key and mnemonic with anyone, it guarantees access to your funds!!!
!!!{C.END}
""")
class UD:
cls: Type[UserData]
type_ud: UDTypes
ud_dir: str
config = configparser.ConfigParser()
def __init__(self, config_file: str, profiles_dir: Optional[str], accounts_dir: Optional[str]):
self.config_file = config_file
if self.type_ud == UDTypes.PROFILE:
self.ud_dir = profiles_dir
self.cls = ProfileData
elif self.type_ud == UDTypes.ACCOUNT:
self.ud_dir = accounts_dir
self.cls = AccountData
@property
def user_data(self) -> Optional[UserData]:
self.config.read(self.config_file)
ud_name = self.config[self.type_ud.value]['default']
uds = self.load_uds()
ud = uds.get(ud_name)
return ud
def load_uds(self) -> Dict[str, UserData]:
uds = {}
profiles_paths = os.listdir(self.ud_dir)
for pp in profiles_paths:
path = os.path.join(self.ud_dir, pp)
ud = self.cls.read(path)
uds[os.path.splitext(pp)[0]] = ud
return uds
def set_default_ud(self, ud: UserData):
self.config.read(self.config_file)
self.config[self.type_ud.value]['default'] = ud.name
with open(self.config_file, 'w') as configfile:
self.config.write(configfile)
def get_default_ud_name(self) -> str:
self.config.read(self.config_file)
ud_name = self.config[self.type_ud.value]['default']
return ud_name
class AccountI(UD):
def __init__(self, config_file: str, accounts_dir: str):
self.type_ud = UDTypes.ACCOUNT
super().__init__(config_file, None, accounts_dir)
@property
def data(self) -> Optional[AccountData]:
return self.user_data
def load_accounts(self) -> Dict[str, AccountData]:
return self.load_uds()
def set_default_account(self, account: AccountData):
self.set_default_ud(account)
class ProfileI(UD):
def __init__(self, config_file: str, profiles_dir: str, accounts_dir: str):
self.type_ud = UDTypes.PROFILE
self.account_i = AccountI(config_file, accounts_dir)
super().__init__(config_file, profiles_dir, None)
accounts_data = self.load_accounts()
if (self.account.data is None and accounts_data) \
or (self.account.data is not None and self.account.data.name not in accounts_data):
account_data = AccountUI.ui_default_account(accounts_data)
self.set_default_account(account_data)
@property
def data(self) -> Optional[ProfileData]:
return self.user_data
def load_profiles(self) -> Dict[str, ProfileData]:
return self.load_uds()
def set_default_profile(self, profile: ProfileData):
self.set_default_ud(profile)
@property
def account(self) -> AccountI:
return self.account_i
def load_accounts(self) -> Dict[str, AccountData]:
accounts_data = self.account_i.load_accounts()
accounts_data = {key: account for key, account in accounts_data.items() if account.profile == self.data.name}
return accounts_data
def set_default_account(self, account: AccountData):
self.account_i.set_default_account(account)
class AccountUI(AccountI):
@staticmethod
def ui_init_general_params(network_type: NetworkType, accounts_dir: str, is_default: bool = False) -> Tuple[str, str, int, bool]:
while True:
name = input('Enter the account name: ')
if name != '':
account_path = os.path.join(accounts_dir, name + '.account')
if os.path.exists(account_path):
print('An account with the same name already exists, please select a different name')
continue
break
print('The name cannot be empty.')
if not is_default:
answer = input(f'Set `{name}` account as default? [Y/n]: ') or 'y'
if answer.lower() == 'y':
is_default = True
if network_type == NetworkType.MAIN_NET:
bip32_coin_id = 4343
elif network_type == NetworkType.TEST_NET:
bip32_coin_id = 1
else:
raise ValueError('Invalid URL or network not supported')
return account_path, name, bip32_coin_id, is_default
@staticmethod
def iu_create_account(profile_data: ProfileData,
accounts_dir: str,
is_default: bool = False,
is_import: bool = False) -> Tuple[Optional[AccountData], bool]:
account_path, name, bip32_coin_id, is_default = AccountUI.ui_init_general_params(profile_data.network_type, accounts_dir, is_default)
password = ProfileUI.ui_check_pass(profile_data, attempts=3)
if password is not None:
if is_import:
gen_type = AccountUI.ui_generation_type_inquirer()
if gen_type == GenerationType.MNEMONIC:
account_data = AccountUI.ui_account_by_mnemonic(profile_data.network_type, bip32_coin_id, is_import=True)
elif gen_type == GenerationType.PRIVATE_KEY:
raise NotImplementedError(
'The functionality of building an account from a private key is not implemented')
else:
raise NotImplementedError(
f'The functionality of building an account from a {gen_type.name} key is not implemented')
else:
account_data = AccountUI.ui_account_by_mnemonic(profile_data.network_type, bip32_coin_id, is_import=is_import)
account_data.name = name
account_data.profile = profile_data.name
AccountUI.save_and_check(account_data, account_path, password)
return account_data, is_default
else:
return None, is_default
@staticmethod
def save_and_check(account_data: AccountData, account_path: str, password: str):
account_data.encrypt(password).write(account_path)
print(f'\nAccount created at: {account_path}')
# checking the ability to read and display information about the account
account_data = AccountData.read(account_path).decrypt(password)
print(account_data)
print_warning()
return account_data
@staticmethod
def ui_default_account(accounts: Dict[str, AccountData]):
questions = [inquirer.List("name", message="Select default account", choices=accounts.keys(), ), ]
answers = inquirer.prompt(questions)
if answers is None:
exit(1)
account = accounts[answers['name']]
return account # -> set_default_account(account)
@staticmethod
def ui_generation_type_inquirer() -> GenerationType:
questions = [inquirer.List("type", message="Select an import type?", choices=["Mnemonic", "Private Key"], ), ]
answers = inquirer.prompt(questions)
import_type = answers['type']
if import_type == 'Private Key':
return GenerationType.PRIVATE_KEY
return GenerationType.MNEMONIC
@staticmethod
def ui_history_inquirer(address: str = None, page_size: int = 10):
conf_transactions: List[TransactionResponse] = network.search_transactions(address=address,
page_size=page_size,
transaction_status=TransactionStatus.CONFIRMED_ADDED)
unconf_transactions: List[TransactionResponse] = network.search_transactions(address=address,
page_size=page_size,
transaction_status=TransactionStatus.UNCONFIRMED_ADDED)
transactions = unconf_transactions + conf_transactions
short_names = {}
for transaction in transactions:
is_mosaic = '∴' if len(transaction.transaction.mosaics) > 1 else ''
message = '🖂' if transaction.transaction.message is not None else ' '
direction = '+' if transaction.transaction.recipientAddress == address else '−'
status = '🗸' if transaction.status == TransactionStatus.CONFIRMED_ADDED.value else '?'
mosaic = next(iter(transaction.transaction.mosaics), '')
short_name = f'{status} {transaction.transaction.recipientAddress} | {transaction.meta.height} | {transaction.transaction.deadline} |{message} |{direction}{mosaic} {is_mosaic}'
short_names[short_name] = transaction
_short_names = list(short_names.keys())
_short_names.append('Exit')
while True:
questions = [
inquirer.List(
"transaction",
message="Select an transaction?",
choices=_short_names,
carousel=True
),
]
answers = inquirer.prompt(questions)
transaction = answers['transaction']
if transaction == 'Exit':
exit(0)
print(short_names[transaction])
@staticmethod
def ui_keyprint_entropy():
random_char_set = ''
print('Write something (random character set), the input will be interrupted automatically')
attempts = list(range(random.randint(3, 5)))
for i in attempts:
something = input(f'Something else ({len(attempts) - i}): ')
if not something:
print('Only a non-empty line will have to be repeated :(')
attempts.append(len(attempts))
continue
random_char_set += something
return random_char_set
@staticmethod
def ui_account_inquirer(accounts_names):
addresses = [account for account in accounts_names]
questions = [inquirer.List("address", message="Select an import type?", choices=addresses,), ]
answers = inquirer.prompt(questions)
account_name = answers['address']
return account_name
@classmethod
def ui_account_by_mnemonic(cls, network_type: NetworkType, bip32_coin_id: int, is_import: bool = False) -> 'AccountData':
if is_import:
mnemonic = stdiomask.getpass('Enter a mnemonic passphrase. Words must be separated by spaces: ')
else:
random_char_set = cls.ui_keyprint_entropy()
entropy_bytes_hex = blake2b(random_char_set.encode(), digest_size=32).hexdigest().encode()
mnemonic = Bip39MnemonicGenerator(Bip39Languages.ENGLISH).FromEntropy(binascii.unhexlify(entropy_bytes_hex))
accounts = AccountData.accounts_pool_by_mnemonic(network_type, bip32_coin_id, mnemonic)
account_name = cls.ui_account_inquirer(accounts.keys())
return accounts[account_name]
class ProfileUI(ProfileI):
@staticmethod
def ui_default_profile(profiles: Dict[str, ProfileData]) -> ProfileData:
names = {profile.name + f' [{profile.network_type.name}]': profile.name for profile in profiles.values()}
questions = [inquirer.List("name", message="Select default profile", choices=names.keys(),), ]
answers = inquirer.prompt(questions)
if answers is None:
exit(1)
name = names[answers['name']]
profile_data = profiles[name]
network.node_selector.network_type = profile_data.network_type
return profile_data # -> set_default_profile(profile)
@classmethod
def ui_create_profile(cls, profiles_dir: str, config_file: str) -> Tuple[ProfileData, str]:
name, path = cls.ui_profile_name(profiles_dir)
network_type = cls.ui_network_type()
new_pass = cls.ui_new_pass(10)
pass_hash = bcrypt.hashpw(new_pass.encode('utf-8'), bcrypt.gensalt(12))
accounts_dir = os.path.join(os.path.dirname(profiles_dir), 'accounts')
profile = ProfileData(name=name,
network_type=network_type,
pass_hash=pass_hash,
accounts_dir=accounts_dir,
config_file=config_file)
return profile, path
@staticmethod
def ui_check_pass(profile_data: ProfileData, attempts: int = 1) -> Optional[str]:
"""
Verifies the password from the profile
:return: password or None if password is failed
"""
for i in range(attempts):
password = stdiomask.getpass(f'({attempts - i}) Enter your `{profile_data.name} [{profile_data.network_type.name}]` profile password: ')
if bcrypt.checkpw(password.encode('utf-8'), profile_data.pass_hash):
return password
if i != attempts - 1:
print(f'Incorrect password. Try again)')
logger.error('Incorrect password')
return None
@staticmethod
def ui_is_default(name):
answer = input(f'Set `{name}` profile as default? [Y/n]: ') or 'y'
if answer.lower() == 'y':
return True
return False
@staticmethod
def ui_profile_name(profiles_dir, attempts: int = 5):
name = None
for i in range(attempts):
name = input(f'({attempts - i}) Enter profile name: ')
if '.' in name:
print('Dot is not a valid character for filename')
continue
path = os.path.join(profiles_dir, f'{name}.profile')
if name == '':
print('The name cannot be empty')
elif os.path.exists(path):
print(f'A profile named `{name}` already exists')
else:
return name, path
raise ValueError(f'Incorrect name for new profile - `{name}`')
@staticmethod
def ui_network_type() -> NetworkType:
questions = [inquirer.List("type", message="Select an network type?", choices=["TEST_NET", "MAIN_NET"],), ]
answers = inquirer.prompt(questions)
network_type = answers['type']
if network_type == 'MAIN_NET':
network_type = NetworkType.MAIN_NET
elif network_type == 'TEST_NET':
network_type = NetworkType.TEST_NET
else:
raise TypeError('Unknown network type')
return network_type
@staticmethod
def ui_new_pass(n_attempts: int):
policy = PasswordPolicy.from_names(
length=8, # min length: 8
uppercase=1, # need min. 1 uppercase letters
numbers=2, # need min. 2 digits
special=1, # need min. 1 special characters
nonletters=2, # need min. 2 non-letter characters (digits, specials, anything)
)
new_password = None
not_in_policies = True
for i in range(n_attempts):
new_password = stdiomask.getpass(f'Enter your new account password {policy.test("")}: ')
not_in_policies = policy.test(new_password)
if not_in_policies:
print(not_in_policies)
else:
break
if not_in_policies:
raise PasswordPolicyError(not_in_policies)
return ProfileUI.ui_repeat_password(n_attempts, new_password)
@staticmethod
def ui_repeat_password(n_attempts: int, password):
for i in range(n_attempts):
repeat_password = stdiomask.getpass(f'Repeat password for confirmation: ')
if repeat_password != password:
print(f'Try again, attempts left {n_attempts - i}')
else:
return password
raise RepeatPasswordError('Failed to confirm password on re-entry')
Functions
def print_warning()-
Expand source code
def print_warning(): print(f""" {C.ORANGE} !!! Important !!! Save the mnemonic, it will be needed to restore access to the wallet in case of password loss Where to store can be found here - https://en.bitcoinwiki.org/wiki/Mnemonic_phrase !!!Do not share your secret key and mnemonic with anyone, it guarantees access to your funds!!! !!!{C.END} """)
Classes
class AccountI (config_file: str, accounts_dir: str)-
Expand source code
class AccountI(UD): def __init__(self, config_file: str, accounts_dir: str): self.type_ud = UDTypes.ACCOUNT super().__init__(config_file, None, accounts_dir) @property def data(self) -> Optional[AccountData]: return self.user_data def load_accounts(self) -> Dict[str, AccountData]: return self.load_uds() def set_default_account(self, account: AccountData): self.set_default_ud(account)Ancestors
Subclasses
Class variables
var cls : Type[UserData]var type_ud : UDTypesvar ud_dir : str
Instance variables
var data : Optional[AccountData]-
Expand source code
@property def data(self) -> Optional[AccountData]: return self.user_data
Methods
def load_accounts(self) ‑> Dict[str, AccountData]-
Expand source code
def load_accounts(self) -> Dict[str, AccountData]: return self.load_uds() def set_default_account(self, account: AccountData)-
Expand source code
def set_default_account(self, account: AccountData): self.set_default_ud(account)
class AccountUI (config_file: str, accounts_dir: str)-
Expand source code
class AccountUI(AccountI): @staticmethod def ui_init_general_params(network_type: NetworkType, accounts_dir: str, is_default: bool = False) -> Tuple[str, str, int, bool]: while True: name = input('Enter the account name: ') if name != '': account_path = os.path.join(accounts_dir, name + '.account') if os.path.exists(account_path): print('An account with the same name already exists, please select a different name') continue break print('The name cannot be empty.') if not is_default: answer = input(f'Set `{name}` account as default? [Y/n]: ') or 'y' if answer.lower() == 'y': is_default = True if network_type == NetworkType.MAIN_NET: bip32_coin_id = 4343 elif network_type == NetworkType.TEST_NET: bip32_coin_id = 1 else: raise ValueError('Invalid URL or network not supported') return account_path, name, bip32_coin_id, is_default @staticmethod def iu_create_account(profile_data: ProfileData, accounts_dir: str, is_default: bool = False, is_import: bool = False) -> Tuple[Optional[AccountData], bool]: account_path, name, bip32_coin_id, is_default = AccountUI.ui_init_general_params(profile_data.network_type, accounts_dir, is_default) password = ProfileUI.ui_check_pass(profile_data, attempts=3) if password is not None: if is_import: gen_type = AccountUI.ui_generation_type_inquirer() if gen_type == GenerationType.MNEMONIC: account_data = AccountUI.ui_account_by_mnemonic(profile_data.network_type, bip32_coin_id, is_import=True) elif gen_type == GenerationType.PRIVATE_KEY: raise NotImplementedError( 'The functionality of building an account from a private key is not implemented') else: raise NotImplementedError( f'The functionality of building an account from a {gen_type.name} key is not implemented') else: account_data = AccountUI.ui_account_by_mnemonic(profile_data.network_type, bip32_coin_id, is_import=is_import) account_data.name = name account_data.profile = profile_data.name AccountUI.save_and_check(account_data, account_path, password) return account_data, is_default else: return None, is_default @staticmethod def save_and_check(account_data: AccountData, account_path: str, password: str): account_data.encrypt(password).write(account_path) print(f'\nAccount created at: {account_path}') # checking the ability to read and display information about the account account_data = AccountData.read(account_path).decrypt(password) print(account_data) print_warning() return account_data @staticmethod def ui_default_account(accounts: Dict[str, AccountData]): questions = [inquirer.List("name", message="Select default account", choices=accounts.keys(), ), ] answers = inquirer.prompt(questions) if answers is None: exit(1) account = accounts[answers['name']] return account # -> set_default_account(account) @staticmethod def ui_generation_type_inquirer() -> GenerationType: questions = [inquirer.List("type", message="Select an import type?", choices=["Mnemonic", "Private Key"], ), ] answers = inquirer.prompt(questions) import_type = answers['type'] if import_type == 'Private Key': return GenerationType.PRIVATE_KEY return GenerationType.MNEMONIC @staticmethod def ui_history_inquirer(address: str = None, page_size: int = 10): conf_transactions: List[TransactionResponse] = network.search_transactions(address=address, page_size=page_size, transaction_status=TransactionStatus.CONFIRMED_ADDED) unconf_transactions: List[TransactionResponse] = network.search_transactions(address=address, page_size=page_size, transaction_status=TransactionStatus.UNCONFIRMED_ADDED) transactions = unconf_transactions + conf_transactions short_names = {} for transaction in transactions: is_mosaic = '∴' if len(transaction.transaction.mosaics) > 1 else '' message = '🖂' if transaction.transaction.message is not None else ' ' direction = '+' if transaction.transaction.recipientAddress == address else '−' status = '🗸' if transaction.status == TransactionStatus.CONFIRMED_ADDED.value else '?' mosaic = next(iter(transaction.transaction.mosaics), '') short_name = f'{status} {transaction.transaction.recipientAddress} | {transaction.meta.height} | {transaction.transaction.deadline} |{message} |{direction}{mosaic} {is_mosaic}' short_names[short_name] = transaction _short_names = list(short_names.keys()) _short_names.append('Exit') while True: questions = [ inquirer.List( "transaction", message="Select an transaction?", choices=_short_names, carousel=True ), ] answers = inquirer.prompt(questions) transaction = answers['transaction'] if transaction == 'Exit': exit(0) print(short_names[transaction]) @staticmethod def ui_keyprint_entropy(): random_char_set = '' print('Write something (random character set), the input will be interrupted automatically') attempts = list(range(random.randint(3, 5))) for i in attempts: something = input(f'Something else ({len(attempts) - i}): ') if not something: print('Only a non-empty line will have to be repeated :(') attempts.append(len(attempts)) continue random_char_set += something return random_char_set @staticmethod def ui_account_inquirer(accounts_names): addresses = [account for account in accounts_names] questions = [inquirer.List("address", message="Select an import type?", choices=addresses,), ] answers = inquirer.prompt(questions) account_name = answers['address'] return account_name @classmethod def ui_account_by_mnemonic(cls, network_type: NetworkType, bip32_coin_id: int, is_import: bool = False) -> 'AccountData': if is_import: mnemonic = stdiomask.getpass('Enter a mnemonic passphrase. Words must be separated by spaces: ') else: random_char_set = cls.ui_keyprint_entropy() entropy_bytes_hex = blake2b(random_char_set.encode(), digest_size=32).hexdigest().encode() mnemonic = Bip39MnemonicGenerator(Bip39Languages.ENGLISH).FromEntropy(binascii.unhexlify(entropy_bytes_hex)) accounts = AccountData.accounts_pool_by_mnemonic(network_type, bip32_coin_id, mnemonic) account_name = cls.ui_account_inquirer(accounts.keys()) return accounts[account_name]Ancestors
Class variables
var cls : Type[UserData]var type_ud : UDTypesvar ud_dir : str
Static methods
def iu_create_account(profile_data: ProfileData, accounts_dir: str, is_default: bool = False, is_import: bool = False) ‑> Tuple[Optional[AccountData], bool]-
Expand source code
@staticmethod def iu_create_account(profile_data: ProfileData, accounts_dir: str, is_default: bool = False, is_import: bool = False) -> Tuple[Optional[AccountData], bool]: account_path, name, bip32_coin_id, is_default = AccountUI.ui_init_general_params(profile_data.network_type, accounts_dir, is_default) password = ProfileUI.ui_check_pass(profile_data, attempts=3) if password is not None: if is_import: gen_type = AccountUI.ui_generation_type_inquirer() if gen_type == GenerationType.MNEMONIC: account_data = AccountUI.ui_account_by_mnemonic(profile_data.network_type, bip32_coin_id, is_import=True) elif gen_type == GenerationType.PRIVATE_KEY: raise NotImplementedError( 'The functionality of building an account from a private key is not implemented') else: raise NotImplementedError( f'The functionality of building an account from a {gen_type.name} key is not implemented') else: account_data = AccountUI.ui_account_by_mnemonic(profile_data.network_type, bip32_coin_id, is_import=is_import) account_data.name = name account_data.profile = profile_data.name AccountUI.save_and_check(account_data, account_path, password) return account_data, is_default else: return None, is_default def save_and_check(account_data: AccountData, account_path: str, password: str)-
Expand source code
@staticmethod def save_and_check(account_data: AccountData, account_path: str, password: str): account_data.encrypt(password).write(account_path) print(f'\nAccount created at: {account_path}') # checking the ability to read and display information about the account account_data = AccountData.read(account_path).decrypt(password) print(account_data) print_warning() return account_data def ui_account_by_mnemonic(network_type: NetworkType, bip32_coin_id: int, is_import: bool = False) ‑> AccountData-
Expand source code
@classmethod def ui_account_by_mnemonic(cls, network_type: NetworkType, bip32_coin_id: int, is_import: bool = False) -> 'AccountData': if is_import: mnemonic = stdiomask.getpass('Enter a mnemonic passphrase. Words must be separated by spaces: ') else: random_char_set = cls.ui_keyprint_entropy() entropy_bytes_hex = blake2b(random_char_set.encode(), digest_size=32).hexdigest().encode() mnemonic = Bip39MnemonicGenerator(Bip39Languages.ENGLISH).FromEntropy(binascii.unhexlify(entropy_bytes_hex)) accounts = AccountData.accounts_pool_by_mnemonic(network_type, bip32_coin_id, mnemonic) account_name = cls.ui_account_inquirer(accounts.keys()) return accounts[account_name] def ui_account_inquirer(accounts_names)-
Expand source code
@staticmethod def ui_account_inquirer(accounts_names): addresses = [account for account in accounts_names] questions = [inquirer.List("address", message="Select an import type?", choices=addresses,), ] answers = inquirer.prompt(questions) account_name = answers['address'] return account_name def ui_default_account(accounts: Dict[str, AccountData])-
Expand source code
@staticmethod def ui_default_account(accounts: Dict[str, AccountData]): questions = [inquirer.List("name", message="Select default account", choices=accounts.keys(), ), ] answers = inquirer.prompt(questions) if answers is None: exit(1) account = accounts[answers['name']] return account # -> set_default_account(account) def ui_generation_type_inquirer() ‑> GenerationType-
Expand source code
@staticmethod def ui_generation_type_inquirer() -> GenerationType: questions = [inquirer.List("type", message="Select an import type?", choices=["Mnemonic", "Private Key"], ), ] answers = inquirer.prompt(questions) import_type = answers['type'] if import_type == 'Private Key': return GenerationType.PRIVATE_KEY return GenerationType.MNEMONIC def ui_history_inquirer(address: str = None, page_size: int = 10)-
Expand source code
@staticmethod def ui_history_inquirer(address: str = None, page_size: int = 10): conf_transactions: List[TransactionResponse] = network.search_transactions(address=address, page_size=page_size, transaction_status=TransactionStatus.CONFIRMED_ADDED) unconf_transactions: List[TransactionResponse] = network.search_transactions(address=address, page_size=page_size, transaction_status=TransactionStatus.UNCONFIRMED_ADDED) transactions = unconf_transactions + conf_transactions short_names = {} for transaction in transactions: is_mosaic = '∴' if len(transaction.transaction.mosaics) > 1 else '' message = '🖂' if transaction.transaction.message is not None else ' ' direction = '+' if transaction.transaction.recipientAddress == address else '−' status = '🗸' if transaction.status == TransactionStatus.CONFIRMED_ADDED.value else '?' mosaic = next(iter(transaction.transaction.mosaics), '') short_name = f'{status} {transaction.transaction.recipientAddress} | {transaction.meta.height} | {transaction.transaction.deadline} |{message} |{direction}{mosaic} {is_mosaic}' short_names[short_name] = transaction _short_names = list(short_names.keys()) _short_names.append('Exit') while True: questions = [ inquirer.List( "transaction", message="Select an transaction?", choices=_short_names, carousel=True ), ] answers = inquirer.prompt(questions) transaction = answers['transaction'] if transaction == 'Exit': exit(0) print(short_names[transaction]) def ui_init_general_params(network_type: NetworkType, accounts_dir: str, is_default: bool = False) ‑> Tuple[str, str, int, bool]-
Expand source code
@staticmethod def ui_init_general_params(network_type: NetworkType, accounts_dir: str, is_default: bool = False) -> Tuple[str, str, int, bool]: while True: name = input('Enter the account name: ') if name != '': account_path = os.path.join(accounts_dir, name + '.account') if os.path.exists(account_path): print('An account with the same name already exists, please select a different name') continue break print('The name cannot be empty.') if not is_default: answer = input(f'Set `{name}` account as default? [Y/n]: ') or 'y' if answer.lower() == 'y': is_default = True if network_type == NetworkType.MAIN_NET: bip32_coin_id = 4343 elif network_type == NetworkType.TEST_NET: bip32_coin_id = 1 else: raise ValueError('Invalid URL or network not supported') return account_path, name, bip32_coin_id, is_default def ui_keyprint_entropy()-
Expand source code
@staticmethod def ui_keyprint_entropy(): random_char_set = '' print('Write something (random character set), the input will be interrupted automatically') attempts = list(range(random.randint(3, 5))) for i in attempts: something = input(f'Something else ({len(attempts) - i}): ') if not something: print('Only a non-empty line will have to be repeated :(') attempts.append(len(attempts)) continue random_char_set += something return random_char_set
class PasswordPolicyError (*args, **kwargs)-
Common base class for all non-exit exceptions.
Expand source code
class PasswordPolicyError(Exception): passAncestors
- builtins.Exception
- builtins.BaseException
class ProfileI (config_file: str, profiles_dir: str, accounts_dir: str)-
Expand source code
class ProfileI(UD): def __init__(self, config_file: str, profiles_dir: str, accounts_dir: str): self.type_ud = UDTypes.PROFILE self.account_i = AccountI(config_file, accounts_dir) super().__init__(config_file, profiles_dir, None) accounts_data = self.load_accounts() if (self.account.data is None and accounts_data) \ or (self.account.data is not None and self.account.data.name not in accounts_data): account_data = AccountUI.ui_default_account(accounts_data) self.set_default_account(account_data) @property def data(self) -> Optional[ProfileData]: return self.user_data def load_profiles(self) -> Dict[str, ProfileData]: return self.load_uds() def set_default_profile(self, profile: ProfileData): self.set_default_ud(profile) @property def account(self) -> AccountI: return self.account_i def load_accounts(self) -> Dict[str, AccountData]: accounts_data = self.account_i.load_accounts() accounts_data = {key: account for key, account in accounts_data.items() if account.profile == self.data.name} return accounts_data def set_default_account(self, account: AccountData): self.account_i.set_default_account(account)Ancestors
Subclasses
Class variables
var cls : Type[UserData]var type_ud : UDTypesvar ud_dir : str
Instance variables
var account : AccountI-
Expand source code
@property def account(self) -> AccountI: return self.account_i var data : Optional[ProfileData]-
Expand source code
@property def data(self) -> Optional[ProfileData]: return self.user_data
Methods
def load_accounts(self) ‑> Dict[str, AccountData]-
Expand source code
def load_accounts(self) -> Dict[str, AccountData]: accounts_data = self.account_i.load_accounts() accounts_data = {key: account for key, account in accounts_data.items() if account.profile == self.data.name} return accounts_data def load_profiles(self) ‑> Dict[str, ProfileData]-
Expand source code
def load_profiles(self) -> Dict[str, ProfileData]: return self.load_uds() def set_default_account(self, account: AccountData)-
Expand source code
def set_default_account(self, account: AccountData): self.account_i.set_default_account(account) def set_default_profile(self, profile: ProfileData)-
Expand source code
def set_default_profile(self, profile: ProfileData): self.set_default_ud(profile)
class ProfileUI (config_file: str, profiles_dir: str, accounts_dir: str)-
Expand source code
class ProfileUI(ProfileI): @staticmethod def ui_default_profile(profiles: Dict[str, ProfileData]) -> ProfileData: names = {profile.name + f' [{profile.network_type.name}]': profile.name for profile in profiles.values()} questions = [inquirer.List("name", message="Select default profile", choices=names.keys(),), ] answers = inquirer.prompt(questions) if answers is None: exit(1) name = names[answers['name']] profile_data = profiles[name] network.node_selector.network_type = profile_data.network_type return profile_data # -> set_default_profile(profile) @classmethod def ui_create_profile(cls, profiles_dir: str, config_file: str) -> Tuple[ProfileData, str]: name, path = cls.ui_profile_name(profiles_dir) network_type = cls.ui_network_type() new_pass = cls.ui_new_pass(10) pass_hash = bcrypt.hashpw(new_pass.encode('utf-8'), bcrypt.gensalt(12)) accounts_dir = os.path.join(os.path.dirname(profiles_dir), 'accounts') profile = ProfileData(name=name, network_type=network_type, pass_hash=pass_hash, accounts_dir=accounts_dir, config_file=config_file) return profile, path @staticmethod def ui_check_pass(profile_data: ProfileData, attempts: int = 1) -> Optional[str]: """ Verifies the password from the profile :return: password or None if password is failed """ for i in range(attempts): password = stdiomask.getpass(f'({attempts - i}) Enter your `{profile_data.name} [{profile_data.network_type.name}]` profile password: ') if bcrypt.checkpw(password.encode('utf-8'), profile_data.pass_hash): return password if i != attempts - 1: print(f'Incorrect password. Try again)') logger.error('Incorrect password') return None @staticmethod def ui_is_default(name): answer = input(f'Set `{name}` profile as default? [Y/n]: ') or 'y' if answer.lower() == 'y': return True return False @staticmethod def ui_profile_name(profiles_dir, attempts: int = 5): name = None for i in range(attempts): name = input(f'({attempts - i}) Enter profile name: ') if '.' in name: print('Dot is not a valid character for filename') continue path = os.path.join(profiles_dir, f'{name}.profile') if name == '': print('The name cannot be empty') elif os.path.exists(path): print(f'A profile named `{name}` already exists') else: return name, path raise ValueError(f'Incorrect name for new profile - `{name}`') @staticmethod def ui_network_type() -> NetworkType: questions = [inquirer.List("type", message="Select an network type?", choices=["TEST_NET", "MAIN_NET"],), ] answers = inquirer.prompt(questions) network_type = answers['type'] if network_type == 'MAIN_NET': network_type = NetworkType.MAIN_NET elif network_type == 'TEST_NET': network_type = NetworkType.TEST_NET else: raise TypeError('Unknown network type') return network_type @staticmethod def ui_new_pass(n_attempts: int): policy = PasswordPolicy.from_names( length=8, # min length: 8 uppercase=1, # need min. 1 uppercase letters numbers=2, # need min. 2 digits special=1, # need min. 1 special characters nonletters=2, # need min. 2 non-letter characters (digits, specials, anything) ) new_password = None not_in_policies = True for i in range(n_attempts): new_password = stdiomask.getpass(f'Enter your new account password {policy.test("")}: ') not_in_policies = policy.test(new_password) if not_in_policies: print(not_in_policies) else: break if not_in_policies: raise PasswordPolicyError(not_in_policies) return ProfileUI.ui_repeat_password(n_attempts, new_password) @staticmethod def ui_repeat_password(n_attempts: int, password): for i in range(n_attempts): repeat_password = stdiomask.getpass(f'Repeat password for confirmation: ') if repeat_password != password: print(f'Try again, attempts left {n_attempts - i}') else: return password raise RepeatPasswordError('Failed to confirm password on re-entry')Ancestors
Class variables
var cls : Type[UserData]var type_ud : UDTypesvar ud_dir : str
Static methods
def ui_check_pass(profile_data: ProfileData, attempts: int = 1) ‑> Optional[str]-
Verifies the password from the profile :return: password or None if password is failed
Expand source code
@staticmethod def ui_check_pass(profile_data: ProfileData, attempts: int = 1) -> Optional[str]: """ Verifies the password from the profile :return: password or None if password is failed """ for i in range(attempts): password = stdiomask.getpass(f'({attempts - i}) Enter your `{profile_data.name} [{profile_data.network_type.name}]` profile password: ') if bcrypt.checkpw(password.encode('utf-8'), profile_data.pass_hash): return password if i != attempts - 1: print(f'Incorrect password. Try again)') logger.error('Incorrect password') return None def ui_create_profile(profiles_dir: str, config_file: str) ‑> Tuple[ProfileData, str]-
Expand source code
@classmethod def ui_create_profile(cls, profiles_dir: str, config_file: str) -> Tuple[ProfileData, str]: name, path = cls.ui_profile_name(profiles_dir) network_type = cls.ui_network_type() new_pass = cls.ui_new_pass(10) pass_hash = bcrypt.hashpw(new_pass.encode('utf-8'), bcrypt.gensalt(12)) accounts_dir = os.path.join(os.path.dirname(profiles_dir), 'accounts') profile = ProfileData(name=name, network_type=network_type, pass_hash=pass_hash, accounts_dir=accounts_dir, config_file=config_file) return profile, path def ui_default_profile(profiles: Dict[str, ProfileData]) ‑> ProfileData-
Expand source code
@staticmethod def ui_default_profile(profiles: Dict[str, ProfileData]) -> ProfileData: names = {profile.name + f' [{profile.network_type.name}]': profile.name for profile in profiles.values()} questions = [inquirer.List("name", message="Select default profile", choices=names.keys(),), ] answers = inquirer.prompt(questions) if answers is None: exit(1) name = names[answers['name']] profile_data = profiles[name] network.node_selector.network_type = profile_data.network_type return profile_data # -> set_default_profile(profile) def ui_is_default(name)-
Expand source code
@staticmethod def ui_is_default(name): answer = input(f'Set `{name}` profile as default? [Y/n]: ') or 'y' if answer.lower() == 'y': return True return False def ui_network_type() ‑> NetworkType-
Expand source code
@staticmethod def ui_network_type() -> NetworkType: questions = [inquirer.List("type", message="Select an network type?", choices=["TEST_NET", "MAIN_NET"],), ] answers = inquirer.prompt(questions) network_type = answers['type'] if network_type == 'MAIN_NET': network_type = NetworkType.MAIN_NET elif network_type == 'TEST_NET': network_type = NetworkType.TEST_NET else: raise TypeError('Unknown network type') return network_type def ui_new_pass(n_attempts: int)-
Expand source code
@staticmethod def ui_new_pass(n_attempts: int): policy = PasswordPolicy.from_names( length=8, # min length: 8 uppercase=1, # need min. 1 uppercase letters numbers=2, # need min. 2 digits special=1, # need min. 1 special characters nonletters=2, # need min. 2 non-letter characters (digits, specials, anything) ) new_password = None not_in_policies = True for i in range(n_attempts): new_password = stdiomask.getpass(f'Enter your new account password {policy.test("")}: ') not_in_policies = policy.test(new_password) if not_in_policies: print(not_in_policies) else: break if not_in_policies: raise PasswordPolicyError(not_in_policies) return ProfileUI.ui_repeat_password(n_attempts, new_password) def ui_profile_name(profiles_dir, attempts: int = 5)-
Expand source code
@staticmethod def ui_profile_name(profiles_dir, attempts: int = 5): name = None for i in range(attempts): name = input(f'({attempts - i}) Enter profile name: ') if '.' in name: print('Dot is not a valid character for filename') continue path = os.path.join(profiles_dir, f'{name}.profile') if name == '': print('The name cannot be empty') elif os.path.exists(path): print(f'A profile named `{name}` already exists') else: return name, path raise ValueError(f'Incorrect name for new profile - `{name}`') def ui_repeat_password(n_attempts: int, password)-
Expand source code
@staticmethod def ui_repeat_password(n_attempts: int, password): for i in range(n_attempts): repeat_password = stdiomask.getpass(f'Repeat password for confirmation: ') if repeat_password != password: print(f'Try again, attempts left {n_attempts - i}') else: return password raise RepeatPasswordError('Failed to confirm password on re-entry')
class RepeatPasswordError (*args, **kwargs)-
Common base class for all non-exit exceptions.
Expand source code
class RepeatPasswordError(Exception): passAncestors
- builtins.Exception
- builtins.BaseException
class UD (config_file: str, profiles_dir: Optional[str], accounts_dir: Optional[str])-
Expand source code
class UD: cls: Type[UserData] type_ud: UDTypes ud_dir: str config = configparser.ConfigParser() def __init__(self, config_file: str, profiles_dir: Optional[str], accounts_dir: Optional[str]): self.config_file = config_file if self.type_ud == UDTypes.PROFILE: self.ud_dir = profiles_dir self.cls = ProfileData elif self.type_ud == UDTypes.ACCOUNT: self.ud_dir = accounts_dir self.cls = AccountData @property def user_data(self) -> Optional[UserData]: self.config.read(self.config_file) ud_name = self.config[self.type_ud.value]['default'] uds = self.load_uds() ud = uds.get(ud_name) return ud def load_uds(self) -> Dict[str, UserData]: uds = {} profiles_paths = os.listdir(self.ud_dir) for pp in profiles_paths: path = os.path.join(self.ud_dir, pp) ud = self.cls.read(path) uds[os.path.splitext(pp)[0]] = ud return uds def set_default_ud(self, ud: UserData): self.config.read(self.config_file) self.config[self.type_ud.value]['default'] = ud.name with open(self.config_file, 'w') as configfile: self.config.write(configfile) def get_default_ud_name(self) -> str: self.config.read(self.config_file) ud_name = self.config[self.type_ud.value]['default'] return ud_nameSubclasses
Class variables
var cls : Type[UserData]var configvar type_ud : UDTypesvar ud_dir : str
Instance variables
var user_data : Optional[UserData]-
Expand source code
@property def user_data(self) -> Optional[UserData]: self.config.read(self.config_file) ud_name = self.config[self.type_ud.value]['default'] uds = self.load_uds() ud = uds.get(ud_name) return ud
Methods
def get_default_ud_name(self) ‑> str-
Expand source code
def get_default_ud_name(self) -> str: self.config.read(self.config_file) ud_name = self.config[self.type_ud.value]['default'] return ud_name def load_uds(self) ‑> Dict[str, UserData]-
Expand source code
def load_uds(self) -> Dict[str, UserData]: uds = {} profiles_paths = os.listdir(self.ud_dir) for pp in profiles_paths: path = os.path.join(self.ud_dir, pp) ud = self.cls.read(path) uds[os.path.splitext(pp)[0]] = ud return uds def set_default_ud(self, ud: UserData)-
Expand source code
def set_default_ud(self, ud: UserData): self.config.read(self.config_file) self.config[self.type_ud.value]['default'] = ud.name with open(self.config_file, 'w') as configfile: self.config.write(configfile)
class UDTypes (value, names=None, *, module=None, qualname=None, type=None, start=1)-
An enumeration.
Expand source code
class UDTypes(Enum): PROFILE = 'profile' ACCOUNT = 'account'Ancestors
- enum.Enum
Class variables
var ACCOUNTvar PROFILE