URI: 
       twallet: make importing thousands of addr/privkeys fast - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 34569d172ff797047d11ae6f6bb6f95a9189879b
   DIR parent 917b7fa898479a80d19633279ced4d539819b8c2
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Sat, 27 Oct 2018 17:36:10 +0200
       
       wallet: make importing thousands of addr/privkeys fast
       
       fixes #3101
       closes #3106
       closes #3113
       
       Diffstat:
         M electrum/base_wizard.py             |      14 ++++++++++----
         M electrum/commands.py                |      16 +++++++++++-----
         M electrum/gui/qt/main_window.py      |      29 ++++++++++++++---------------
         M electrum/tests/test_wallet_vertica… |       6 +++---
         M electrum/wallet.py                  |      73 +++++++++++++++++++------------
       
       5 files changed, 84 insertions(+), 54 deletions(-)
       ---
   DIR diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
       t@@ -189,17 +189,23 @@ class BaseWizard(object):
                # will be reflected on self.storage
                if keystore.is_address_list(text):
                    w = Imported_Wallet(self.storage)
       -            for x in text.split():
       -                w.import_address(x)
       +            addresses = text.split()
       +            good_inputs, bad_inputs = w.import_addresses(addresses)
                elif keystore.is_private_key_list(text):
                    k = keystore.Imported_KeyStore({})
                    self.storage.put('keystore', k.dump())
                    w = Imported_Wallet(self.storage)
       -            for x in keystore.get_private_keys(text):
       -                w.import_private_key(x, None)
       +            keys = keystore.get_private_keys(text)
       +            good_inputs, bad_inputs = w.import_private_keys(keys, None)
                    self.keystores.append(w.keystore)
                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):
   DIR diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -166,14 +166,20 @@ class Commands:
                text = text.strip()
                if keystore.is_address_list(text):
                    wallet = Imported_Wallet(storage)
       -            for x in text.split():
       -                wallet.import_address(x)
       +            addresses = text.split()
       +            good_inputs, bad_inputs = wallet.import_addresses(addresses)
       +            # FIXME tell user about bad_inputs
       +            if not good_inputs:
       +                raise Exception("None of the given addresses can be imported")
                elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
                    k = keystore.Imported_KeyStore({})
                    storage.put('keystore', k.dump())
                    wallet = Imported_Wallet(storage)
       -            for x in text.split():
       -                wallet.import_private_key(x, password)
       +            keys = keystore.get_private_keys(text)
       +            good_inputs, bad_inputs = wallet.import_private_keys(keys, password)
       +            # FIXME tell user about bad_inputs
       +            if not good_inputs:
       +                raise Exception("None of the given privkeys can be imported")
                else:
                    if keystore.is_seed(text):
                        k = keystore.from_seed(text, passphrase)
       t@@ -435,7 +441,7 @@ class Commands:
                try:
                    addr = self.wallet.import_private_key(privkey, password)
                    out = "Keypair imported: " + addr
       -        except BaseException as e:
       +        except Exception as e:
                    out = "Error: " + str(e)
                return out
        
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -2612,19 +2612,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True)
                if not text:
                    return
       -        bad = []
       -        good = []
       -        for key in str(text).split():
       -            try:
       -                addr = func(key)
       -                good.append(addr)
       -            except BaseException as e:
       -                bad.append(key)
       -                continue
       -        if good:
       -            self.show_message(_("The following addresses were added") + ':\n' + '\n'.join(good))
       -        if bad:
       -            self.show_critical(_("The following inputs could not be imported") + ':\n'+ '\n'.join(bad))
       +        keys = str(text).split()
       +        good_inputs, bad_inputs = func(keys)
       +        if good_inputs:
       +            msg = '\n'.join(good_inputs[:10])
       +            if len(good_inputs) > 10: msg += '\n...'
       +            self.show_message(_("The following addresses were added")
       +                              + f' ({len(good_inputs)}):\n' + msg)
       +        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)
                self.address_list.update()
                self.history_list.update()
        
       t@@ -2632,7 +2631,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                if not self.wallet.can_import_address():
                    return
                title, msg = _('Import addresses'), _("Enter addresses")+':'
       -        self._do_import(title, msg, self.wallet.import_address)
       +        self._do_import(title, msg, self.wallet.import_addresses)
        
            @protected
            def do_import_privkey(self, password):
       t@@ -2642,7 +2641,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                header_layout = QHBoxLayout()
                header_layout.addWidget(QLabel(_("Enter private keys")+':'))
                header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight)
       -        self._do_import(title, header_layout, lambda x: self.wallet.import_private_key(x, password))
       +        self._do_import(title, header_layout, lambda x: self.wallet.import_private_keys(x, password))
        
            def update_fiat(self):
                b = self.fx and self.fx.is_enabled()
   DIR diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py
       t@@ -1158,7 +1158,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
            @mock.patch.object(storage.WalletStorage, '_write')
            def test_sending_offline_wif_online_addr_p2pkh(self, mock_write):  # compressed pubkey
                wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True)
       -        wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', pw=None)
       +        wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', password=None)
                wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False)
                wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG')
        
       t@@ -1192,7 +1192,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
            @mock.patch.object(storage.WalletStorage, '_write')
            def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_write):
                wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True)
       -        wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', pw=None)
       +        wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', password=None)
                wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False)
                wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8')
        
       t@@ -1226,7 +1226,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
            @mock.patch.object(storage.WalletStorage, '_write')
            def test_sending_offline_wif_online_addr_p2wpkh(self, mock_write):
                wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True)
       -        wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', pw=None)
       +        wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', password=None)
                wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False)
                wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529')
        
   DIR diff --git a/electrum/wallet.py b/electrum/wallet.py
       t@@ -38,7 +38,7 @@ import traceback
        from functools import partial
        from numbers import Number
        from decimal import Decimal
       -from typing import TYPE_CHECKING
       +from typing import TYPE_CHECKING, List, Optional, Tuple
        
        from .i18n import _
        from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler,
       t@@ -1227,16 +1227,29 @@ class Imported_Wallet(Simple_Wallet):
            def get_change_addresses(self):
                return []
        
       -    def import_address(self, address):
       -        if not bitcoin.is_address(address):
       -            return ''
       -        if address in self.addresses:
       -            return ''
       -        self.addresses[address] = {}
       -        self.add_address(address)
       +    def import_addresses(self, addresses: List[str]) -> Tuple[List[str], List[Tuple[str, str]]]:
       +        good_addr = []  # type: List[str]
       +        bad_addr = []  # type: List[Tuple[str, str]]
       +        for address in addresses:
       +            if not bitcoin.is_address(address):
       +                bad_addr.append((address, _('invalid address')))
       +                continue
       +            if address in self.addresses:
       +                bad_addr.append((address, _('address already in wallet')))
       +                continue
       +            good_addr.append(address)
       +            self.addresses[address] = {}
       +            self.add_address(address)
                self.save_addresses()
                self.save_transactions(write=True)
       -        return address
       +        return good_addr, bad_addr
       +
       +    def import_address(self, address: str) -> str:
       +        good_addr, bad_addr = self.import_addresses([address])
       +        if good_addr and good_addr[0] == address:
       +            return address
       +        else:
       +            raise BitcoinException(str(bad_addr[0][1]))
        
            def delete_address(self, address):
                if address not in self.addresses:
       t@@ -1293,28 +1306,34 @@ class Imported_Wallet(Simple_Wallet):
            def get_public_key(self, address):
                return self.addresses[address].get('pubkey')
        
       -    def import_private_key(self, sec, pw, redeem_script=None):
       -        try:
       -            txin_type, pubkey = self.keystore.import_privkey(sec, pw)
       -        except Exception:
       -            neutered_privkey = str(sec)[:3] + '..' + str(sec)[-2:]
       -            raise BitcoinException('Invalid private key: {}'.format(neutered_privkey))
       -        if txin_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
       -            if redeem_script is not None:
       -                raise BitcoinException('Cannot use redeem script with script type {}'.format(txin_type))
       +    def import_private_keys(self, keys: List[str], password: Optional[str]) -> Tuple[List[str],
       +                                                                                     List[Tuple[str, str]]]:
       +        good_addr = []  # type: List[str]
       +        bad_keys = []  # type: List[Tuple[str, str]]
       +        for key in keys:
       +            try:
       +                txin_type, pubkey = self.keystore.import_privkey(key, password)
       +            except Exception:
       +                bad_keys.append((key, _('invalid private key')))
       +                continue
       +            if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
       +                bad_keys.append((key, _('not implemented type') + f': {txin_type}'))
       +                continue
                    addr = bitcoin.pubkey_to_address(txin_type, pubkey)
       -        elif txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
       -            if redeem_script is None:
       -                raise BitcoinException('Redeem script required for script type {}'.format(txin_type))
       -            addr = bitcoin.redeem_script_to_address(txin_type, redeem_script)
       -        else:
       -            raise NotImplementedError(txin_type)
       -        self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':redeem_script}
       +            good_addr.append(addr)
       +            self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None}
       +            self.add_address(addr)
                self.save_keystore()
       -        self.add_address(addr)
                self.save_addresses()
                self.save_transactions(write=True)
       -        return addr
       +        return good_addr, bad_keys
       +
       +    def import_private_key(self, key: str, password: Optional[str]) -> str:
       +        good_addr, bad_keys = self.import_private_keys([key], password=password)
       +        if good_addr:
       +            return good_addr[0]
       +        else:
       +            raise BitcoinException(str(bad_keys[0][1]))
        
            def get_redeem_script(self, address):
                d = self.addresses[address]