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
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):

16
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

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)
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()

6
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')

73
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]

Loading…
Cancel
Save