electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit d5790ea10994f71d4cd0c01d5a08fe87a1d25b82
parent d6c2a0af946a7691b226a59aa5e3bcdcd6e31fa7
Author: ThomasV <thomasv@electrum.org>
Date:   Sat, 23 Feb 2019 15:59:01 +0100

wizard: do not use on storage object during wallet creation

Diffstat:
Melectrum/base_wizard.py | 97++++++++++++++++++++++++++++++++++++++++---------------------------------------
Melectrum/gui/kivy/main_window.py | 12++++++------
Melectrum/gui/kivy/uix/dialogs/installwizard.py | 7++++---
Melectrum/gui/qt/__init__.py | 20++++++++++++++------
Melectrum/gui/qt/installwizard.py | 73++++++++++++++++++++++++++++++++++++++-----------------------------------
Melectrum/plugins/trustedcoin/qt.py | 3++-
Melectrum/plugins/trustedcoin/trustedcoin.py | 45++++++++++++++++-----------------------------
Melectrum/storage.py | 14--------------
8 files changed, 129 insertions(+), 142 deletions(-)

diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py @@ -25,6 +25,7 @@ import os import sys +import copy import traceback from functools import partial from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any @@ -65,12 +66,12 @@ class WizardStackItem(NamedTuple): class BaseWizard(object): - def __init__(self, config: SimpleConfig, plugins: Plugins, storage: WalletStorage): + def __init__(self, config: SimpleConfig, plugins: Plugins): super(BaseWizard, self).__init__() self.config = config self.plugins = plugins - self.storage = storage - self.wallet = None # type: Abstract_Wallet + self.data = {} + self.pw_args = None self._stack = [] # type: List[WizardStackItem] self.plugin = None self.keystores = [] @@ -83,7 +84,7 @@ class BaseWizard(object): def run(self, *args): action = args[0] args = args[1:] - storage_data = self.storage.get_all_data() + storage_data = copy.deepcopy(self.data) self._stack.append(WizardStackItem(action, args, storage_data)) if not action: return @@ -110,7 +111,7 @@ class BaseWizard(object): stack_item = self._stack.pop() # try to undo side effects since we last entered 'previous' frame # FIXME only self.storage is properly restored - self.storage.overwrite_all_data(stack_item.storage_data) + self.data = copy.deepcopy(stack_item.storage_data) # rerun 'previous' frame self.run(stack_item.action, *stack_item.args) @@ -118,8 +119,7 @@ class BaseWizard(object): self._stack = [] def new(self): - name = os.path.basename(self.storage.path) - title = _("Create") + ' ' + name + title = _("Create new wallet") message = '\n'.join([ _("What kind of wallet do you want to create?") ]) @@ -132,36 +132,35 @@ class BaseWizard(object): choices = [pair for pair in wallet_kinds if pair[0] in wallet_types] self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type) - def upgrade_storage(self): + def upgrade_storage(self, storage): exc = None def on_finished(): if exc is None: - self.wallet = Wallet(self.storage) self.terminate() else: raise exc def do_upgrade(): nonlocal exc try: - self.storage.upgrade() + storage.upgrade() except Exception as e: exc = e self.waiting_dialog(do_upgrade, _('Upgrading wallet format...'), on_finished=on_finished) def load_2fa(self): - self.storage.put('wallet_type', '2fa') - self.storage.put('use_trustedcoin', True) + self.data['wallet_type'] = '2fa' + self.data['use_trustedcoin'] = True self.plugin = self.plugins.load_plugin('trustedcoin') def on_wallet_type(self, choice): - self.wallet_type = choice + self.data['wallet_type'] = self.wallet_type = choice if choice == 'standard': action = 'choose_keystore' elif choice == 'multisig': action = 'choose_multisig' elif choice == '2fa': self.load_2fa() - action = self.storage.get_action() + action = self.plugin.get_action(self.data) elif choice == 'imported': action = 'import_addresses_or_keys' self.run(action) @@ -169,7 +168,7 @@ class BaseWizard(object): def choose_multisig(self): def on_multisig(m, n): multisig_type = "%dof%d" % (m, n) - self.storage.put('wallet_type', multisig_type) + self.data['wallet_type'] = multisig_type self.n = n self.run('choose_keystore') self.multisig_dialog(run_next=on_multisig) @@ -206,27 +205,24 @@ class BaseWizard(object): is_valid=v, allow_multi=True, show_wif_help=True) def on_import(self, text): - # create a temporary wallet and exploit that modifications - # will be reflected on self.storage + # text is already sanitized by is_address_list and is_private_keys_list if keystore.is_address_list(text): - w = Imported_Wallet(self.storage) - addresses = text.split() - good_inputs, bad_inputs = w.import_addresses(addresses, write_to_disk=False) + self.data['addresses'] = {} + for addr in text.split(): + assert bitcoin.is_address(addr) + self.data['addresses'][addr] = {} elif keystore.is_private_key_list(text): + self.data['addresses'] = {} k = keystore.Imported_KeyStore({}) - self.storage.put('keystore', k.dump()) - w = Imported_Wallet(self.storage) keys = keystore.get_private_keys(text) - good_inputs, bad_inputs = w.import_private_keys(keys, None, write_to_disk=False) - self.keystores.append(w.keystore) + for pk in keys: + assert bitcoin.is_private_key(pk) + txin_type, pubkey = k.import_privkey(pk, None) + addr = bitcoin.pubkey_to_address(txin_type, pubkey) + self.data['addresses'][addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None} + self.keystores.append(k) else: return self.terminate() - if bad_inputs: - msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10]) - if len(bad_inputs) > 10: msg += '\n...' - self.show_error(_("The following inputs could not be imported") - + f' ({len(bad_inputs)}):\n' + msg) - # FIXME what if len(good_inputs) == 0 ? return self.run('create_wallet') def restore_from_key(self): @@ -246,7 +242,7 @@ class BaseWizard(object): k = keystore.from_master_key(text) self.on_keystore(k) - def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET): + def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET, storage=None): title = _('Hardware Keystore') # check available plugins supported_plugins = self.plugins.get_hardware_support() @@ -348,7 +344,7 @@ class BaseWizard(object): xpub = self.plugin.get_xpub(device_info.device.id_, derivation, 'standard', self) password = keystore.Xpub.get_pubkey_from_xpub(xpub, ()) try: - self.storage.decrypt(password) + storage.decrypt(password) except InvalidPassword: # try to clear session so that user can type another passphrase devmgr = self.plugins.device_manager @@ -539,32 +535,37 @@ class BaseWizard(object): def on_password(self, password, *, encrypt_storage, storage_enc_version=STO_EV_USER_PW, encrypt_keystore): - assert not self.storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk" - self.storage.set_keystore_encryption(bool(password) and encrypt_keystore) - if encrypt_storage: - self.storage.set_password(password, enc_version=storage_enc_version) for k in self.keystores: if k.may_have_password(): k.update_password(None, password) if self.wallet_type == 'standard': - self.storage.put('seed_type', self.seed_type) + self.data['seed_type'] = self.seed_type keys = self.keystores[0].dump() - self.storage.put('keystore', keys) - self.wallet = Standard_Wallet(self.storage) - self.run('create_addresses') + self.data['keystore'] = keys elif self.wallet_type == 'multisig': for i, k in enumerate(self.keystores): - self.storage.put('x%d/'%(i+1), k.dump()) - self.storage.write() - self.wallet = Multisig_Wallet(self.storage) - self.run('create_addresses') + self.data['x%d/'%(i+1)] = k.dump() elif self.wallet_type == 'imported': if len(self.keystores) > 0: keys = self.keystores[0].dump() - self.storage.put('keystore', keys) - self.wallet = Imported_Wallet(self.storage) - self.wallet.storage.write() - self.terminate() + self.data['keystore'] = keys + else: + raise BaseException('Unknown wallet type') + self.pw_args = password, encrypt_storage, storage_enc_version + self.terminate() + + def create_storage(self, path): + if not self.pw_args: + return + password, encrypt_storage, storage_enc_version = self.pw_args + storage = WalletStorage(path) + for key, value in self.data.items(): + storage.put(key, value) + storage.set_keystore_encryption(bool(password))# and encrypt_keystore) + if encrypt_storage: + storage.set_password(password, enc_version=storage_enc_version) + storage.write() + return storage def show_xpub_and_add_cosigners(self, xpub): self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore')) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -530,8 +530,9 @@ class ElectrumWindow(App): else: return '' - def on_wizard_complete(self, wizard, wallet): - if wallet: # wizard returned a wallet + def on_wizard_complete(self, wizard, storage): + if storage: + wallet = Wallet(storage) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) self.load_wallet(wallet) @@ -553,11 +554,10 @@ class ElectrumWindow(App): self.load_wallet(wallet) else: def launch_wizard(): - storage = WalletStorage(path, manual_upgrades=True) - wizard = Factory.InstallWizard(self.electrum_config, self.plugins, storage) + wizard = Factory.InstallWizard(self.electrum_config, self.plugins) + wizard.path = path wizard.bind(on_wizard_complete=self.on_wizard_complete) - action = wizard.storage.get_action() - wizard.run(action) + wizard.run('new') if not ask_if_wizard: launch_wizard() else: diff --git a/electrum/gui/kivy/uix/dialogs/installwizard.py b/electrum/gui/kivy/uix/dialogs/installwizard.py @@ -25,8 +25,8 @@ from .password_dialog import PasswordDialog # global Variables is_test = (platform == "linux") -test_seed = "time taxi field recycle tiny license olive virus report rare steel portion achieve" test_seed = "grape impose jazz bind spatial mind jelly tourist tank today holiday stomach" +test_seed = "time taxi field recycle tiny license olive virus report rare steel portion achieve" test_xpub = "xpub661MyMwAqRbcEbvVtRRSjqxVnaWVUMewVzMiURAKyYratih4TtBpMypzzefmv8zUNebmNVzB3PojdC5sV2P9bDgMoo9B3SARw1MXUUfU1GL" Builder.load_string(''' @@ -629,7 +629,7 @@ class WizardKnownOTPDialog(WizardOTPDialogBase): def abort_wallet_creation(self): self._on_release = True - os.unlink(self.wizard.storage.path) + os.unlink(self.path) self.wizard.terminate() self.dismiss() @@ -972,7 +972,8 @@ class InstallWizard(BaseWizard, Widget): t.start() def terminate(self, **kwargs): - self.dispatch('on_wizard_complete', self.wallet) + storage = self.create_storage(self.path) + self.dispatch('on_wizard_complete', storage) def choice_dialog(self, **kwargs): choices = kwargs['choices'] diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py @@ -46,6 +46,7 @@ from electrum.plugin import run_hook from electrum.base_wizard import GoBack from electrum.util import (UserCancelled, PrintError, profiler, WalletFileException, BitcoinException, get_new_wallet_name) +from electrum.wallet import Wallet from .installwizard import InstallWizard @@ -227,12 +228,18 @@ class ElectrumGui(PrintError): else: return if not wallet: - wizard = InstallWizard(self.config, self.app, self.plugins, None) + wizard = InstallWizard(self.config, self.app, self.plugins) try: - if wizard.select_storage(path, self.daemon.get_wallet): - wallet = wizard.run_and_get_wallet() + path, storage = wizard.select_storage(path, self.daemon.get_wallet) + # storage is None if file does not exist + if storage is None: + wizard.path = path # needed by trustedcoin plugin + wizard.run('new') + storage = wizard.create_storage(path) + else: + wizard.run_upgrades(storage) except UserCancelled: - pass + return except GoBack as e: self.print_error('[start_new_window] Exception caught (GoBack)', e) except (WalletFileException, BitcoinException) as e: @@ -243,9 +250,10 @@ class ElectrumGui(PrintError): return finally: wizard.terminate() - if not wallet: + # return if wallet creation is not complete + if storage is None or storage.get_action(): return - + wallet = Wallet(storage) if not self.daemon.get_wallet(wallet.storage.path): # wallet was not in memory wallet.start_network(self.daemon.network) diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py @@ -25,7 +25,7 @@ from .network_dialog import NetworkChoiceLayout from .util import (MessageBoxMixin, Buttons, icon_path, ChoicesLayout, WWLabel, InfoButton) from .password_dialog import PasswordLayout, PasswordLayoutForHW, PW_NEW - +from electrum.plugin import run_hook MSG_ENTER_PASSWORD = _("Choose a password to encrypt your wallet keys.") + '\n'\ + _("Leave this field empty if you want to disable encryption.") @@ -110,8 +110,8 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): accept_signal = pyqtSignal() - def __init__(self, config, app, plugins, storage): - BaseWizard.__init__(self, config, plugins, storage) + def __init__(self, config, app, plugins): + BaseWizard.__init__(self, config, plugins) QDialog.__init__(self, None) self.setWindowTitle('Electrum - ' + _('Install Wizard')) self.app = app @@ -186,8 +186,8 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): vbox.addLayout(hbox2) self.set_layout(vbox, title=_('Electrum wallet')) - self.storage = WalletStorage(path, manual_upgrades=True) - wallet_folder = os.path.dirname(self.storage.path) + self.temp_storage = WalletStorage(path, manual_upgrades=True) + wallet_folder = os.path.dirname(self.temp_storage.path) def on_choose(): path, __ = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) @@ -199,25 +199,25 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): wallet_from_memory = get_wallet_from_daemon(path) try: if wallet_from_memory: - self.storage = wallet_from_memory.storage + self.temp_storage = wallet_from_memory.storage else: - self.storage = WalletStorage(path, manual_upgrades=True) + self.temp_storage = WalletStorage(path, manual_upgrades=True) self.next_button.setEnabled(True) except BaseException: traceback.print_exc(file=sys.stderr) - self.storage = None + self.temp_storage = None self.next_button.setEnabled(False) - if self.storage: - if not self.storage.file_exists(): + if self.temp_storage: + if not self.temp_storage.file_exists(): msg =_("This file does not exist.") + '\n' \ + _("Press 'Next' to create this wallet, or choose another file.") pw = False elif not wallet_from_memory: - if self.storage.is_encrypted_with_user_pw(): + if self.temp_storage.is_encrypted_with_user_pw(): msg = _("This file is encrypted with a password.") + '\n' \ + _('Enter your password or choose another file.') pw = True - elif self.storage.is_encrypted_with_hw_device(): + elif self.temp_storage.is_encrypted_with_hw_device(): msg = _("This file is encrypted using a hardware device.") + '\n' \ + _("Press 'Next' to choose device to decrypt.") pw = False @@ -242,24 +242,24 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): button.clicked.connect(on_choose) self.name_e.textChanged.connect(on_filename) - n = os.path.basename(self.storage.path) + n = os.path.basename(self.temp_storage.path) self.name_e.setText(n) while True: if self.loop.exec_() != 2: # 2 = next - return - if self.storage.file_exists() and not self.storage.is_encrypted(): + raise UserCancelled + if self.temp_storage.file_exists() and not self.temp_storage.is_encrypted(): break - if not self.storage.file_exists(): + if not self.temp_storage.file_exists(): break - wallet_from_memory = get_wallet_from_daemon(self.storage.path) + wallet_from_memory = get_wallet_from_daemon(self.temp_storage.path) if wallet_from_memory: return wallet_from_memory - if self.storage.file_exists() and self.storage.is_encrypted(): - if self.storage.is_encrypted_with_user_pw(): + if self.temp_storage.file_exists() and self.temp_storage.is_encrypted(): + if self.temp_storage.is_encrypted_with_user_pw(): password = self.pw_e.text() try: - self.storage.decrypt(password) + self.temp_storage.decrypt(password) break except InvalidPassword as e: QMessageBox.information(None, _('Error'), str(e)) @@ -268,9 +268,9 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): traceback.print_exc(file=sys.stdout) QMessageBox.information(None, _('Error'), str(e)) return - elif self.storage.is_encrypted_with_hw_device(): + elif self.temp_storage.is_encrypted_with_hw_device(): try: - self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET) + self.run('choose_hw_device', HWD_SETUP_DECRYPT_WALLET, self.temp_storage) except InvalidPassword as e: QMessageBox.information( None, _('Error'), @@ -282,31 +282,32 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): traceback.print_exc(file=sys.stdout) QMessageBox.information(None, _('Error'), str(e)) return - if self.storage.is_past_initial_decryption(): + if self.temp_storage.is_past_initial_decryption(): break else: return else: raise Exception('Unexpected encryption version') - return True - def run_and_get_wallet(self): - path = self.storage.path - if self.storage.requires_split(): + return self.temp_storage.path, self.temp_storage if self.temp_storage.file_exists() else None + + def run_upgrades(self, storage): + path = storage.path + if storage.requires_split(): self.hide() msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n" "Do you want to split your wallet into multiple files?").format(path) if not self.question(msg): return - file_list = '\n'.join(self.storage.split_accounts()) + file_list = '\n'.join(storage.split_accounts()) msg = _('Your accounts have been moved to') + ':\n' + file_list + '\n\n'+ _('Do you want to delete the old file') + ':\n' + path if self.question(msg): os.remove(path) self.show_warning(_('The file was removed')) return - action = self.storage.get_action() - if action and action not in ('new', 'upgrade_storage'): + action = storage.get_action() + if action: #< and action not in ('new', 'upgrade_storage'): self.hide() msg = _("The file '{}' contains an incompletely created wallet.\n" "Do you want to complete its creation now?").format(path) @@ -316,13 +317,15 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): self.show_warning(_('The file was removed')) return self.show() - if action: - # self.wallet is set in run + self.data = storage.data self.run(action) - return self.wallet + for k, v in self.data.items(): + storage.put(k, v) + storage.write() + return - self.wallet = Wallet(self.storage) - return self.wallet + if storage.requires_upgrade(): + self.upgrade_storage(storage) def finished(self): """Called in hardware client wrapper, in order to close popups.""" diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py @@ -200,7 +200,7 @@ class Plugin(TrustedCoinPlugin): def go_online_dialog(self, wizard: InstallWizard): msg = [ - _("Your wallet file is: {}.").format(os.path.abspath(wizard.storage.path)), + _("Your wallet file is: {}.").format(os.path.abspath(wizard.path)), _("You need to be online in order to complete the creation of " "your wallet. If you generated your seed on an offline " 'computer, click on "{}" to close this window, move your ' @@ -209,6 +209,7 @@ class Plugin(TrustedCoinPlugin): _('If you are online, click on "{}" to continue.').format(_('Next')) ] msg = '\n\n'.join(msg) + wizard.create_storage(wizard.path) wizard.reset_stack() wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use')) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py @@ -578,12 +578,9 @@ class TrustedCoinPlugin(BasePlugin): def on_password(self, wizard, password, encrypt_storage, k1, k2): k1.update_password(None, password) - wizard.storage.set_keystore_encryption(bool(password)) - if encrypt_storage: - wizard.storage.set_password(password, enc_version=STO_EV_USER_PW) - wizard.storage.put('x1/', k1.dump()) - wizard.storage.put('x2/', k2.dump()) - wizard.storage.write() + wizard.data['x1/'] = k1.dump() + wizard.data['x2/'] = k2.dump() + wizard.pw_args = password, encrypt_storage, STO_EV_USER_PW self.go_online_dialog(wizard) def restore_wallet(self, wizard): @@ -618,34 +615,26 @@ class TrustedCoinPlugin(BasePlugin): self.create_keystore(wizard, seed, passphrase) def on_restore_pw(self, wizard, seed, passphrase, password, encrypt_storage): - storage = wizard.storage xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) k1 = keystore.from_xprv(xprv1) k2 = keystore.from_xprv(xprv2) k1.add_seed(seed) k1.update_password(None, password) k2.update_password(None, password) - storage.put('x1/', k1.dump()) - storage.put('x2/', k2.dump()) - long_user_id, short_id = get_user_id(storage) + wizard.data['x1/'] = k1.dump() + wizard.data['x2/'] = k2.dump() + long_user_id, short_id = get_user_id(wizard.data) xtype = xpub_type(xpub1) xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id) k3 = keystore.from_xpub(xpub3) - storage.put('x3/', k3.dump()) - - storage.set_keystore_encryption(bool(password)) - if encrypt_storage: - storage.set_password(password, enc_version=STO_EV_USER_PW) - - wizard.wallet = Wallet_2fa(storage) - wizard.create_addresses() - + wizard.data['x3/'] = k3.dump() + wizard.pw_args = password, encrypt_storage, STO_EV_USER_PW def create_remote_key(self, email, wizard): - xpub1 = wizard.storage.get('x1/')['xpub'] - xpub2 = wizard.storage.get('x2/')['xpub'] + xpub1 = wizard.data['x1/']['xpub'] + xpub2 = wizard.data['x2/']['xpub'] # Generate third key deterministically. - long_user_id, short_id = get_user_id(wizard.storage) + long_user_id, short_id = get_user_id(wizard.data) xtype = xpub_type(xpub1) xpub3 = make_xpub(get_signing_xpub(xtype), long_user_id) # secret must be sent by the server @@ -709,16 +698,14 @@ class TrustedCoinPlugin(BasePlugin): wizard.terminate() else: k3 = keystore.from_xpub(xpub3) - wizard.storage.put('x3/', k3.dump()) - wizard.storage.put('use_trustedcoin', True) - wizard.storage.write() - wizard.wallet = Wallet_2fa(wizard.storage) - wizard.run('create_addresses') + wizard.data['x3/'] = k3.dump() + wizard.data['use_trustedcoin'] = True + wizard.terminate() def on_reset_auth(self, wizard, short_id, seed, passphrase, xpub3): xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) - if (wizard.storage.get('x1/')['xpub'] != xpub1 or - wizard.storage.get('x2/')['xpub'] != xpub2): + if (wizard.data['x1/']['xpub'] != xpub1 or + wizard.data['x2/']['xpub'] != xpub2): wizard.show_message(_('Incorrect seed')) return r = server.get_challenge(short_id) diff --git a/electrum/storage.py b/electrum/storage.py @@ -102,20 +102,6 @@ class JsonDB(PrintError): self.modified = True self.data.pop(key) - def get_all_data(self) -> dict: - with self.db_lock: - return copy.deepcopy(self.data) - - def overwrite_all_data(self, data: dict) -> None: - try: - json.dumps(data, cls=util.MyEncoder) - except: - self.print_error(f"json error: cannot save {repr(data)}") - return - with self.db_lock: - self.modified = True - self.data = copy.deepcopy(data) - @profiler def write(self): with self.db_lock: