def check_legacy_parent_of_imported_privkey_wallet( parent_wallet: ParentWallet, keypairs: Optional[Dict[str, str]] = None) -> None: assert len(parent_wallet.get_child_wallets()) == 1 child_wallet: ImportedPrivkeyWallet = parent_wallet.get_child_wallets()[0] parent_keystores = parent_wallet.get_keystores() assert len(parent_keystores) == 1 child_keystores = child_wallet.get_keystores() assert len(child_keystores) == 1 assert parent_keystores[0] is child_keystores[0] keystore_data = parent_keystores[0].dump() assert 'type' in keystore_data assert len(keystore_data) == 2 assert keystore_data['type'] == 'imported' if keypairs is not None: assert keystore_data['keypairs'] == keypairs else: assert "keypairs" in keystore_data child_wallet_data = child_wallet.dump() # A newly created wallet. expected_count = 3 if "stored_height" in child_wallet_data: # A wallet that has synced after it was created. expected_count = 4 assert len(child_wallet_data) == expected_count assert child_wallet_data['id'] == 0 assert child_wallet_data[ 'wallet_type'] == ImportedPrivkeyWallet.wallet_type keystore_usage = child_wallet_data['keystore_usage'] assert len(keystore_usage) == 1 assert len(keystore_usage[0]) == 1 assert keystore_usage[0]['index'] == 0
def check_legacy_parent_of_standard_wallet( parent_wallet: ParentWallet, seed_words: Optional[str] = None, is_bip39: bool = False, password: Optional[str] = None) -> None: assert len(parent_wallet.get_child_wallets()) == 1 child_wallet: Standard_Wallet = parent_wallet.get_child_wallets()[0] parent_keystores = parent_wallet.get_keystores() assert len(parent_keystores) == 1 child_keystores = child_wallet.get_keystores() assert len(child_keystores) == 1 assert parent_keystores[0] is child_keystores[0] keystore_encrypted = parent_wallet.has_password() if keystore_encrypted: assert password is not None assert not child_keystores[0].has_seed( ) or child_keystores[0].get_seed(password) assert type(child_keystores[0].get_passphrase(password)) is str assert child_keystores[0].get_master_private_key(password) keystore_data = parent_keystores[0].dump() entry_count = 4 if is_bip39: entry_count = 3 assert len(keystore_data) == entry_count assert keystore_data['type'] == 'bip32' assert 'xpub' in keystore_data assert 'xprv' in keystore_data assert "encrypted" not in parent_wallet.name() or keystore_encrypted if is_bip39: assert "seed" not in keystore_data else: if seed_words is None: assert "seed" in keystore_data else: assert keystore_data['seed'] == seed_words child_wallet_data = child_wallet.dump() # A newly created wallet. expected_count = 3 if "stored_height" in child_wallet_data: # A wallet that has synced after it was created. assert "labels" in child_wallet_data expected_count = 5 assert len(child_wallet_data) == expected_count assert child_wallet_data['id'] == 0 assert child_wallet_data['wallet_type'] == 'standard' keystore_usage = child_wallet_data['keystore_usage'] assert len(keystore_usage) == 1 assert len(keystore_usage[0]) == 1 assert keystore_usage[0]['index'] == 0
def run_and_get_wallet(self): path = self.storage.get_path() if self.storage.requires_split(): msg = _( "The wallet '{}' contains multiple accounts, which are not supported.\n\n" "Do you want to split your wallet into multiple files?" ).format(path) if not MessageBox.question(msg): return file_list = '\n'.join(self.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 if self.storage.requires_upgrade(): msg = _( "The format of your wallet '%s' must be upgraded for ElectrumSV. " "This change will not be backward compatible, " + "and your existing wallet will be backed up. Proceed?") % path if not MessageBox.question(msg): return self.storage.upgrade() self.parent_wallet = ParentWallet(self.storage) return self.parent_wallet action = None if self.storage.file_exists() else 'new' if action and action != 'new': msg = _("The file '{}' contains an incompletely created wallet.\n" "Do you want to complete its creation now?").format(path) if not MessageBox.question(msg): if MessageBox.question( _("Do you want to delete '{}'?").format(path)): os.remove(path) self.show_warning(_('The file was removed')) return self.show() if action: # self.parent_wallet is set in run, unless they go back. self.run(action) if action == "new" and self.parent_wallet: # We forceably save new wallets in order to get the initial state synced on disk # that the user can both find it if ESV crashes, and that the externally referenced # and encrypted data has synchronised persisted keys self.parent_wallet.save_storage() return self.parent_wallet self.parent_wallet = ParentWallet(self.storage) return self.parent_wallet
def test_old(self, tmp_storage) -> None: seed_words = ('powerful random nobody notice nothing important ' + 'anyway look away hidden message over') child_keystore = from_seed(seed_words, '', False) assert isinstance(child_keystore, Old_KeyStore) parent_wallet = ParentWallet.as_legacy_wallet_container(tmp_storage) keystore_usage = parent_wallet.add_keystore(child_keystore.dump()) child_wallet = Standard_Wallet.create_within_parent( parent_wallet, keystore_usage=[keystore_usage]) parent_keystores = parent_wallet.get_keystores() assert len(parent_keystores) == 1 child_keystores = child_wallet.get_keystores() assert len(child_keystores) == 1 assert parent_keystores[0] is child_keystores[0] keystore_data = parent_keystores[0].dump() assert len(keystore_data) == 3 assert keystore_data['type'] == 'old' assert 'mpk' in keystore_data assert 'seed' in keystore_data child_wallet_data = child_wallet.dump() assert len(child_wallet_data) == 3 assert child_wallet_data['id'] == 0 assert child_wallet_data['wallet_type'] == 'standard' keystore_usage = child_wallet_data['keystore_usage'] assert len(keystore_usage) == 1 assert len(keystore_usage[0]) == 1 assert keystore_usage[0]['index'] == 0
def check_legacy_parent_of_imported_address_wallet( parent_wallet: ParentWallet) -> None: assert len(parent_wallet.get_child_wallets()) == 1 child_wallet: ImportedAddressWallet = parent_wallet.get_child_wallets()[0] assert len(parent_wallet.get_keystores()) == 0 assert len(child_wallet.get_keystores()) == 0 child_wallet_data = child_wallet.dump() # A newly created wallet. expected_count = 2 if "stored_height" in child_wallet_data: # A wallet that has synced after it was created. expected_count = 3 assert len(child_wallet_data) == expected_count assert child_wallet_data['id'] == 0 assert child_wallet_data[ 'wallet_type'] == ImportedAddressWallet.wallet_type assert "keystore_usage" not in child_wallet_data
def test_imported_pubkey(self, tmp_storage) -> None: text = """ 15hETetDmcXm1mM4sEf7U2KXC9hDHFMSzz 1GPHVTY8UD9my6jyP4tb2TYJwUbDetyNC6 """ parent_wallet = ParentWallet.as_legacy_wallet_container(tmp_storage) child_wallet = ImportedAddressWallet.from_text(parent_wallet, text) check_legacy_parent_of_imported_address_wallet(parent_wallet)
def test_standard_electrum(self, tmp_storage) -> None: seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song' child_keystore = from_seed(seed_words, '', False) parent_wallet = ParentWallet.as_legacy_wallet_container(tmp_storage) keystore_usage = parent_wallet.add_keystore(child_keystore.dump()) child_wallet = Standard_Wallet.create_within_parent( parent_wallet, keystore_usage=[keystore_usage]) check_legacy_parent_of_standard_wallet(parent_wallet)
def check_legacy_parent_of_standard_wallet(parent_wallet: ParentWallet, seed_words: Optional[str] = None, is_bip39: bool = False) -> None: assert len(parent_wallet.get_child_wallets()) == 1 child_wallet: Standard_Wallet = parent_wallet.get_child_wallets()[0] parent_keystores = parent_wallet.get_keystores() assert len(parent_keystores) == 1 child_keystores = child_wallet.get_keystores() assert len(child_keystores) == 1 assert parent_keystores[0] is child_keystores[0] keystore_data = parent_keystores[0].dump() entry_count = 4 if is_bip39: entry_count = 3 assert len(keystore_data) == entry_count assert keystore_data['type'] == 'bip32' assert 'xpub' in keystore_data assert 'xprv' in keystore_data if is_bip39: assert "seed" not in keystore_data else: if seed_words is None: assert "seed" in keystore_data else: assert keystore_data['seed'] == seed_words child_wallet_data = child_wallet.dump() # A newly created wallet. expected_count = 3 if "stored_height" in child_wallet_data: # A wallet that has synced after it was created. assert "labels" in child_wallet_data expected_count = 5 assert len(child_wallet_data) == expected_count assert child_wallet_data['id'] == 0 assert child_wallet_data['wallet_type'] == 'standard' keystore_usage = child_wallet_data['keystore_usage'] assert len(keystore_usage) == 1 assert len(keystore_usage[0]) == 1 assert keystore_usage[0]['index'] == 0
def check_legacy_parent_of_multisig_wallet( parent_wallet: ParentWallet) -> None: assert len(parent_wallet.get_child_wallets()) == 1 child_wallet: Multisig_Wallet = parent_wallet.get_child_wallets()[0] wallet_type = child_wallet.wallet_type m, n = multisig_type(wallet_type) parent_keystores = parent_wallet.get_keystores() assert len(parent_keystores) == 2 child_keystores = child_wallet.get_keystores() assert len(child_keystores) == n for i in range(n): assert parent_keystores[i] is child_keystores[i] for i in range(n): keystore_data = parent_keystores[0].dump() if len(keystore_data) == 4: assert keystore_data['type'] == 'bip32' assert keystore_data['seed'] is not None # == seed_words assert keystore_data['xpub'] is not None assert keystore_data['xprv'] is not None else: assert len(keystore_data) == 3 assert keystore_data['type'] == 'bip32' assert keystore_data['xpub'] is not None assert keystore_data['xprv'] is None child_wallet_data = child_wallet.dump() # A newly created wallet. entry_count = 3 if "stored_height" in child_wallet_data: assert "labels" in child_wallet_data # A wallet that has synced after it was created. entry_count = 5 assert len(child_wallet_data) == entry_count assert child_wallet_data['id'] == 0 assert child_wallet_data['wallet_type'] == wallet_type keystore_usage = [] for i in range(n): keystore_usage.append({'index': i, 'name': f'x{i+1}/'}) assert child_wallet_data['keystore_usage'] == keystore_usage
def test_legacy_wallet_loading(storage_info: WalletStorageInfo) -> None: # When a wallet is composed of multiple files, we need to know which to load. wallet_filenames = [] if storage_info.kind != StorageKind.DATABASE: wallet_filenames.append(storage_info.filename) if storage_info.kind in (StorageKind.DATABASE, StorageKind.HYBRID): wallet_filenames.append(storage_info.filename + DATABASE_EXT) temp_dir = tempfile.mkdtemp() for _wallet_filename in wallet_filenames: source_wallet_path = os.path.join(TEST_WALLET_PATH, _wallet_filename) wallet_path = os.path.join(temp_dir, _wallet_filename) shutil.copyfile(source_wallet_path, wallet_path) # net = None # if "testnet" in wallet_filename: # net = SVTestnet # elif "mainnet" in wallet_filename: # net = SVMainnet # else: # raise Exception(f"unable to identify wallet network for {wallet_filename}") wallet_filename = storage_info.filename wallet_path = os.path.join(temp_dir, wallet_filename) password = "******" storage = WalletStorage(wallet_path) if "passworded" in wallet_filename: storage.decrypt(password) try: parent_wallet = ParentWallet(storage) except OSError as e: if "is not a valid Win32 application" not in e.args[1]: raise e pytest.xfail("Missing libusb for this architecture") return if "standard" in wallet_filename: is_bip39 = "bip39" in wallet_filename check_legacy_parent_of_standard_wallet(parent_wallet, is_bip39=is_bip39, password=password) elif "imported_privkey" in wallet_filename: check_legacy_parent_of_imported_privkey_wallet(parent_wallet) elif "imported_address" in wallet_filename: check_legacy_parent_of_imported_address_wallet(parent_wallet) elif "multisig" in wallet_filename: check_legacy_parent_of_multisig_wallet(parent_wallet) elif "hardware" in wallet_filename: check_legacy_parent_of_hardware_wallet(parent_wallet) else: raise Exception(f"unrecognised wallet file {wallet_filename}")
def test_imported_privkey(self, tmp_storage) -> None: text = """ KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6 """ parent_wallet = ParentWallet.as_legacy_wallet_container(tmp_storage) child_wallet = ImportedPrivkeyWallet.from_text(parent_wallet, text) keypairs = { '02c6467b7e621144105ed3e4835b0b4ab7e35266a2ae1c4f8baa19e9ca93452997': 'KzMFjMC2MPadjvX5Cd7b8AKKjjpBSoRKUTpoAtN6B3J9ezWYyXS6' } check_legacy_parent_of_imported_privkey_wallet(parent_wallet, keypairs=keypairs)
def __init__(self, parent: QWidget, parent_wallet: ParentWallet) -> None: WindowModalDialog.__init__(self, parent) is_encrypted = parent_wallet.is_encrypted() if not parent_wallet.has_password(): msg = _('Your wallet is not protected.') msg += ' ' + _('Use this dialog to add a password to your wallet.') else: if not is_encrypted: msg = _( 'Your bitcoins are password protected. However, your wallet file ' 'is not encrypted.') else: msg = _('Your wallet is password protected and encrypted.') msg += ' ' + _('Use this dialog to change your password.') OK_button = OkButton(self) self.playout = PasswordLayout(parent_wallet, msg, PW_CHANGE, OK_button) self.setWindowTitle(self.playout.title()) vbox = QVBoxLayout(self) vbox.setSizeConstraint(QVBoxLayout.SetFixedSize) vbox.addLayout(self.playout.layout()) vbox.addStretch(1) vbox.addLayout(Buttons(CancelButton(self), OK_button))
def check_legacy_parent_of_hardware_wallet( parent_wallet: ParentWallet) -> None: assert len(parent_wallet.get_child_wallets()) == 1 child_wallet = parent_wallet.get_child_wallets()[0] parent_keystores = parent_wallet.get_keystores() assert len(parent_keystores) == 1 child_keystores = child_wallet.get_keystores() assert len(child_keystores) == 1 assert parent_keystores[0] is child_keystores[0] keystore_data = parent_keystores[0].dump() # General hardware wallet. entry_count = 5 if keystore_data['hw_type'] == "ledger": # Ledger wallets extend the keystore. assert "cfg" in keystore_data entry_count = 6 assert len(keystore_data) == entry_count assert keystore_data['type'] == 'hardware' assert 'hw_type' in keystore_data assert 'label' in keystore_data assert "derivation" in keystore_data child_wallet_data = child_wallet.dump() # A newly created wallet. expected_count = 3 if "stored_height" in child_wallet_data: # A wallet that has synced after it was created. assert "labels" in child_wallet_data expected_count = 5 assert len(child_wallet_data) == expected_count assert child_wallet_data['id'] == 0 assert child_wallet_data['wallet_type'] == 'standard' keystore_usage = child_wallet_data['keystore_usage'] assert len(keystore_usage) == 1 assert len(keystore_usage[0]) == 1 assert keystore_usage[0]['index'] == 0
def test_multisig(self, tmp_storage) -> None: parent_wallet = ParentWallet.as_legacy_wallet_container(tmp_storage) seed_words = ( 'blast uniform dragon fiscal ensure vast young utility dinosaur abandon ' + 'rookie sure') ks1 = from_seed(seed_words, '', True) ks2 = from_xpub( 'xpub661MyMwAqRbcGfCPEkkyo5WmcrhTq8mi3xuBS7VEZ3LYvsgY1cCFDben' + 'T33bdD12axvrmXhuX3xkAbKci3yZY9ZEk8vhLic7KNhLjqdh5ec') keystores = [ks1, ks2] keystore_usages = [] for i, k in enumerate(keystores): keystore_usage = parent_wallet.add_keystore(k.dump()) keystore_usage['name'] = f'x{i+1}/' keystore_usages.append(keystore_usage) child_wallet = Multisig_Wallet.create_within_parent( parent_wallet, keystore_usage=keystore_usages, wallet_type="2of2") check_legacy_parent_of_multisig_wallet(parent_wallet)
def __init__(self, parent: ElectrumWindow, parent_wallet: ParentWallet) -> None: super().__init__(parent) self._main_window = parent self._parent_wallet = parent_wallet # Left-hand side a list of wallets. # Right-hand side a history view showing the current wallet. self._selection_list = QListWidget() self._history_list = HistoryList(parent, parent_wallet.get_default_wallet()) self.addWidget(self._selection_list) self.addWidget(self._history_list) self.setStretchFactor(1, 2) self._selection_list.setContextMenuPolicy(Qt.CustomContextMenu) self._selection_list.customContextMenuRequested.connect(self._create_wallet_menu) self._update_wallet_list()
def run_offline_command(config, config_options): cmdname = config.get('cmd') cmd = known_commands[cmdname] password = config_options.get('password') if cmd.requires_wallet: wallet_path = config.get_wallet_path() if not WalletStorage.files_are_matched_by_path(wallet_path): print("Error: wallet does not exist at given path") sys.exit(1) storage = WalletStorage(wallet_path) if storage.is_encrypted(): storage.decrypt(password) parent_wallet = ParentWallet(storage) else: parent_wallet = None # check password if cmd.requires_password and parent_wallet.has_password(): try: parent_wallet.check_password(password) except InvalidPassword: print("Error: This password does not decode this wallet.") sys.exit(1) if cmd.requires_network: print("Warning: running command offline") # arguments passed to function args = [config.get(x) for x in cmd.params] # decode json arguments if cmdname not in ('setconfig', ): args = [json_decode(arg) for arg in args] # options kwargs = {} for x in cmd.options: kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x)) cmd_runner = Commands(config, parent_wallet, None) func = getattr(cmd_runner, cmd.name) result = func(*args, **kwargs) # save wallet if parent_wallet: parent_wallet.save_storage() return result
def run_and_get_wallet(self) -> Optional[Abstract_Wallet]: # NOTE(rt12): This used to have unused code related to resuming incompletely created # wallets. This is worth supporting, but not at this stage where we will eventually # rewrite for a new wallet wizard and multiple accounts and legacy wallets so on. if self._storage_existing is not None: path = self._storage_existing.get_path() if self._storage_existing.requires_split(): msg = _( "The wallet '{}' contains multiple accounts, which are not supported.\n\n" "Do you want to split your wallet " "into multiple files?").format(path) if not MessageBox.question(msg): return file_list = '\n'.join(self._storage_existing.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): # We know that this will be the only relevant path and will not require # extensions, as it predates the use of the database. os.remove(path) self.show_warning(_('The file was removed')) return if self._storage_existing.requires_upgrade(): msg = _( "The format of your wallet '%s' must be upgraded for ElectrumSV. " "This change will not be backward compatible, " + "and your existing wallet will be backed up. Proceed?" ) % path if not MessageBox.question(msg): return self._storage_existing.upgrade() self._parent_wallet = ParentWallet(self._storage_existing) else: assert self._path_new is not None, "user should have selected storage already" wallet_filename = os.path.basename(self._path_new) # Make a temporary directory as securely as possible, where the new wallet will be # created. creation_path = tempfile.mkdtemp() try: path_new = os.path.join(creation_path, wallet_filename) self._storage_new = WalletStorage(path_new) # Ensure the path is full and includes the extension. path_new = self._storage_new.get_path() self.run("new") if self._parent_wallet is not None: self._parent_wallet.save_storage() if self._parent_wallet is not None: self._parent_wallet.move_to(self._path_new) else: # We want to make sure we are not keeping the database open, before deleting. self._storage_new.close() del self._storage_new finally: shutil.rmtree(creation_path) return self._parent_wallet
class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): accept_signal = pyqtSignal() synchronized_signal = pyqtSignal(str) def __init__(self): BaseWizard.__init__(self) QDialog.__init__(self, None) self.setWindowTitle('ElectrumSV') self.language_for_seed = app_state.config.get('language') self.setMinimumSize(600, 420) self.accept_signal.connect(self.accept) self.back_button = QPushButton(_(MSG_BUTTON_BACK), self) self.back_button.setText( _(MSG_BUTTON_BACK) if self.can_go_back() else _(MSG_BUTTON_CANCEL)) self.next_button = QPushButton(_(MSG_BUTTON_NEXT), self) self.next_button.setDefault(True) self.icon_filename = None self.loop = QEventLoop() self.rejected.connect(lambda: self.loop.exit(0)) self.back_button.clicked.connect(lambda: self.loop.exit(1)) self.next_button.clicked.connect(lambda: self.loop.exit(2)) self.scroll_widget = QWidget() self.scroll_widget.setLayout(self.create_template_layout()) scroll = QScrollArea() scroll.setWidget(self.scroll_widget) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll.setWidgetResizable(True) outer_vbox = QVBoxLayout(self) outer_vbox.addWidget(scroll) outer_vbox.addLayout(Buttons(self.back_button, self.next_button)) self.show() self.raise_() self.refresh_gui() # Need for QT on MacOSX. Lame. def create_template_layout(self): """ The standard layout divides creates a three part template. """ self.title = QLabel() self.main_widget = QWidget() self.please_wait = QLabel(_("Please wait...")) self.please_wait.setAlignment(Qt.AlignCenter) vbox = QVBoxLayout() vbox.addWidget(self.title) vbox.addWidget(self.main_widget) vbox.addStretch(1) vbox.addWidget(self.please_wait) vbox.addStretch(1) self.template_hbox = QHBoxLayout() vbox.addLayout(self.template_hbox) return vbox def select_storage(self, initial_path: str, is_startup=False): if is_startup: self._copy_electron_cash_wallets() vbox = QVBoxLayout() hbox = QHBoxLayout() hbox.addWidget(QLabel(_('Wallet') + ':')) self.name_e = QLineEdit() hbox.addWidget(self.name_e) button = QPushButton(_('Choose...')) hbox.addWidget(button) vbox.addLayout(hbox) self.msg_label = QLabel('') vbox.addWidget(self.msg_label) hbox2 = QHBoxLayout() self.pw_e = PasswordLineEdit() self.pw_e.setMinimumWidth(200) self.pw_label = QLabel(_('Password') + ':') self.pw_label.setAlignment(Qt.AlignTop) hbox2.addWidget(self.pw_label) hbox2.addWidget(self.pw_e) hbox2.addStretch() vbox.addLayout(hbox2) self._set_standard_layout(vbox, title=_('ElectrumSV wallet'), back_text=_(MSG_BUTTON_CANCEL)) esv_wallets_dir = os.path.join(app_state.config.electrum_path(), "wallets") if is_startup: def _show_copy_electron_cash_wallets_dialog(*args): nonlocal esv_wallets_dir, ec_wallets_dir d = WindowModalDialog(self, _("Copy Electron Cash Wallets")) d.setMinimumWidth(400) vbox, file_list = self._create_copy_electron_cash_wallets_layout( ec_wallets_dir) bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) bbox.rejected.connect(d.reject) bbox.accepted.connect(d.accept) vbox.addWidget(bbox) d.setLayout(vbox) result = d.exec() if result == QDialog.Accepted: self._do_copy_electron_cash_wallets( file_list, esv_wallets_dir, ec_wallets_dir) _update_selected_wallet() ec_import_text = ("<p>" + _( "You have previously run Electron Cash and created at " + "least one wallet with it. ElectrumSV should not be used to open these wallets " + "directly using the 'Choose' button, and you should instead use the 'Import' " + "button to help you copy them." ) + "</p>" + "<p>" + _( "There are many reasons for this, and the simplest is that if Electron Cash " + "and ElectrumSV were to operate on the same wallet at the same time, then the " + "wallet will most likely become corrupted. It's simpler in every way to just " + "copy the wallet over to ElectrumSV and use a separate wallet file for each." ) + "</p>") ec_import_icon = HelpLabel("label text", ec_import_text) ec_import_icon.setPixmap( QPixmap(icon_path("icons8-info.svg")).scaledToWidth( 16, Qt.SmoothTransformation)) ec_import_label = QLabel( _("Existing Electron Cash wallets detected")) ec_import_button = QPushButton(_("Import...")) ec_import_button.clicked.connect( _show_copy_electron_cash_wallets_dialog) self.template_hbox.addWidget(ec_import_icon) self.template_hbox.addWidget(ec_import_label) self.template_hbox.addWidget(ec_import_button) self.template_hbox.addStretch(1) ec_wallets_dir = get_electron_cash_user_dir(esv_wallets_dir) if len(self._list_user_wallets(ec_wallets_dir)) == 0: ec_import_icon.set_help_text("<p>" + _( "This feature is aimed at users who were " + "already using Electron Cash and have existing wallets associated with it. " + "None were detected on this computer, but if you do have some stored in " + "places ElectrumSV does not know about you may need to copy them " + "yourself." ) + "</p>" + "<p>" + _( "You should never open your existing Electron Cash wallets directly in " + "ElectrumSV as this can lead to them being opened in both applications at " + "the same time, and can result in corruption.") + "</p>") ec_import_button.setEnabled(False) ec_import_button.setToolTip(_("Nothing to import")) ec_import_label.setText(_("No Electron Cash wallets detected")) if WalletStorage.files_are_matched_by_path(initial_path): self._storage_existing = WalletStorage(initial_path, manual_upgrades=True) wallet_folder = os.path.dirname(initial_path) def _on_choose() -> None: path, __ = QFileDialog.getOpenFileName(self, "Select your wallet file", wallet_folder) if path: self.name_e.setText(path) def _on_filename(filename: str) -> None: pw = False self._storage_existing = None # A relative path will be relative to the folder we offered in the choose dialog. # An absolute path will not get joined to the dialog folder (no-op). path = os.path.join(wallet_folder, filename) if WalletStorage.files_are_matched_by_path(path): try: self._storage_existing = WalletStorage( path, manual_upgrades=True) except IOError: self.next_button.setEnabled(False) msg = _('Cannot read file') else: self.next_button.setEnabled(True) if self._storage_existing.is_encrypted(): msg = '\n'.join([ _("This file is encrypted."), _('Enter your password or choose another file.'), ]) pw = True else: msg = _("Press 'Next' to open this wallet.") else: msg = _("This file does not exist.") if os.access(wallet_folder, os.W_OK): self.next_button.setEnabled(True) msg += "\n" + _( "Press 'Next' to create this wallet, or choose another file." ) self._path_new = path else: self.next_button.setEnabled(False) msg += "\n" + _("You do not have write access " + "to this folder to create a new wallet.") self.msg_label.setText(msg) if pw: self.pw_label.show() self.pw_e.show() self.pw_e.setFocus() else: self.pw_label.hide() self.pw_e.hide() def _update_selected_wallet( skip_pick_most_recent: bool = False) -> None: wallet_filename = None if is_startup and not skip_pick_most_recent and self._storage_existing is None: esv_wallet_names = self._list_user_wallets(esv_wallets_dir) if len(esv_wallet_names): wallet_filename = esv_wallet_names[0] if wallet_filename is None: if self._storage_existing is not None: wallet_filename = os.path.basename( self._storage_existing.get_path()) else: wallet_filename = os.path.basename(initial_path) self.name_e.setText(wallet_filename) button.clicked.connect(_on_choose) self.name_e.textChanged.connect(_on_filename) # We do not pick the most recent when first displaying the wizard because we want to # treat the preselected wallet as the user's explicit choice. So a non-existent name # should be a possible wallet creation. _update_selected_wallet(skip_pick_most_recent=True) while True: if self._storage_existing is not None and not self._storage_existing.is_encrypted( ): break if self.loop.exec_() != 2: # 2 = next return if self._storage_existing is None: break if self._storage_existing is not None and self._storage_existing.is_encrypted( ): password = self.pw_e.text() try: self._storage_existing.decrypt(password) self.pw_e.setText('') break except DecryptionError: QMessageBox.information(None, _('Error'), _("Incorrect password")) continue except Exception as e: logger.exception("decrypting storage") QMessageBox.information(None, _('Error'), str(e)) return return True def _copy_electron_cash_wallets(self): """ Work out whether we should show UI to offer to copy the user's Electron Cash wallets to their ElectrumSV wallet directory, and if so, show it and give them the chance. """ # If the user has ElectrumSV wallets already, we do not offer to copy the one's # Electron Cash has. esv_wallets_dir = os.path.join(app_state.config.electrum_path(), "wallets") if len(self._list_user_wallets(esv_wallets_dir)) > 0: return ec_wallets_dir = get_electron_cash_user_dir(esv_wallets_dir) ec_wallet_count = len(self._list_user_wallets(ec_wallets_dir)) # If the user does not have Electron Cash wallets to copy, there's no point in offering. if ec_wallet_count == 0: return vbox, file_list = self._create_copy_electron_cash_wallets_layout( ec_wallets_dir) self._set_standard_layout(vbox, title=_('Import Electron Cash wallets')) v = self.loop.exec_() # Cancel, exit application. if v == -1: raise UserCancelled() if v != 2: raise GoBack() self._do_copy_electron_cash_wallets(file_list, esv_wallets_dir, ec_wallets_dir) def _do_copy_electron_cash_wallets(self, file_list, esv_wallets_dir, ec_wallets_dir): # If the user selected any files, then we copy them before exiting to the next page. copy_count = 0 for item in file_list.selectedItems(): filename = item.text() source_path = os.path.join(ec_wallets_dir, filename) target_path = os.path.join(esv_wallets_dir, filename) # If they are copying an Electron Cash wallet over an ElectrumSV wallet, make sure # they confirm they are going to replace/overwrite it. if os.path.exists(target_path): if self.question( _("You already have a wallet named '{}' for ElectrumSV. " + "Replace/overwrite it?").format(filename), self, _("Delete Wallet?")): os.remove(target_path) else: continue try: shutil.copyfile(source_path, target_path) copy_count += 1 except shutil.Error: # For now we ignore copy errors. pass if copy_count == 1: self.show_message(_("1 wallet copied.")) elif copy_count > 1: self.show_message(_("%d wallets copied.") % copy_count) def _create_copy_electron_cash_wallets_layout(self, ec_wallets_dir): def update_summary_label(): selection_count = len(file_list.selectedItems()) if selection_count == 0: summary_label.setText( _("No wallets are selected / will be copied.")) elif selection_count == 1: summary_label.setText( _("1 wallet is selected / will be copied.")) else: summary_label.setText( _("%d wallets are selected / will be copied.") % selection_count) wallet_filenames = sorted(os.listdir(ec_wallets_dir), key=lambda s: s.lower()) file_list = QListWidget() file_list.setSelectionMode(QAbstractItemView.ExtendedSelection) for filename in wallet_filenames: if not self._ignore_wallet_file( os.path.join(ec_wallets_dir, filename)): file_list.addItem(QListWidgetItem(filename)) file_list.itemSelectionChanged.connect(update_summary_label) vbox = QVBoxLayout() introduction_label = QLabel( _("Your Electron Cash wallet directory was found. If you want ElectrumSV to import " "any of them on your behalf, select the ones you want copied from the list below " "before clicking the Next button.")) introduction_label.setWordWrap(True) vbox.setSpacing(20) vbox.addWidget(introduction_label) vbox.addWidget(file_list) summary_label = QLabel() update_summary_label() vbox.addWidget(summary_label) return vbox, file_list def _list_user_wallets(self, wallets_path): if os.path.exists(wallets_path): from stat import ST_MTIME l = [(os.stat(os.path.join(wallets_path, filename))[ST_MTIME], filename) for filename in os.listdir(wallets_path) if not self._ignore_wallet_file( os.path.join(wallets_path, filename))] l = sorted(l, reverse=True) return [entry[1] for entry in l] return [] def _ignore_wallet_file(self, wallet_path): if os.path.isdir(wallet_path): return True if wallet_path.startswith("."): return True return False def run_and_get_wallet(self) -> Optional[Abstract_Wallet]: # NOTE(rt12): This used to have unused code related to resuming incompletely created # wallets. This is worth supporting, but not at this stage where we will eventually # rewrite for a new wallet wizard and multiple accounts and legacy wallets so on. if self._storage_existing is not None: path = self._storage_existing.get_path() if self._storage_existing.requires_split(): msg = _( "The wallet '{}' contains multiple accounts, which are not supported.\n\n" "Do you want to split your wallet " "into multiple files?").format(path) if not MessageBox.question(msg): return file_list = '\n'.join(self._storage_existing.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): # We know that this will be the only relevant path and will not require # extensions, as it predates the use of the database. os.remove(path) self.show_warning(_('The file was removed')) return if self._storage_existing.requires_upgrade(): msg = _( "The format of your wallet '%s' must be upgraded for ElectrumSV. " "This change will not be backward compatible, " + "and your existing wallet will be backed up. Proceed?" ) % path if not MessageBox.question(msg): return self._storage_existing.upgrade() self._parent_wallet = ParentWallet(self._storage_existing) else: assert self._path_new is not None, "user should have selected storage already" wallet_filename = os.path.basename(self._path_new) # Make a temporary directory as securely as possible, where the new wallet will be # created. creation_path = tempfile.mkdtemp() try: path_new = os.path.join(creation_path, wallet_filename) self._storage_new = WalletStorage(path_new) # Ensure the path is full and includes the extension. path_new = self._storage_new.get_path() self.run("new") if self._parent_wallet is not None: self._parent_wallet.save_storage() if self._parent_wallet is not None: self._parent_wallet.move_to(self._path_new) else: # We want to make sure we are not keeping the database open, before deleting. self._storage_new.close() del self._storage_new finally: shutil.rmtree(creation_path) return self._parent_wallet def finished(self): """Called in hardware client wrapper, in order to close popups.""" return def on_error(self, exc_info): if not isinstance(exc_info[1], UserCancelled): logger.exception("") self.show_error(str(exc_info[1])) def _remove_layout_from_widget(self, widget): """ The only way to remove a layout from a first widget, is to transfer it to a second one. This needs to be done, to be able to set a new layout on the first widget. """ existing_layout = widget.layout() QWidget().setLayout(existing_layout) def _set_layout(self, layout, next_enabled=True, back_text=None): """ Set a layout that is in control of the whole display area. """ self._remove_layout_from_widget(self.scroll_widget) self.scroll_widget.setLayout(layout) self.back_button.setEnabled(True) if back_text is not None: self.back_button.setText(back_text) self.next_button.setEnabled(next_enabled) if next_enabled: self.next_button.setFocus() def _set_standard_layout(self, layout, title=None, next_enabled=True, back_text=None): """ Ensure the standard template layout is in place. And put the current stage's sub-layout in the defined place. """ self._remove_layout_from_widget(self.scroll_widget) self.scroll_widget.setLayout(self.create_template_layout()) self.title.setText("<b>%s</b>" % title if title else "") self.title.setVisible(bool(title)) self.main_widget.setLayout(layout) if back_text is None: self.back_button.setText(_(MSG_BUTTON_BACK)) else: self.back_button.setText(back_text) self.back_button.setEnabled(True) self.next_button.setText(_(MSG_BUTTON_NEXT)) self.next_button.setEnabled(next_enabled) if next_enabled: self.next_button.setFocus() self.main_widget.setVisible(True) self.please_wait.setVisible(False) def exec_layout(self, layout, title=None, raise_on_cancel=True, next_enabled=True): self._set_standard_layout(layout, title, next_enabled) result = self.loop.exec_() if not result and raise_on_cancel: raise UserCancelled if result == 1: raise GoBack self.title.setVisible(False) self.back_button.setEnabled(False) self.next_button.setEnabled(False) self.main_widget.setVisible(False) self.please_wait.setVisible(True) self.refresh_gui() return result def refresh_gui(self): # For some reason, to refresh the GUI this needs to be called twice app_state.app.processEvents() app_state.app.processEvents() def text_input(self, title, message, is_valid, allow_multi=False): slayout = KeysLayout(parent=self, title=message, is_valid=is_valid, allow_multi=allow_multi) self.exec_layout(slayout, title, next_enabled=False) return slayout.get_text() def seed_input(self, title, message, is_seed, options): slayout = SeedLayout(title=message, is_seed=is_seed, options=options, parent=self) self.exec_layout(slayout, title, next_enabled=False) return slayout.get_seed(), slayout.is_bip39, slayout.is_ext @wizard_dialog def add_xpub_dialog(self, title, message, is_valid, run_next, allow_multi=False): return self.text_input(title, message, is_valid, allow_multi) @wizard_dialog def add_cosigner_dialog(self, run_next, index, is_valid): title = _("Add Cosigner") + " %d" % index message = ' '.join([ _('Please enter the master public key (xpub) of your cosigner.'), _('Enter their master private key (xprv) if you want to be able to sign for them.' ) ]) return self.text_input(title, message, is_valid) @wizard_dialog def restore_seed_dialog(self, run_next, test): options = [] if self.opt_ext: options.append('ext') if self.opt_bip39: options.append('bip39') title = _('Enter Seed') message = _( 'Please enter your seed phrase in order to restore your wallet.') return self.seed_input(title, message, test, options) @wizard_dialog def confirm_seed_dialog(self, run_next, test): app_state.app.clipboard().clear() title = _('Confirm Seed') message = ' '.join([ _('Your seed is important!'), _('If you lose your seed, your money will be permanently lost.'), _('To make sure that you have properly saved your seed, please retype it here.' ) ]) seed, is_bip39, is_ext = self.seed_input(title, message, test, None) return seed @wizard_dialog def show_seed_dialog(self, run_next, seed_text): title = _("Your wallet generation seed is:") slayout = SeedLayout(seed=seed_text, title=title, msg=True, options=['ext']) self.exec_layout(slayout) return slayout.is_ext def pw_layout(self, msg, kind): playout = PasswordLayout(None, msg, kind, self.next_button) self.exec_layout(playout.layout()) return playout.new_password() @wizard_dialog def request_password(self, run_next): """Request the user enter a new password and confirm it. Return the password or None for no password.""" return self.pw_layout(MSG_ENTER_PASSWORD, PW_NEW) @wizard_dialog def confirm_dialog(self, title, message, run_next): self.confirm(message, title) def confirm(self, message, title): label = WWLabel(message) vbox = QVBoxLayout() vbox.addWidget(label) self.exec_layout(vbox, title) @wizard_dialog def action_dialog(self, action, run_next): self.run(action) def terminate(self): self.accept_signal.emit() @wizard_dialog def choice_dialog(self, title, message, choices, run_next): c_values = [x[0] for x in choices] c_titles = [x[1] for x in choices] clayout = ChoicesLayout(message, c_titles) vbox = QVBoxLayout() vbox.addLayout(clayout.layout()) self.exec_layout(vbox, title) action = c_values[clayout.selected_index()] return action def query_choice(self, msg, choices): """called by hardware wallets""" clayout = ChoicesLayout(msg, choices) vbox = QVBoxLayout() vbox.addLayout(clayout.layout()) self.exec_layout(vbox, '') return clayout.selected_index() @wizard_dialog def line_dialog(self, run_next, title, message, default, test, warning=''): vbox = QVBoxLayout() vbox.addWidget(WWLabel(message)) line = QLineEdit() line.setText(default) def f(text): self.next_button.setEnabled(test(text)) line.textEdited.connect(f) vbox.addWidget(line) vbox.addWidget(WWLabel(warning)) self.exec_layout(vbox, title, next_enabled=test(default)) return ' '.join(line.text().split()) @wizard_dialog def show_xpub_dialog(self, xpub, run_next): msg = ' '.join([ _("Here is your master public key."), _("Please share it with your cosigners.") ]) vbox = QVBoxLayout() layout = SeedLayout(xpub, title=msg, icon=False) vbox.addLayout(layout.layout()) self.exec_layout(vbox, _('Master Public Key')) return None def init_network(self, network): message = _( "ElectrumSV communicates with remote servers to get " "information about your transactions and addresses. The " "servers all fulfil the same purpose only differing in " "hardware. In most cases you simply want to let ElectrumSV " "pick one at random. However if you prefer feel free to " "select a server manually.") choices = [_("Auto connect"), _("Select server manually")] title = _("How do you want to connect to a server? ") clayout = ChoicesLayout(message, choices) self.back_button.setText(_(MSG_BUTTON_CANCEL)) self.exec_layout(clayout.layout(), title) r = clayout.selected_index() app_state.config.set_key('auto_connect', r == 0, True) if r == 1: nlayout = NetworkChoiceLayout(network, app_state.config, wizard=True) if self.exec_layout(nlayout.layout()): nlayout.accept() @wizard_dialog def multisig_dialog(self, run_next): cw = CosignWidget(2, 2) m_edit = QSlider(Qt.Horizontal, self) n_edit = QSlider(Qt.Horizontal, self) n_edit.setMinimum(2) n_edit.setMaximum(15) m_edit.setMinimum(1) m_edit.setMaximum(2) n_edit.setValue(2) m_edit.setValue(2) n_label = QLabel() m_label = QLabel() grid = QGridLayout() grid.addWidget(n_label, 0, 0) grid.addWidget(n_edit, 0, 1) grid.addWidget(m_label, 1, 0) grid.addWidget(m_edit, 1, 1) def on_m(m): m_label.setText(_('Require %d signatures') % m) cw.set_m(m) def on_n(n): n_label.setText(_('From %d cosigners') % n) cw.set_n(n) m_edit.setMaximum(n) n_edit.valueChanged.connect(on_n) m_edit.valueChanged.connect(on_m) on_n(2) on_m(2) vbox = QVBoxLayout() vbox.addWidget(cw) vbox.addWidget( WWLabel( _("Choose the number of signatures needed to unlock " "funds in your wallet:"))) vbox.addLayout(grid) self.exec_layout(vbox, _("Multi-Signature Wallet")) m = int(m_edit.value()) n = int(n_edit.value()) return (m, n)
def run_non_RPC(config): cmdname = config.get('cmd') storage = WalletStorage(config.get_wallet_path()) if storage.file_exists(): sys.exit("Error: Remove the existing wallet first!") def password_dialog(): return prompt_password( "Password (hit return if you do not wish to encrypt your wallet):") if cmdname == 'restore': text = config.get('text').strip() passphrase = config.get('passphrase', '') password = password_dialog() if keystore.is_private(text) else None parent_wallet = ParentWallet.as_legacy_wallet_container(storage) if keystore.is_address_list(text): legacy_wallet = ImportedAddressWallet.from_text( parent_wallet, text) elif keystore.is_private_key_list(text): legacy_wallet = ImportedPrivkeyWallet.from_text( parent_wallet, text) else: if keystore.is_seed(text): k = keystore.from_seed(text, passphrase, False) elif keystore.is_master_key(text): k = keystore.from_master_key(text) else: sys.exit("Error: Seed or key not recognized") keystore_usage = parent_wallet.add_keystore(k.dump()) Standard_Wallet.create_within_parent( parent_wallet, keystore_usage=[keystore_usage]) if password: parent_wallet.update_password(None, password) if not config.get('offline'): network = Network() network.add_wallet(parent_wallet) print("Recovering wallet...") parent_wallet.synchronize() msg = ("Recovery successful" if parent_wallet.has_usage() else "Found no history for this wallet") else: msg = ("This wallet was restored offline. " "It may contain more addresses than displayed.") print(msg) elif cmdname == 'create': password = password_dialog() passphrase = config.get('passphrase', '') seed_type = 'standard' seed = Mnemonic('en').make_seed(seed_type) k = keystore.from_seed(seed, passphrase, False) parent_wallet = ParentWallet.as_legacy_wallet_container(storage) keystore_usage = parent_wallet.add_keystore(k.dump()) Standard_Wallet.create_within_parent(parent_wallet, keystore_usage=[keystore_usage]) parent_wallet.update_password(None, password) parent_wallet.synchronize() print("Your wallet generation seed is:\n\"%s\"" % seed) print("Please keep it in a safe place; if you lose it, " "you will not be able to restore your wallet.") parent_wallet.save_storage() print("Wallet saved in '%s'" % parent_wallet.get_storage_path()) sys.exit(0)
def __init__(self, parent: QWidget, parent_wallet: ParentWallet, address: Address) -> None: assert isinstance(address, Address) WindowModalDialog.__init__(self, parent, _("Address")) self.address = address self.parent = parent self.config = parent.config self.parent_wallet = parent_wallet wallet = parent_wallet.get_wallet_for_address(address) assert wallet is not None self.wallet = wallet self.app = parent.app self.saved = True self.setMinimumWidth(700) vbox = QVBoxLayout() self.setLayout(vbox) vbox.addWidget(QLabel(_("Address:"))) self.addr_e = ButtonsLineEdit() self.addr_e.addCopyButton(self.app) icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png" self.addr_e.addButton(icon, self.show_qr, _("Show QR Code")) self.addr_e.setReadOnly(True) vbox.addWidget(self.addr_e) self.update_addr() try: # the below line only works for deterministic wallets, other wallets lack this method pubkeys = self.wallet.get_public_keys(address) except Exception: try: # ok, now try the usual method for imported wallets, etc pubkey = self.wallet.get_public_key(address) pubkeys = [pubkey.to_string()] except Exception: # watching only wallets (totally lacks a private/public key pair for this address) pubkeys = None if pubkeys: vbox.addWidget(QLabel(_("Public keys") + ':')) for pubkey in pubkeys: pubkey_e = ButtonsLineEdit(pubkey) pubkey_e.addCopyButton(self.app) vbox.addWidget(pubkey_e) try: redeem_script = self.wallet.pubkeys_to_redeem_script(pubkeys) except AttributeError as e: pass else: vbox.addWidget(QLabel(_("Redeem Script") + ':')) redeem_e = ShowQRTextEdit(text=redeem_script.hex()) redeem_e.addCopyButton(self.app) vbox.addWidget(redeem_e) vbox.addWidget(QLabel(_("History"))) self.hw = HistoryList(self.parent, wallet) self.hw.get_domain = self.get_domain vbox.addWidget(self.hw) vbox.addLayout(Buttons(CloseButton(self))) self.format_amount = self.parent.format_amount self.hw.update() # connect slots so the embedded history list gets updated whenever the history changes parent.history_updated_signal.connect(self.hw.update) parent.network_signal.connect(self.got_verified_tx)