Browse Source

wallet: make importing thousands of addr/privkeys fast

fixes #3101
closes #3106
closes #3113
3.3.3.1
SomberNight 6 years ago
parent
commit
34569d172f
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 14
      electrum/base_wizard.py
  2. 16
      electrum/commands.py
  3. 29
      electrum/gui/qt/main_window.py
  4. 6
      electrum/tests/test_wallet_vertical.py
  5. 73
      electrum/wallet.py

14
electrum/base_wizard.py

@ -189,17 +189,23 @@ class BaseWizard(object):
# will be reflected on self.storage # will be reflected on self.storage
if keystore.is_address_list(text): if keystore.is_address_list(text):
w = Imported_Wallet(self.storage) w = Imported_Wallet(self.storage)
for x in text.split(): addresses = text.split()
w.import_address(x) good_inputs, bad_inputs = w.import_addresses(addresses)
elif keystore.is_private_key_list(text): elif keystore.is_private_key_list(text):
k = keystore.Imported_KeyStore({}) k = keystore.Imported_KeyStore({})
self.storage.put('keystore', k.dump()) self.storage.put('keystore', k.dump())
w = Imported_Wallet(self.storage) w = Imported_Wallet(self.storage)
for x in keystore.get_private_keys(text): keys = keystore.get_private_keys(text)
w.import_private_key(x, None) good_inputs, bad_inputs = w.import_private_keys(keys, None)
self.keystores.append(w.keystore) self.keystores.append(w.keystore)
else: else:
return self.terminate() 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') return self.run('create_wallet')
def restore_from_key(self): def restore_from_key(self):

16
electrum/commands.py

@ -166,14 +166,20 @@ class Commands:
text = text.strip() text = text.strip()
if keystore.is_address_list(text): if keystore.is_address_list(text):
wallet = Imported_Wallet(storage) wallet = Imported_Wallet(storage)
for x in text.split(): addresses = text.split()
wallet.import_address(x) 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): elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
k = keystore.Imported_KeyStore({}) k = keystore.Imported_KeyStore({})
storage.put('keystore', k.dump()) storage.put('keystore', k.dump())
wallet = Imported_Wallet(storage) wallet = Imported_Wallet(storage)
for x in text.split(): keys = keystore.get_private_keys(text)
wallet.import_private_key(x, password) 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: else:
if keystore.is_seed(text): if keystore.is_seed(text):
k = keystore.from_seed(text, passphrase) k = keystore.from_seed(text, passphrase)
@ -435,7 +441,7 @@ class Commands:
try: try:
addr = self.wallet.import_private_key(privkey, password) addr = self.wallet.import_private_key(privkey, password)
out = "Keypair imported: " + addr out = "Keypair imported: " + addr
except BaseException as e: except Exception as e:
out = "Error: " + str(e) out = "Error: " + str(e)
return out return out

29
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) text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True)
if not text: if not text:
return return
bad = [] keys = str(text).split()
good = [] good_inputs, bad_inputs = func(keys)
for key in str(text).split(): if good_inputs:
try: msg = '\n'.join(good_inputs[:10])
addr = func(key) if len(good_inputs) > 10: msg += '\n...'
good.append(addr) self.show_message(_("The following addresses were added")
except BaseException as e: + f' ({len(good_inputs)}):\n' + msg)
bad.append(key) if bad_inputs:
continue msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10])
if good: if len(bad_inputs) > 10: msg += '\n...'
self.show_message(_("The following addresses were added") + ':\n' + '\n'.join(good)) self.show_error(_("The following inputs could not be imported")
if bad: + f' ({len(bad_inputs)}):\n' + msg)
self.show_critical(_("The following inputs could not be imported") + ':\n'+ '\n'.join(bad))
self.address_list.update() self.address_list.update()
self.history_list.update() self.history_list.update()
@ -2632,7 +2631,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if not self.wallet.can_import_address(): if not self.wallet.can_import_address():
return return
title, msg = _('Import addresses'), _("Enter addresses")+':' 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 @protected
def do_import_privkey(self, password): def do_import_privkey(self, password):
@ -2642,7 +2641,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
header_layout = QHBoxLayout() header_layout = QHBoxLayout()
header_layout.addWidget(QLabel(_("Enter private keys")+':')) header_layout.addWidget(QLabel(_("Enter private keys")+':'))
header_layout.addWidget(InfoButton(WIF_HELP_TEXT), alignment=Qt.AlignRight) 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): def update_fiat(self):
b = self.fx and self.fx.is_enabled() b = self.fx and self.fx.is_enabled()

