commit 34569d172ff797047d11ae6f6bb6f95a9189879b
parent 917b7fa898479a80d19633279ced4d539819b8c2
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:
5 files changed, 84 insertions(+), 54 deletions(-)
diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
@@ -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):
diff --git a/electrum/commands.py b/electrum/commands.py
@@ -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)
@@ -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
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -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()
@@ -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):
@@ -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()
diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py
@@ -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')
@@ -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')
@@ -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')
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -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,
@@ -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:
@@ -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]