6
electrum/tests/test_wallet_vertical.py

@ -1158,7 +1158,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(storage.WalletStorage, '_write')
def test_sending_offline_wif_online_addr_p2pkh(self, mock_write): # compressed pubkey def test_sending_offline_wif_online_addr_p2pkh(self, mock_write): # compressed pubkey
wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) 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 = WalletIntegrityHelper.create_imported_wallet(privkeys=False)
wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG') wallet_online.import_address('mg2jk6S5WGDhUPA8mLSxDLWpUoQnX1zzoG')
@ -1192,7 +1192,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(storage.WalletStorage, '_write')
def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_write): def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_write):
wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) 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 = WalletIntegrityHelper.create_imported_wallet(privkeys=False)
wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8') wallet_online.import_address('2NA2JbUVK7HGWUCK5RXSVNHrkgUYF8d9zV8')
@ -1226,7 +1226,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(storage.WalletStorage, '_write')
def test_sending_offline_wif_online_addr_p2wpkh(self, mock_write): def test_sending_offline_wif_online_addr_p2wpkh(self, mock_write):
wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True) 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 = WalletIntegrityHelper.create_imported_wallet(privkeys=False)
wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529') wallet_online.import_address('tb1qm2eh4787lwanrzr6pf0ekf5c7jnmghm2y9k529')

73
electrum/wallet.py

@ -38,7 +38,7 @@ import traceback
from functools import partial from functools import partial
from numbers import Number from numbers import Number
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, List, Optional, Tuple
from .i18n import _ from .i18n import _
from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler, from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler,
@ -1227,16 +1227,29 @@ class Imported_Wallet(Simple_Wallet):
def get_change_addresses(self): def get_change_addresses(self):
return [] return []
def import_address(self, address): def import_addresses(self, addresses: List[str]) -> Tuple[List[str], List[Tuple[str, str]]]:
if not bitcoin.is_address(address): good_addr = [] # type: List[str]
return '' bad_addr = [] # type: List[Tuple[str, str]]
if address in self.addresses: for address in addresses:
return '' if not bitcoin.is_address(address):
self.addresses[address] = {} bad_addr.append((address, _('invalid address')))
self.add_address(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_addresses()
self.save_transactions(write=True) 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): def delete_address(self, address):
if address not in self.addresses: if address not in self.addresses:
@ -1293,28 +1306,34 @@ class Imported_Wallet(Simple_Wallet):
def get_public_key(self, address): def get_public_key(self, address):
return self.addresses[address].get('pubkey') return self.addresses[address].get('pubkey')
def import_private_key(self, sec, pw, redeem_script=None): def import_private_keys(self, keys: List[str], password: Optional[str]) -> Tuple[List[str],
try: List[Tuple[str, str]]]:
txin_type, pubkey = self.keystore.import_privkey(sec, pw) good_addr = [] # type: List[str]
except Exception: bad_keys = [] # type: List[Tuple[str, str]]
neutered_privkey = str(sec)[:3] + '..' + str(sec)[-2:] for key in keys:
raise BitcoinException('Invalid private key: {}'.format(neutered_privkey)) try:
if txin_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: txin_type, pubkey = self.keystore.import_privkey(key, password)
if redeem_script is not None: except Exception:
raise BitcoinException('Cannot use redeem script with script type {}'.format(txin_type)) 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) addr = bitcoin.pubkey_to_address(txin_type, pubkey)
elif txin_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: good_addr.append(addr)
if redeem_script is None: self.addresses[addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None}
raise BitcoinException('Redeem script required for script type {}'.format(txin_type)) self.add_address(addr)
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}
self.save_keystore() self.save_keystore()
self.add_address(addr)
self.save_addresses() self.save_addresses()
self.save_transactions(write=True) 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): def get_redeem_script(self, address):
d = self.addresses[address] d = self.addresses[address]

Loading…
Cancel
Save