Separate db from storage

- storage is content-agnostic
 - db and storage are passed to wallet contructor
This commit is contained in:
ThomasV 2020-02-05 15:13:37 +01:00
parent c61e5db6a9
commit e1ce3aace7
25 changed files with 421 additions and 421 deletions

View file

@ -39,6 +39,7 @@ from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet,
wallet_types, Wallet, Abstract_Wallet) wallet_types, Wallet, Abstract_Wallet)
from .storage import (WalletStorage, StorageEncryptionVersion, from .storage import (WalletStorage, StorageEncryptionVersion,
get_derivation_used_for_hw_device_encryption) get_derivation_used_for_hw_device_encryption)
from .wallet_db import WalletDB
from .i18n import _ from .i18n import _
from .util import UserCancelled, InvalidPassword, WalletFileException from .util import UserCancelled, InvalidPassword, WalletFileException
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
@ -64,7 +65,7 @@ class WizardStackItem(NamedTuple):
action: Any action: Any
args: Any args: Any
kwargs: Dict[str, Any] kwargs: Dict[str, Any]
storage_data: dict db_data: dict
class WizardWalletPasswordSetting(NamedTuple): class WizardWalletPasswordSetting(NamedTuple):
@ -95,8 +96,8 @@ class BaseWizard(Logger):
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
action = args[0] action = args[0]
args = args[1:] args = args[1:]
storage_data = copy.deepcopy(self.data) db_data = copy.deepcopy(self.data)
self._stack.append(WizardStackItem(action, args, kwargs, storage_data)) self._stack.append(WizardStackItem(action, args, kwargs, db_data))
if not action: if not action:
return return
if type(action) is tuple: if type(action) is tuple:
@ -122,7 +123,7 @@ class BaseWizard(Logger):
stack_item = self._stack.pop() stack_item = self._stack.pop()
# try to undo side effects since we last entered 'previous' frame # try to undo side effects since we last entered 'previous' frame
# FIXME only self.storage is properly restored # FIXME only self.storage is properly restored
self.data = copy.deepcopy(stack_item.storage_data) self.data = copy.deepcopy(stack_item.db_data)
# rerun 'previous' frame # rerun 'previous' frame
self.run(stack_item.action, *stack_item.args, **stack_item.kwargs) self.run(stack_item.action, *stack_item.args, **stack_item.kwargs)
@ -143,17 +144,17 @@ class BaseWizard(Logger):
choices = [pair for pair in wallet_kinds if pair[0] in wallet_types] choices = [pair for pair in wallet_kinds if pair[0] in wallet_types]
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type) self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type)
def upgrade_storage(self, storage): def upgrade_db(self, storage, db):
exc = None exc = None
def on_finished(): def on_finished():
if exc is None: if exc is None:
self.terminate(storage=storage) self.terminate(storage=storage, db=db)
else: else:
raise exc raise exc
def do_upgrade(): def do_upgrade():
nonlocal exc nonlocal exc
try: try:
storage.upgrade() db.upgrade()
except Exception as e: except Exception as e:
exc = e exc = e
self.waiting_dialog(do_upgrade, _('Upgrading wallet format...'), on_finished=on_finished) self.waiting_dialog(do_upgrade, _('Upgrading wallet format...'), on_finished=on_finished)
@ -592,6 +593,7 @@ class BaseWizard(Logger):
encrypt_keystore=encrypt_keystore) encrypt_keystore=encrypt_keystore)
self.terminate() self.terminate()
def create_storage(self, path): def create_storage(self, path):
if os.path.exists(path): if os.path.exists(path):
raise Exception('file already exists at path') raise Exception('file already exists at path')
@ -600,16 +602,17 @@ class BaseWizard(Logger):
pw_args = self.pw_args pw_args = self.pw_args
self.pw_args = None # clean-up so that it can get GC-ed self.pw_args = None # clean-up so that it can get GC-ed
storage = WalletStorage(path) storage = WalletStorage(path)
storage.set_keystore_encryption(bool(pw_args.password) and pw_args.encrypt_keystore)
if pw_args.encrypt_storage: if pw_args.encrypt_storage:
storage.set_password(pw_args.password, enc_version=pw_args.storage_enc_version) storage.set_password(pw_args.password, enc_version=pw_args.storage_enc_version)
db = WalletDB('', manual_upgrades=False)
db.set_keystore_encryption(bool(pw_args.password) and pw_args.encrypt_keystore)
for key, value in self.data.items(): for key, value in self.data.items():
storage.put(key, value) db.put(key, value)
storage.write() db.load_plugins()
storage.load_plugins() db.write(storage)
return storage return storage, db
def terminate(self, *, storage: Optional[WalletStorage] = None): def terminate(self, *, storage: Optional[WalletStorage], db: Optional[WalletDB] = None):
raise NotImplementedError() # implemented by subclasses raise NotImplementedError() # implemented by subclasses
def show_xpub_and_add_cosigners(self, xpub): def show_xpub_and_add_cosigners(self, xpub):

View file

@ -262,13 +262,13 @@ class Commands:
raise Exception("Can't change the password of a wallet encrypted with a hw device.") raise Exception("Can't change the password of a wallet encrypted with a hw device.")
b = wallet.storage.is_encrypted() b = wallet.storage.is_encrypted()
wallet.update_password(password, new_password, encrypt_storage=b) wallet.update_password(password, new_password, encrypt_storage=b)
wallet.storage.write() wallet.save_db()
return {'password':wallet.has_password()} return {'password':wallet.has_password()}
@command('w') @command('w')
async def get(self, key, wallet: Abstract_Wallet = None): async def get(self, key, wallet: Abstract_Wallet = None):
"""Return item from wallet storage""" """Return item from wallet storage"""
return wallet.storage.get(key) return wallet.db.get(key)
@command('') @command('')
async def getconfig(self, key): async def getconfig(self, key):
@ -830,7 +830,7 @@ class Commands:
tx = Transaction(tx) tx = Transaction(tx)
if not wallet.add_transaction(tx): if not wallet.add_transaction(tx):
return False return False
wallet.storage.write() wallet.save_db()
return tx.txid() return tx.txid()
@command('wp') @command('wp')
@ -906,7 +906,7 @@ class Commands:
to_delete |= wallet.get_depending_transactions(txid) to_delete |= wallet.get_depending_transactions(txid)
for tx_hash in to_delete: for tx_hash in to_delete:
wallet.remove_transaction(tx_hash) wallet.remove_transaction(tx_hash)
wallet.storage.write() wallet.save_db()
@command('wn') @command('wn')
async def get_tx_status(self, txid, wallet: Abstract_Wallet = None): async def get_tx_status(self, txid, wallet: Abstract_Wallet = None):

View file

@ -33,10 +33,10 @@ from .logging import Logger
class Contacts(dict, Logger): class Contacts(dict, Logger):
def __init__(self, storage): def __init__(self, db):
Logger.__init__(self) Logger.__init__(self)
self.storage = storage self.db = db
d = self.storage.get('contacts', {}) d = self.db.get('contacts', {})
try: try:
self.update(d) self.update(d)
except: except:
@ -49,7 +49,7 @@ class Contacts(dict, Logger):
self[n] = ('address', k) self[n] = ('address', k)
def save(self): def save(self):
self.storage.put('contacts', dict(self)) self.db.put('contacts', dict(self))
def import_file(self, path): def import_file(self, path):
import_meta(path, self._validate, self.load_meta) import_meta(path, self._validate, self.load_meta)

View file

@ -47,6 +47,7 @@ from .util import PR_PAID, PR_EXPIRED, get_request_status
from .util import log_exceptions, ignore_exceptions from .util import log_exceptions, ignore_exceptions
from .wallet import Wallet, Abstract_Wallet from .wallet import Wallet, Abstract_Wallet
from .storage import WalletStorage from .storage import WalletStorage
from .wallet_db import WalletDB
from .commands import known_commands, Commands from .commands import known_commands, Commands
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
from .exchange_rate import FxThread from .exchange_rate import FxThread
@ -401,20 +402,22 @@ class Daemon(Logger):
if path in self._wallets: if path in self._wallets:
wallet = self._wallets[path] wallet = self._wallets[path]
return wallet return wallet
storage = WalletStorage(path, manual_upgrades=manual_upgrades) storage = WalletStorage(path)
if not storage.file_exists(): if not storage.file_exists():
return return
if storage.is_encrypted(): if storage.is_encrypted():
if not password: if not password:
return return
storage.decrypt(password) storage.decrypt(password)
if storage.requires_split(): # read data, pass it to db
db = WalletDB(storage.read(), manual_upgrades=manual_upgrades)
if db.requires_split():
return return
if storage.requires_upgrade(): if db.requires_upgrade():
return return
if storage.get_action(): if db.get_action():
return return
wallet = Wallet(storage, config=self.config) wallet = Wallet(db, storage, config=self.config)
wallet.start_network(self.network) wallet.start_network(self.network)
self._wallets[path] = wallet self._wallets[path] = wallet
self.wallet = wallet self.wallet = wallet

View file

@ -10,6 +10,7 @@ import asyncio
from typing import TYPE_CHECKING, Optional, Union, Callable from typing import TYPE_CHECKING, Optional, Union, Callable
from electrum.storage import WalletStorage, StorageReadWriteError from electrum.storage import WalletStorage, StorageReadWriteError
from electrum.wallet_db import WalletDB
from electrum.wallet import Wallet, InternalAddressCorruption, Abstract_Wallet from electrum.wallet import Wallet, InternalAddressCorruption, Abstract_Wallet
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter, from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
@ -166,8 +167,8 @@ class ElectrumWindow(App):
def on_use_change(self, instance, x): def on_use_change(self, instance, x):
if self.wallet: if self.wallet:
self.wallet.use_change = self.use_change self.wallet.use_change = self.use_change
self.wallet.storage.put('use_change', self.use_change) self.wallet.db.put('use_change', self.use_change)
self.wallet.storage.write() self.wallet.save_db()
use_unconfirmed = BooleanProperty(False) use_unconfirmed = BooleanProperty(False)
def on_use_unconfirmed(self, instance, x): def on_use_unconfirmed(self, instance, x):
@ -588,9 +589,9 @@ class ElectrumWindow(App):
else: else:
return '' return ''
def on_wizard_complete(self, wizard, storage): def on_wizard_complete(self, wizard, storage, db):
if storage: if storage:
wallet = Wallet(storage, config=self.electrum_config) wallet = Wallet(db, storage, config=self.electrum_config)
wallet.start_network(self.daemon.network) wallet.start_network(self.daemon.network)
self.daemon.add_wallet(wallet) self.daemon.add_wallet(wallet)
self.load_wallet(wallet) self.load_wallet(wallet)
@ -602,13 +603,14 @@ class ElectrumWindow(App):
def _on_decrypted_storage(self, storage: WalletStorage): def _on_decrypted_storage(self, storage: WalletStorage):
assert storage.is_past_initial_decryption() assert storage.is_past_initial_decryption()
if storage.requires_upgrade(): db = WalletDB(storage.read(), manual_upgrades=False)
if db.requires_upgrade():
wizard = Factory.InstallWizard(self.electrum_config, self.plugins) wizard = Factory.InstallWizard(self.electrum_config, self.plugins)
wizard.path = storage.path wizard.path = storage.path
wizard.bind(on_wizard_complete=self.on_wizard_complete) wizard.bind(on_wizard_complete=self.on_wizard_complete)
wizard.upgrade_storage(storage) wizard.upgrade_storage(storage, db)
else: else:
self.on_wizard_complete(wizard=None, storage=storage) self.on_wizard_complete(None, storage, db)
def load_wallet_by_name(self, path, ask_if_wizard=False): def load_wallet_by_name(self, path, ask_if_wizard=False):
if not path: if not path:
@ -624,7 +626,7 @@ class ElectrumWindow(App):
self.load_wallet(wallet) self.load_wallet(wallet)
else: else:
def launch_wizard(): def launch_wizard():
storage = WalletStorage(path, manual_upgrades=True) storage = WalletStorage(path)
if not storage.file_exists(): if not storage.file_exists():
wizard = Factory.InstallWizard(self.electrum_config, self.plugins) wizard = Factory.InstallWizard(self.electrum_config, self.plugins)
wizard.path = path wizard.path = path

View file

@ -633,7 +633,7 @@ class WizardDialog(EventsDialog):
self._on_release = True self._on_release = True
self.close() self.close()
if not button: if not button:
self.parent.dispatch('on_wizard_complete', None) self.parent.dispatch('on_wizard_complete', None, None)
return return
if button is self.ids.back: if button is self.ids.back:
self.wizard.go_back() self.wizard.go_back()
@ -1055,7 +1055,7 @@ class InstallWizard(BaseWizard, Widget):
__events__ = ('on_wizard_complete', ) __events__ = ('on_wizard_complete', )
def on_wizard_complete(self, wallet): def on_wizard_complete(self, storage, db):
"""overriden by main_window""" """overriden by main_window"""
pass pass
@ -1086,10 +1086,10 @@ class InstallWizard(BaseWizard, Widget):
t = threading.Thread(target = target) t = threading.Thread(target = target)
t.start() t.start()
def terminate(self, *, storage=None, aborted=False): def terminate(self, *, storage=None, db=None, aborted=False):
if storage is None and not aborted: if storage is None and not aborted:
storage = self.create_storage(self.path) storage, db = self.create_storage(self.path)
self.dispatch('on_wizard_complete', storage) self.dispatch('on_wizard_complete', storage, db)
def choice_dialog(self, **kwargs): def choice_dialog(self, **kwargs):
choices = kwargs['choices'] choices = kwargs['choices']

View file

@ -48,6 +48,7 @@ from electrum.base_wizard import GoBack
from electrum.util import (UserCancelled, profiler, from electrum.util import (UserCancelled, profiler,
WalletFileException, BitcoinException, get_new_wallet_name) WalletFileException, BitcoinException, get_new_wallet_name)
from electrum.wallet import Wallet, Abstract_Wallet from electrum.wallet import Wallet, Abstract_Wallet
from electrum.wallet_db import WalletDB
from electrum.logging import Logger from electrum.logging import Logger
from .installwizard import InstallWizard, WalletAlreadyOpenInMemory from .installwizard import InstallWizard, WalletAlreadyOpenInMemory
@ -306,9 +307,10 @@ class ElectrumGui(Logger):
if storage is None: if storage is None:
wizard.path = path # needed by trustedcoin plugin wizard.path = path # needed by trustedcoin plugin
wizard.run('new') wizard.run('new')
storage = wizard.create_storage(path) storage, db = wizard.create_storage(path)
else: else:
wizard.run_upgrades(storage) db = WalletDB(storage.read(), manual_upgrades=False)
wizard.run_upgrades(storage, db)
except (UserCancelled, GoBack): except (UserCancelled, GoBack):
return return
except WalletAlreadyOpenInMemory as e: except WalletAlreadyOpenInMemory as e:
@ -316,9 +318,9 @@ class ElectrumGui(Logger):
finally: finally:
wizard.terminate() wizard.terminate()
# return if wallet creation is not complete # return if wallet creation is not complete
if storage is None or storage.get_action(): if storage is None or db.get_action():
return return
wallet = Wallet(storage, config=self.config) wallet = Wallet(db, storage, config=self.config)
wallet.start_network(self.daemon.network) wallet.start_network(self.daemon.network)
self.daemon.add_wallet(wallet) self.daemon.add_wallet(wallet)
return wallet return wallet

View file

@ -3,6 +3,7 @@
# file LICENCE or http://www.opensource.org/licenses/mit-license.php # file LICENCE or http://www.opensource.org/licenses/mit-license.php
import os import os
import json
import sys import sys
import threading import threading
import traceback import traceback
@ -225,7 +226,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
if wallet_from_memory: if wallet_from_memory:
temp_storage = wallet_from_memory.storage # type: Optional[WalletStorage] temp_storage = wallet_from_memory.storage # type: Optional[WalletStorage]
else: else:
temp_storage = WalletStorage(path, manual_upgrades=True) temp_storage = WalletStorage(path)
except (StorageReadWriteError, WalletFileException) as e: except (StorageReadWriteError, WalletFileException) as e:
msg = _('Cannot read file') + f'\n{repr(e)}' msg = _('Cannot read file') + f'\n{repr(e)}'
except Exception as e: except Exception as e:
@ -316,24 +317,24 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
return temp_storage.path, (temp_storage if temp_storage.file_exists() else None) return temp_storage.path, (temp_storage if temp_storage.file_exists() else None)
def run_upgrades(self, storage): def run_upgrades(self, storage, db):
path = storage.path path = storage.path
if storage.requires_split(): if db.requires_split():
self.hide() self.hide()
msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n" msg = _("The wallet '{}' contains multiple accounts, which are no longer supported since Electrum 2.7.\n\n"
"Do you want to split your wallet into multiple files?").format(path) "Do you want to split your wallet into multiple files?").format(path)
if not self.question(msg): if not self.question(msg):
return return
file_list = '\n'.join(storage.split_accounts()) file_list = db.split_accounts(path)
msg = _('Your accounts have been moved to') + ':\n' + file_list + '\n\n'+ _('Do you want to delete the old file') + ':\n' + path msg = _('Your accounts have been moved to') + ':\n' + '\n'.join(file_list) + '\n\n'+ _('Do you want to delete the old file') + ':\n' + path
if self.question(msg): if self.question(msg):
os.remove(path) os.remove(path)
self.show_warning(_('The file was removed')) self.show_warning(_('The file was removed'))
# raise now, to avoid having the old storage opened # raise now, to avoid having the old storage opened
raise UserCancelled() raise UserCancelled()
action = storage.get_action() action = db.get_action()
if action and storage.requires_upgrade(): if action and db.requires_upgrade():
raise WalletFileException('Incomplete wallet files cannot be upgraded.') raise WalletFileException('Incomplete wallet files cannot be upgraded.')
if action: if action:
self.hide() self.hide()
@ -345,15 +346,17 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
self.show_warning(_('The file was removed')) self.show_warning(_('The file was removed'))
return return
self.show() self.show()
self.data = storage.db.data # FIXME self.data = json.loads(storage.read())
self.run(action) self.run(action)
for k, v in self.data.items(): for k, v in self.data.items():
storage.put(k, v) db.put(k, v)
storage.write() db.write(storage)
return return
if storage.requires_upgrade(): if db.requires_upgrade():
self.upgrade_storage(storage) self.upgrade_db(storage, db)
return db
def finished(self): def finished(self):
"""Called in hardware client wrapper, in order to close popups.""" """Called in hardware client wrapper, in order to close popups."""

View file

@ -456,7 +456,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
send_exception_to_crash_reporter(e) send_exception_to_crash_reporter(e)
def init_geometry(self): def init_geometry(self):
winpos = self.wallet.storage.get("winpos-qt") winpos = self.wallet.db.get("winpos-qt")
try: try:
screen = self.app.desktop().screenGeometry() screen = self.app.desktop().screenGeometry()
assert screen.contains(QRect(*winpos)) assert screen.contains(QRect(*winpos))
@ -469,7 +469,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
name = "Electrum Testnet" if constants.net.TESTNET else "Electrum" name = "Electrum Testnet" if constants.net.TESTNET else "Electrum"
title = '%s %s - %s' % (name, ELECTRUM_VERSION, title = '%s %s - %s' % (name, ELECTRUM_VERSION,
self.wallet.basename()) self.wallet.basename())
extra = [self.wallet.storage.get('wallet_type', '?')] extra = [self.wallet.db.get('wallet_type', '?')]
if self.wallet.is_watching_only(): if self.wallet.is_watching_only():
extra.append(_('watching only')) extra.append(_('watching only'))
title += ' [%s]'% ', '.join(extra) title += ' [%s]'% ', '.join(extra)
@ -1958,7 +1958,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def update_console(self): def update_console(self):
console = self.console console = self.console
console.history = self.wallet.storage.get("qt-console-history", []) console.history = self.wallet.db.get("qt-console-history", [])
console.history_index = len(console.history) console.history_index = len(console.history)
console.updateNamespace({ console.updateNamespace({
@ -2154,7 +2154,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
dialog.setMinimumSize(500, 100) dialog.setMinimumSize(500, 100)
mpk_list = self.wallet.get_master_public_keys() mpk_list = self.wallet.get_master_public_keys()
vbox = QVBoxLayout() vbox = QVBoxLayout()
wallet_type = self.wallet.storage.get('wallet_type', '') wallet_type = self.wallet.db.get('wallet_type', '')
if self.wallet.is_watching_only(): if self.wallet.is_watching_only():
wallet_type += ' [{}]'.format(_('watching-only')) wallet_type += ' [{}]'.format(_('watching-only'))
seed_available = _('True') if self.wallet.has_seed() else _('False') seed_available = _('True') if self.wallet.has_seed() else _('False')
@ -2788,9 +2788,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.config.set_key("is_maximized", self.isMaximized()) self.config.set_key("is_maximized", self.isMaximized())
if not self.isMaximized(): if not self.isMaximized():
g = self.geometry() g = self.geometry()
self.wallet.storage.put("winpos-qt", [g.left(),g.top(), self.wallet.db.put("winpos-qt", [g.left(),g.top(),
g.width(),g.height()]) g.width(),g.height()])
self.wallet.storage.put("qt-console-history", self.console.history[-50:]) self.wallet.db.put("qt-console-history", self.console.history[-50:])
if self.qr_window: if self.qr_window:
self.qr_window.close() self.qr_window.close()
self.close_wallet() self.close_wallet()

View file

@ -324,7 +324,7 @@ that is always connected to the internet. Configure a port if you want it to be
usechange_result = x == Qt.Checked usechange_result = x == Qt.Checked
if self.window.wallet.use_change != usechange_result: if self.window.wallet.use_change != usechange_result:
self.window.wallet.use_change = usechange_result self.window.wallet.use_change = usechange_result
self.window.wallet.storage.put('use_change', self.window.wallet.use_change) self.window.wallet.db.put('use_change', self.window.wallet.use_change)
multiple_cb.setEnabled(self.window.wallet.use_change) multiple_cb.setEnabled(self.window.wallet.use_change)
usechange_cb.stateChanged.connect(on_usechange) usechange_cb.stateChanged.connect(on_usechange)
usechange_cb.setToolTip(_('Using change addresses makes it more difficult for other people to track your transactions.')) usechange_cb.setToolTip(_('Using change addresses makes it more difficult for other people to track your transactions.'))
@ -334,7 +334,7 @@ that is always connected to the internet. Configure a port if you want it to be
multiple = x == Qt.Checked multiple = x == Qt.Checked
if self.wallet.multiple_change != multiple: if self.wallet.multiple_change != multiple:
self.wallet.multiple_change = multiple self.wallet.multiple_change = multiple
self.wallet.storage.put('multiple_change', multiple) self.wallet.db.put('multiple_change', multiple)
multiple_change = self.wallet.multiple_change multiple_change = self.wallet.multiple_change
multiple_cb = QCheckBox(_('Use multiple change addresses')) multiple_cb = QCheckBox(_('Use multiple change addresses'))
multiple_cb.setEnabled(self.wallet.use_change) multiple_cb.setEnabled(self.wallet.use_change)

View file

@ -864,8 +864,8 @@ def hardware_keystore(d) -> Hardware_KeyStore:
raise WalletFileException(f'unknown hardware type: {hw_type}. ' raise WalletFileException(f'unknown hardware type: {hw_type}. '
f'hw_keystores: {list(hw_keystores)}') f'hw_keystores: {list(hw_keystores)}')
def load_keystore(storage, name) -> KeyStore: def load_keystore(db, name) -> KeyStore:
d = storage.get(name, {}) d = db.get(name, {})
t = d.get('type') t = d.get('type')
if not t: if not t:
raise WalletFileException( raise WalletFileException(

View file

@ -617,7 +617,7 @@ class Peer(Logger):
"revocation_store": {}, "revocation_store": {},
} }
channel_id = chan_dict.get('channel_id') channel_id = chan_dict.get('channel_id')
channels = self.lnworker.storage.db.get_dict('channels') channels = self.lnworker.db.get_dict('channels')
channels[channel_id] = chan_dict channels[channel_id] = chan_dict
return channels.get(channel_id) return channels.get(channel_id)

View file

@ -341,25 +341,25 @@ class LNWallet(LNWorker):
def __init__(self, wallet: 'Abstract_Wallet', xprv): def __init__(self, wallet: 'Abstract_Wallet', xprv):
Logger.__init__(self) Logger.__init__(self)
self.wallet = wallet self.wallet = wallet
self.storage = wallet.storage self.db = wallet.db
self.config = wallet.config self.config = wallet.config
LNWorker.__init__(self, xprv) LNWorker.__init__(self, xprv)
self.ln_keystore = keystore.from_xprv(xprv) self.ln_keystore = keystore.from_xprv(xprv)
self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ
self.payments = self.storage.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid
self.preimages = self.storage.db.get_dict('lightning_preimages') # RHASH -> preimage self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
self.sweep_address = wallet.get_receiving_address() self.sweep_address = wallet.get_receiving_address()
self.lock = threading.RLock() self.lock = threading.RLock()
self.logs = defaultdict(list) # type: Dict[str, List[PaymentAttemptLog]] # key is RHASH self.logs = defaultdict(list) # type: Dict[str, List[PaymentAttemptLog]] # key is RHASH
# note: accessing channels (besides simple lookup) needs self.lock! # note: accessing channels (besides simple lookup) needs self.lock!
self.channels = {} self.channels = {}
channels = self.storage.db.get_dict("channels") channels = self.db.get_dict("channels")
for channel_id, c in channels.items(): for channel_id, c in channels.items():
self.channels[bfh(channel_id)] = Channel(c, sweep_address=self.sweep_address, lnworker=self) self.channels[bfh(channel_id)] = Channel(c, sweep_address=self.sweep_address, lnworker=self)
# timestamps of opening and closing transactions # timestamps of opening and closing transactions
self.channel_timestamps = self.storage.db.get_dict('lightning_channel_timestamps') self.channel_timestamps = self.db.get_dict('lightning_channel_timestamps')
self.pending_payments = defaultdict(asyncio.Future) self.pending_payments = defaultdict(asyncio.Future)
@ignore_exceptions @ignore_exceptions
@ -585,10 +585,10 @@ class LNWallet(LNWorker):
def get_and_inc_counter_for_channel_keys(self): def get_and_inc_counter_for_channel_keys(self):
with self.lock: with self.lock:
ctr = self.storage.get('lightning_channel_key_der_ctr', -1) ctr = self.db.get('lightning_channel_key_der_ctr', -1)
ctr += 1 ctr += 1
self.storage.put('lightning_channel_key_der_ctr', ctr) self.db.put('lightning_channel_key_der_ctr', ctr)
self.storage.write() self.wallet.save_db()
return ctr return ctr
def suggest_peer(self): def suggest_peer(self):
@ -610,7 +610,7 @@ class LNWallet(LNWorker):
assert type(chan) is Channel assert type(chan) is Channel
if chan.config[REMOTE].next_per_commitment_point == chan.config[REMOTE].current_per_commitment_point: if chan.config[REMOTE].next_per_commitment_point == chan.config[REMOTE].current_per_commitment_point:
raise Exception("Tried to save channel with next_point == current_point, this should not happen") raise Exception("Tried to save channel with next_point == current_point, this should not happen")
self.wallet.storage.write() self.wallet.save_db()
self.network.trigger_callback('channel', chan) self.network.trigger_callback('channel', chan)
def save_short_chan_id(self, chan): def save_short_chan_id(self, chan):
@ -1127,7 +1127,7 @@ class LNWallet(LNWorker):
def save_preimage(self, payment_hash: bytes, preimage: bytes): def save_preimage(self, payment_hash: bytes, preimage: bytes):
assert sha256(preimage) == payment_hash assert sha256(preimage) == payment_hash
self.preimages[bh2u(payment_hash)] = bh2u(preimage) self.preimages[bh2u(payment_hash)] = bh2u(preimage)
self.storage.write() self.wallet.save_db()
def get_preimage(self, payment_hash: bytes) -> bytes: def get_preimage(self, payment_hash: bytes) -> bytes:
return bfh(self.preimages.get(bh2u(payment_hash))) return bfh(self.preimages.get(bh2u(payment_hash)))
@ -1145,7 +1145,7 @@ class LNWallet(LNWorker):
assert info.status in [PR_PAID, PR_UNPAID, PR_INFLIGHT] assert info.status in [PR_PAID, PR_UNPAID, PR_INFLIGHT]
with self.lock: with self.lock:
self.payments[key] = info.amount, info.direction, info.status self.payments[key] = info.amount, info.direction, info.status
self.storage.write() self.wallet.save_db()
def get_payment_status(self, payment_hash): def get_payment_status(self, payment_hash):
try: try:
@ -1230,7 +1230,7 @@ class LNWallet(LNWorker):
del self.payments[payment_hash_hex] del self.payments[payment_hash_hex]
except KeyError: except KeyError:
return return
self.storage.write() self.wallet.save_db()
def get_balance(self): def get_balance(self):
with self.lock: with self.lock:
@ -1276,7 +1276,7 @@ class LNWallet(LNWorker):
with self.lock: with self.lock:
self.channels.pop(chan_id) self.channels.pop(chan_id)
self.channel_timestamps.pop(chan_id.hex()) self.channel_timestamps.pop(chan_id.hex())
self.storage.get('channels').pop(chan_id.hex()) self.db.get('channels').pop(chan_id.hex())
self.network.trigger_callback('channels_updated', self.wallet) self.network.trigger_callback('channels_updated', self.wallet)
self.network.trigger_callback('wallet_updated', self.wallet) self.network.trigger_callback('wallet_updated', self.wallet)

View file

@ -46,7 +46,7 @@ class LabelsPlugin(BasePlugin):
def get_nonce(self, wallet): def get_nonce(self, wallet):
# nonce is the nonce to be used with the next change # nonce is the nonce to be used with the next change
nonce = wallet.storage.get('wallet_nonce') nonce = wallet.db.get('wallet_nonce')
if nonce is None: if nonce is None:
nonce = 1 nonce = 1
self.set_nonce(wallet, nonce) self.set_nonce(wallet, nonce)
@ -54,7 +54,7 @@ class LabelsPlugin(BasePlugin):
def set_nonce(self, wallet, nonce): def set_nonce(self, wallet, nonce):
self.logger.info(f"set {wallet.basename()} nonce to {nonce}") self.logger.info(f"set {wallet.basename()} nonce to {nonce}")
wallet.storage.put("wallet_nonce", nonce) wallet.db.put("wallet_nonce", nonce)
@hook @hook
def set_label(self, wallet, item, label): def set_label(self, wallet, item, label):

View file

@ -227,7 +227,7 @@ class Plugin(TrustedCoinPlugin):
wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use')) wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use'))
except GoBack: except GoBack:
# user clicked 'Cancel' and decided to move wallet file manually # user clicked 'Cancel' and decided to move wallet file manually
wizard.create_storage(wizard.path) storage, db = wizard.create_storage(wizard.path)
raise raise
def accept_terms_of_use(self, window): def accept_terms_of_use(self, window):

View file

@ -264,17 +264,17 @@ class Wallet_2fa(Multisig_Wallet):
wallet_type = '2fa' wallet_type = '2fa'
def __init__(self, storage, *, config): def __init__(self, db, *, config):
self.m, self.n = 2, 3 self.m, self.n = 2, 3
Deterministic_Wallet.__init__(self, storage, config=config) Deterministic_Wallet.__init__(self, db, config=config)
self.is_billing = False self.is_billing = False
self.billing_info = None self.billing_info = None
self._load_billing_addresses() self._load_billing_addresses()
def _load_billing_addresses(self): def _load_billing_addresses(self):
billing_addresses = { billing_addresses = {
'legacy': self.storage.get('trustedcoin_billing_addresses', {}), 'legacy': self.db.get('trustedcoin_billing_addresses', {}),
'segwit': self.storage.get('trustedcoin_billing_addresses_segwit', {}) 'segwit': self.db.get('trustedcoin_billing_addresses_segwit', {})
} }
self._billing_addresses = {} # type: Dict[str, Dict[int, str]] # addr_type -> index -> addr self._billing_addresses = {} # type: Dict[str, Dict[int, str]] # addr_type -> index -> addr
self._billing_addresses_set = set() # set of addrs self._billing_addresses_set = set() # set of addrs
@ -289,7 +289,7 @@ class Wallet_2fa(Multisig_Wallet):
return not self.keystores['x2/'].is_watching_only() return not self.keystores['x2/'].is_watching_only()
def get_user_id(self): def get_user_id(self):
return get_user_id(self.storage) return get_user_id(self.db)
def min_prepay(self): def min_prepay(self):
return min(self.price_per_tx.keys()) return min(self.price_per_tx.keys())
@ -383,10 +383,10 @@ class Wallet_2fa(Multisig_Wallet):
billing_addresses_of_this_type[billing_index] = address billing_addresses_of_this_type[billing_index] = address
self._billing_addresses_set.add(address) self._billing_addresses_set.add(address)
self._billing_addresses[addr_type] = billing_addresses_of_this_type self._billing_addresses[addr_type] = billing_addresses_of_this_type
self.storage.put('trustedcoin_billing_addresses', self._billing_addresses['legacy']) self.db.put('trustedcoin_billing_addresses', self._billing_addresses['legacy'])
self.storage.put('trustedcoin_billing_addresses_segwit', self._billing_addresses['segwit']) self.db.put('trustedcoin_billing_addresses_segwit', self._billing_addresses['segwit'])
# FIXME this often runs in a daemon thread, where storage.write will fail # FIXME this often runs in a daemon thread, where storage.write will fail
self.storage.write() self.db.write(self.storage)
def is_billing_address(self, addr: str) -> bool: def is_billing_address(self, addr: str) -> bool:
return addr in self._billing_addresses_set return addr in self._billing_addresses_set
@ -394,11 +394,11 @@ class Wallet_2fa(Multisig_Wallet):
# Utility functions # Utility functions
def get_user_id(storage): def get_user_id(db):
def make_long_id(xpub_hot, xpub_cold): def make_long_id(xpub_hot, xpub_cold):
return sha256(''.join(sorted([xpub_hot, xpub_cold]))) return sha256(''.join(sorted([xpub_hot, xpub_cold])))
xpub1 = storage.get('x1/')['xpub'] xpub1 = db.get('x1/')['xpub']
xpub2 = storage.get('x2/')['xpub'] xpub2 = db.get('x2/')['xpub']
long_id = make_long_id(xpub1, xpub2) long_id = make_long_id(xpub1, xpub2)
short_id = hashlib.sha256(long_id).hexdigest() short_id = hashlib.sha256(long_id).hexdigest()
return long_id, short_id return long_id, short_id
@ -753,12 +753,12 @@ class TrustedCoinPlugin(BasePlugin):
self.request_otp_dialog(wizard, short_id, new_secret, xpub3) self.request_otp_dialog(wizard, short_id, new_secret, xpub3)
@hook @hook
def get_action(self, storage): def get_action(self, db):
if storage.get('wallet_type') != '2fa': if db.get('wallet_type') != '2fa':
return return
if not storage.get('x1/'): if not db.get('x1/'):
return self, 'show_disclaimer' return self, 'show_disclaimer'
if not storage.get('x2/'): if not db.get('x2/'):
return self, 'show_disclaimer' return self, 'show_disclaimer'
if not storage.get('x3/'): if not db.get('x3/'):
return self, 'accept_terms_of_use' return self, 'accept_terms_of_use'

View file

@ -32,7 +32,6 @@ from enum import IntEnum
from . import ecc from . import ecc
from .util import profiler, InvalidPassword, WalletFileException, bfh, standardize_path from .util import profiler, InvalidPassword, WalletFileException, bfh, standardize_path
from .plugin import run_hook, plugin_loaders
from .wallet_db import WalletDB from .wallet_db import WalletDB
from .logging import Logger from .logging import Logger
@ -53,28 +52,27 @@ class StorageEncryptionVersion(IntEnum):
class StorageReadWriteError(Exception): pass class StorageReadWriteError(Exception): pass
# TODO: Rename to Storage
class WalletStorage(Logger): class WalletStorage(Logger):
def __init__(self, path, *, manual_upgrades: bool = False): def __init__(self, path):
Logger.__init__(self) Logger.__init__(self)
self.path = standardize_path(path) self.path = standardize_path(path)
self._file_exists = bool(self.path and os.path.exists(self.path)) self._file_exists = bool(self.path and os.path.exists(self.path))
self._manual_upgrades = manual_upgrades
self.logger.info(f"wallet path {self.path}") self.logger.info(f"wallet path {self.path}")
self.pubkey = None self.pubkey = None
self.decrypted = ''
self._test_read_write_permissions(self.path) self._test_read_write_permissions(self.path)
if self.file_exists(): if self.file_exists():
with open(self.path, "r", encoding='utf-8') as f: with open(self.path, "r", encoding='utf-8') as f:
self.raw = f.read() self.raw = f.read()
self._encryption_version = self._init_encryption_version() self._encryption_version = self._init_encryption_version()
if not self.is_encrypted():
self.db = WalletDB(self.raw, manual_upgrades=manual_upgrades)
self.load_plugins()
else: else:
self.raw = ''
self._encryption_version = StorageEncryptionVersion.PLAINTEXT self._encryption_version = StorageEncryptionVersion.PLAINTEXT
# avoid new wallets getting 'upgraded'
self.db = WalletDB('', manual_upgrades=False) def read(self):
return self.decrypted if self.is_encrypted() else self.raw
@classmethod @classmethod
def _test_read_write_permissions(cls, path): def _test_read_write_permissions(cls, path):
@ -98,29 +96,9 @@ class WalletStorage(Logger):
if echo != echo2: if echo != echo2:
raise StorageReadWriteError('echo sanity-check failed') raise StorageReadWriteError('echo sanity-check failed')
def load_plugins(self):
wallet_type = self.db.get('wallet_type')
if wallet_type in plugin_loaders:
plugin_loaders[wallet_type]()
def put(self, key,value):
self.db.put(key, value)
def get(self, key, default=None):
return self.db.get(key, default)
@profiler @profiler
def write(self): def write(self, data):
with self.db.lock: s = self.encrypt_before_writing(data)
self._write()
def _write(self):
if threading.currentThread().isDaemon():
self.logger.warning('daemon thread cannot write db')
return
if not self.db.modified():
return
s = self.encrypt_before_writing(self.db.dump())
temp_path = "%s.tmp.%s" % (self.path, os.getpid()) temp_path = "%s.tmp.%s" % (self.path, os.getpid())
with open(temp_path, "w", encoding='utf-8') as f: with open(temp_path, "w", encoding='utf-8') as f:
f.write(s) f.write(s)
@ -135,7 +113,6 @@ class WalletStorage(Logger):
os.chmod(self.path, mode) os.chmod(self.path, mode)
self._file_exists = True self._file_exists = True
self.logger.info(f"saved {self.path}") self.logger.info(f"saved {self.path}")
self.db.set_modified(False)
def file_exists(self) -> bool: def file_exists(self) -> bool:
return self._file_exists return self._file_exists
@ -148,7 +125,7 @@ class WalletStorage(Logger):
or if encryption is enabled but the contents have already been decrypted. or if encryption is enabled but the contents have already been decrypted.
""" """
try: try:
return bool(self.db.data) return not self.is_encrypted() or bool(self.decrypted)
except AttributeError: except AttributeError:
return False return False
@ -207,12 +184,12 @@ class WalletStorage(Logger):
if self.raw: if self.raw:
enc_magic = self._get_encryption_magic() enc_magic = self._get_encryption_magic()
s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic)) s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic))
s = s.decode('utf8')
else: else:
s = None s = ''
self.pubkey = ec_key.get_public_key_hex() self.pubkey = ec_key.get_public_key_hex()
s = s.decode('utf8') self.decrypted = s
self.db = WalletDB(s, manual_upgrades=self._manual_upgrades) return s
self.load_plugins()
def encrypt_before_writing(self, plaintext: str) -> str: def encrypt_before_writing(self, plaintext: str) -> str:
s = plaintext s = plaintext
@ -234,9 +211,6 @@ class WalletStorage(Logger):
if self.pubkey and self.pubkey != self.get_eckey_from_password(password).get_public_key_hex(): if self.pubkey and self.pubkey != self.get_eckey_from_password(password).get_public_key_hex():
raise InvalidPassword() raise InvalidPassword()
def set_keystore_encryption(self, enable):
self.put('use_encryption', enable)
def set_password(self, password, enc_version=None): def set_password(self, password, enc_version=None):
"""Set a password to be used for encrypting this storage.""" """Set a password to be used for encrypting this storage."""
if enc_version is None: if enc_version is None:
@ -248,40 +222,7 @@ class WalletStorage(Logger):
else: else:
self.pubkey = None self.pubkey = None
self._encryption_version = StorageEncryptionVersion.PLAINTEXT self._encryption_version = StorageEncryptionVersion.PLAINTEXT
# make sure next storage.write() saves changes
self.db.set_modified(True)
def basename(self) -> str: def basename(self) -> str:
return os.path.basename(self.path) return os.path.basename(self.path)
def requires_upgrade(self):
if not self.is_past_initial_decryption():
raise Exception("storage not yet decrypted!")
return self.db.requires_upgrade()
def is_ready_to_be_used_by_wallet(self):
return not self.requires_upgrade() and self.db._called_after_upgrade_tasks
def upgrade(self):
self.db.upgrade()
self.write()
def requires_split(self):
return self.db.requires_split()
def split_accounts(self):
out = []
result = self.db.split_accounts()
for data in result:
path = self.path + '.' + data['suffix']
storage = WalletStorage(path)
storage.db.data = data
storage.db._called_after_upgrade_tasks = False
storage.db.upgrade()
storage.write()
out.append(path)
return out
def get_action(self):
action = run_hook('get_action', self)
return action

View file

@ -4,7 +4,7 @@ from decimal import Decimal
from electrum.util import create_and_start_event_loop from electrum.util import create_and_start_event_loop
from electrum.commands import Commands, eval_bool from electrum.commands import Commands, eval_bool
from electrum import storage from electrum import storage, wallet
from electrum.wallet import restore_wallet_from_text from electrum.wallet import restore_wallet_from_text
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
@ -77,8 +77,8 @@ class TestCommands(ElectrumTestCase):
for xkey2, xtype2 in xprvs: for xkey2, xtype2 in xprvs:
self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2))) self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2)))
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_encrypt_decrypt(self, mock_write): def test_encrypt_decrypt(self, mock_save_db):
wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN', wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN',
path='if_this_exists_mocking_failed_648151893', path='if_this_exists_mocking_failed_648151893',
config=self.config)['wallet'] config=self.config)['wallet']
@ -88,8 +88,8 @@ class TestCommands(ElectrumTestCase):
ciphertext = cmds._run('encrypt', (pubkey, cleartext)) ciphertext = cmds._run('encrypt', (pubkey, cleartext))
self.assertEqual(cleartext, cmds._run('decrypt', (pubkey, ciphertext), wallet=wallet)) self.assertEqual(cleartext, cmds._run('decrypt', (pubkey, ciphertext), wallet=wallet))
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_export_private_key_imported(self, mock_write): def test_export_private_key_imported(self, mock_save_db):
wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL',
path='if_this_exists_mocking_failed_648151893', path='if_this_exists_mocking_failed_648151893',
config=self.config)['wallet'] config=self.config)['wallet']
@ -107,8 +107,8 @@ class TestCommands(ElectrumTestCase):
self.assertEqual(['p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'], self.assertEqual(['p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'],
cmds._run('getprivatekeys', (['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'], ), wallet=wallet)) cmds._run('getprivatekeys', (['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'], ), wallet=wallet))
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_export_private_key_deterministic(self, mock_write): def test_export_private_key_deterministic(self, mock_save_db):
wallet = restore_wallet_from_text('bitter grass shiver impose acquire brush forget axis eager alone wine silver', wallet = restore_wallet_from_text('bitter grass shiver impose acquire brush forget axis eager alone wine silver',
gap_limit=2, gap_limit=2,
path='if_this_exists_mocking_failed_648151893', path='if_this_exists_mocking_failed_648151893',

View file

@ -68,23 +68,13 @@ class MockNetwork:
if self.tx_queue: if self.tx_queue:
await self.tx_queue.put(tx) await self.tx_queue.put(tx)
class MockStorage:
def put(self, key, value):
pass
def get(self, key, default=None):
pass
def write(self):
pass
class MockWallet: class MockWallet:
storage = MockStorage()
def set_label(self, x, y): def set_label(self, x, y):
pass pass
def save_db(self):
pass
class MockLNWallet: class MockLNWallet:
storage = MockStorage()
def __init__(self, remote_keypair, local_keypair, chan, tx_queue): def __init__(self, remote_keypair, local_keypair, chan, tx_queue):
self.chan = chan self.chan = chan
self.remote_keypair = remote_keypair self.remote_keypair = remote_keypair

View file

@ -1,8 +1,9 @@
import shutil import shutil
import tempfile import tempfile
import os import os
import json
from electrum.storage import WalletStorage from electrum.wallet_db import WalletDB
from electrum.wallet import Wallet from electrum.wallet import Wallet
from electrum import constants from electrum import constants
@ -293,44 +294,33 @@ class TestStorageUpgrade(WalletTestCase):
def _upgrade_storage(self, wallet_json, accounts=1): def _upgrade_storage(self, wallet_json, accounts=1):
if accounts == 1: if accounts == 1:
# test manual upgrades # test manual upgrades
storage = self._load_storage_from_json_string(wallet_json=wallet_json, db = self._load_db_from_json_string(wallet_json=wallet_json,
path=self.wallet_path, manual_upgrades=True)
manual_upgrades=True) self.assertFalse(db.requires_split())
self.assertFalse(storage.requires_split()) if db.requires_upgrade():
if storage.requires_upgrade(): db.upgrade()
storage.upgrade() self._sanity_check_upgraded_db(db)
self._sanity_check_upgraded_storage(storage)
# test automatic upgrades # test automatic upgrades
path2 = os.path.join(self.user_dir, "somewallet2") db2 = self._load_db_from_json_string(wallet_json=wallet_json,
storage2 = self._load_storage_from_json_string(wallet_json=wallet_json, manual_upgrades=False)
path=path2, self._sanity_check_upgraded_db(db2)
manual_upgrades=False)
storage2.write()
self._sanity_check_upgraded_storage(storage2)
# test opening upgraded storages again
s1 = WalletStorage(path2, manual_upgrades=False)
self._sanity_check_upgraded_storage(s1)
s2 = WalletStorage(path2, manual_upgrades=True)
self._sanity_check_upgraded_storage(s2)
else: else:
storage = self._load_storage_from_json_string(wallet_json=wallet_json, db = self._load_db_from_json_string(wallet_json=wallet_json,
path=self.wallet_path, manual_upgrades=True)
manual_upgrades=True) self.assertTrue(db.requires_split())
self.assertTrue(storage.requires_split()) split_data = db.get_split_accounts()
new_paths = storage.split_accounts() self.assertEqual(accounts, len(split_data))
self.assertEqual(accounts, len(new_paths)) for item in split_data:
for new_path in new_paths: data = json.dumps(item)
new_storage = WalletStorage(new_path, manual_upgrades=False) new_db = WalletDB(data, manual_upgrades=False)
self._sanity_check_upgraded_storage(new_storage) self._sanity_check_upgraded_db(new_db)
def _sanity_check_upgraded_storage(self, storage): def _sanity_check_upgraded_db(self, db):
self.assertFalse(storage.requires_split()) self.assertFalse(db.requires_split())
self.assertFalse(storage.requires_upgrade()) self.assertFalse(db.requires_upgrade())
w = Wallet(storage, config=self.config) w = Wallet(db, None, config=self.config)
@staticmethod @staticmethod
def _load_storage_from_json_string(*, wallet_json, path, manual_upgrades): def _load_db_from_json_string(*, wallet_json, manual_upgrades):
with open(path, "w") as f: db = WalletDB(wallet_json, manual_upgrades=manual_upgrades)
f.write(wallet_json) return db
storage = WalletStorage(path, manual_upgrades=manual_upgrades)
return storage

View file

@ -58,13 +58,15 @@ class TestWalletStorage(WalletTestCase):
with open(self.wallet_path, "w") as f: with open(self.wallet_path, "w") as f:
contents = f.write(contents) contents = f.write(contents)
storage = WalletStorage(self.wallet_path, manual_upgrades=True) storage = WalletStorage(self.wallet_path)
self.assertEqual("b", storage.get("a")) db = WalletDB(storage.read(), manual_upgrades=True)
self.assertEqual("d", storage.get("c")) self.assertEqual("b", db.get("a"))
self.assertEqual("d", db.get("c"))
def test_write_dictionary_to_file(self): def test_write_dictionary_to_file(self):
storage = WalletStorage(self.wallet_path) storage = WalletStorage(self.wallet_path)
db = WalletDB('', manual_upgrades=True)
some_dict = { some_dict = {
u"a": u"b", u"a": u"b",
@ -72,8 +74,8 @@ class TestWalletStorage(WalletTestCase):
u"seed_version": FINAL_SEED_VERSION} u"seed_version": FINAL_SEED_VERSION}
for key, value in some_dict.items(): for key, value in some_dict.items():
storage.put(key, value) db.put(key, value)
storage.write() db.write(storage)
with open(self.wallet_path, "r") as f: with open(self.wallet_path, "r") as f:
contents = f.read() contents = f.read()

View file

@ -5,7 +5,7 @@ import tempfile
from typing import Sequence from typing import Sequence
import asyncio import asyncio
from electrum import storage, bitcoin, keystore, bip32 from electrum import storage, bitcoin, keystore, bip32, wallet
from electrum import Transaction from electrum import Transaction
from electrum import SimpleConfig from electrum import SimpleConfig
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
@ -46,33 +46,33 @@ class WalletIntegrityHelper:
@classmethod @classmethod
def create_standard_wallet(cls, ks, *, config: SimpleConfig, gap_limit=None): def create_standard_wallet(cls, ks, *, config: SimpleConfig, gap_limit=None):
store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') db = storage.WalletDB('', manual_upgrades=False)
store.put('keystore', ks.dump()) db.put('keystore', ks.dump())
store.put('gap_limit', gap_limit or cls.gap_limit) db.put('gap_limit', gap_limit or cls.gap_limit)
w = Standard_Wallet(store, config=config) w = Standard_Wallet(db, None, config=config)
w.synchronize() w.synchronize()
return w return w
@classmethod @classmethod
def create_imported_wallet(cls, *, config: SimpleConfig, privkeys: bool): def create_imported_wallet(cls, *, config: SimpleConfig, privkeys: bool):
store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') db = storage.WalletDB('', manual_upgrades=False)
if privkeys: if privkeys:
k = keystore.Imported_KeyStore({}) k = keystore.Imported_KeyStore({})
store.put('keystore', k.dump()) db.put('keystore', k.dump())
w = Imported_Wallet(store, config=config) w = Imported_Wallet(db, None, config=config)
return w return w
@classmethod @classmethod
def create_multisig_wallet(cls, keystores: Sequence, multisig_type: str, *, def create_multisig_wallet(cls, keystores: Sequence, multisig_type: str, *,
config: SimpleConfig, gap_limit=None): config: SimpleConfig, gap_limit=None):
"""Creates a multisig wallet.""" """Creates a multisig wallet."""
store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') db = storage.WalletDB('', manual_upgrades=True)
for i, ks in enumerate(keystores): for i, ks in enumerate(keystores):
cosigner_index = i + 1 cosigner_index = i + 1
store.put('x%d/' % cosigner_index, ks.dump()) db.put('x%d/' % cosigner_index, ks.dump())
store.put('wallet_type', multisig_type) db.put('wallet_type', multisig_type)
store.put('gap_limit', gap_limit or cls.gap_limit) db.put('gap_limit', gap_limit or cls.gap_limit)
w = Multisig_Wallet(store, config=config) w = Multisig_Wallet(db, None, config=config)
w.synchronize() w.synchronize()
return w return w
@ -84,8 +84,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.config = SimpleConfig({'electrum_path': self.electrum_path})
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_electrum_seed_standard(self, mock_write): def test_electrum_seed_standard(self, mock_save_db):
seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song' seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song'
self.assertEqual(seed_type(seed_words), 'standard') self.assertEqual(seed_type(seed_words), 'standard')
@ -104,8 +104,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '1KSezYMhAJMWqFbVFB2JshYg69UpmEXR4D') self.assertEqual(w.get_change_addresses()[0], '1KSezYMhAJMWqFbVFB2JshYg69UpmEXR4D')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_electrum_seed_segwit(self, mock_write): def test_electrum_seed_segwit(self, mock_save_db):
seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'
self.assertEqual(seed_type(seed_words), 'segwit') self.assertEqual(seed_type(seed_words), 'segwit')
@ -124,8 +124,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], 'bc1qdy94n2q5qcp0kg7v9yzwe6wvfkhnvyzje7nx2p') self.assertEqual(w.get_change_addresses()[0], 'bc1qdy94n2q5qcp0kg7v9yzwe6wvfkhnvyzje7nx2p')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_electrum_seed_segwit_passphrase(self, mock_write): def test_electrum_seed_segwit_passphrase(self, mock_save_db):
seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver'
self.assertEqual(seed_type(seed_words), 'segwit') self.assertEqual(seed_type(seed_words), 'segwit')
@ -144,8 +144,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], 'bc1qcywwsy87sdp8vz5rfjh3sxdv6rt95kujdqq38g') self.assertEqual(w.get_change_addresses()[0], 'bc1qcywwsy87sdp8vz5rfjh3sxdv6rt95kujdqq38g')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_electrum_seed_old(self, mock_write): def test_electrum_seed_old(self, mock_save_db):
seed_words = 'powerful random nobody notice nothing important anyway look away hidden message over' seed_words = 'powerful random nobody notice nothing important anyway look away hidden message over'
self.assertEqual(seed_type(seed_words), 'old') self.assertEqual(seed_type(seed_words), 'old')
@ -163,8 +163,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '1KRW8pH6HFHZh889VDq6fEKvmrsmApwNfe') self.assertEqual(w.get_change_addresses()[0], '1KRW8pH6HFHZh889VDq6fEKvmrsmApwNfe')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_electrum_seed_2fa_legacy(self, mock_write): def test_electrum_seed_2fa_legacy(self, mock_save_db):
seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove' seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove'
self.assertEqual(seed_type(seed_words), '2fa') self.assertEqual(seed_type(seed_words), '2fa')
@ -198,8 +198,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy') self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_electrum_seed_2fa_segwit(self, mock_write): def test_electrum_seed_2fa_segwit(self, mock_save_db):
seed_words = 'universe topic remind silver february ranch shine worth innocent cattle enhance wise' seed_words = 'universe topic remind silver february ranch shine worth innocent cattle enhance wise'
self.assertEqual(seed_type(seed_words), '2fa_segwit') self.assertEqual(seed_type(seed_words), '2fa_segwit')
@ -233,8 +233,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], 'bc1qd4q50nft7kxm9yglfnpup9ed2ukj3tkxp793y0zya8dc9m39jcwq308dxz') self.assertEqual(w.get_change_addresses()[0], 'bc1qd4q50nft7kxm9yglfnpup9ed2ukj3tkxp793y0zya8dc9m39jcwq308dxz')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_bip39_seed_bip44_standard(self, mock_write): def test_bip39_seed_bip44_standard(self, mock_save_db):
seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial'
self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))
@ -252,8 +252,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '1GG5bVeWgAp5XW7JLCphse14QaC4qiHyWn') self.assertEqual(w.get_change_addresses()[0], '1GG5bVeWgAp5XW7JLCphse14QaC4qiHyWn')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_bip39_seed_bip44_standard_passphrase(self, mock_write): def test_bip39_seed_bip44_standard_passphrase(self, mock_save_db):
seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial'
self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))
@ -271,8 +271,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '1H4QD1rg2zQJ4UjuAVJr5eW1fEM8WMqyxh') self.assertEqual(w.get_change_addresses()[0], '1H4QD1rg2zQJ4UjuAVJr5eW1fEM8WMqyxh')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_bip39_seed_bip49_p2sh_segwit(self, mock_write): def test_bip39_seed_bip49_p2sh_segwit(self, mock_save_db):
seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial'
self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))
@ -290,8 +290,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '3KaBTcviBLEJajTEMstsA2GWjYoPzPK7Y7') self.assertEqual(w.get_change_addresses()[0], '3KaBTcviBLEJajTEMstsA2GWjYoPzPK7Y7')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_bip39_seed_bip84_native_segwit(self, mock_write): def test_bip39_seed_bip84_native_segwit(self, mock_save_db):
# test case from bip84 # test case from bip84
seed_words = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' seed_words = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))
@ -310,8 +310,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el') self.assertEqual(w.get_change_addresses()[0], 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_electrum_multisig_seed_standard(self, mock_write): def test_electrum_multisig_seed_standard(self, mock_save_db):
seed_words = 'blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure' seed_words = 'blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure'
self.assertEqual(seed_type(seed_words), 'standard') self.assertEqual(seed_type(seed_words), 'standard')
@ -333,8 +333,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '36XWwEHrrVCLnhjK5MrVVGmUHghr9oWTN1') self.assertEqual(w.get_change_addresses()[0], '36XWwEHrrVCLnhjK5MrVVGmUHghr9oWTN1')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_electrum_multisig_seed_segwit(self, mock_write): def test_electrum_multisig_seed_segwit(self, mock_save_db):
seed_words = 'snow nest raise royal more walk demise rotate smooth spirit canyon gun' seed_words = 'snow nest raise royal more walk demise rotate smooth spirit canyon gun'
self.assertEqual(seed_type(seed_words), 'segwit') self.assertEqual(seed_type(seed_words), 'segwit')
@ -356,8 +356,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], 'bc1qxqf840dqswcmu7a8v82fj6ej0msx08flvuy6kngr7axstjcaq6us9hrehd') self.assertEqual(w.get_change_addresses()[0], 'bc1qxqf840dqswcmu7a8v82fj6ej0msx08flvuy6kngr7axstjcaq6us9hrehd')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_bip39_multisig_seed_bip45_standard(self, mock_write): def test_bip39_multisig_seed_bip45_standard(self, mock_save_db):
seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial'
self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))
@ -379,8 +379,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '3FGyDuxgUDn2pSZe5xAJH1yUwSdhzDMyEE') self.assertEqual(w.get_change_addresses()[0], '3FGyDuxgUDn2pSZe5xAJH1yUwSdhzDMyEE')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_bip39_multisig_seed_p2sh_segwit(self, mock_write): def test_bip39_multisig_seed_p2sh_segwit(self, mock_save_db):
# bip39 seed: pulse mixture jazz invite dune enrich minor weapon mosquito flight fly vapor # bip39 seed: pulse mixture jazz invite dune enrich minor weapon mosquito flight fly vapor
# der: m/49'/0'/0' # der: m/49'/0'/0'
# NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh
@ -401,8 +401,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase):
self.assertEqual(w.get_change_addresses()[0], '39RhtDchc6igmx5tyoimhojFL1ZbQBrXa6') self.assertEqual(w.get_change_addresses()[0], '39RhtDchc6igmx5tyoimhojFL1ZbQBrXa6')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_bip32_extended_version_bytes(self, mock_write): def test_bip32_extended_version_bytes(self, mock_save_db):
seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant' seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant'
self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))
bip32_seed = keystore.bip39_to_seed(seed_words, '') bip32_seed = keystore.bip39_to_seed(seed_words, '')
@ -467,8 +467,8 @@ class TestWalletKeystoreAddressIntegrityForTestnet(TestCaseForTestnet):
super().setUp() super().setUp()
self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.config = SimpleConfig({'electrum_path': self.electrum_path})
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_bip39_multisig_seed_p2sh_segwit_testnet(self, mock_write): def test_bip39_multisig_seed_p2sh_segwit_testnet(self, mock_save_db):
# bip39 seed: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose # bip39 seed: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose
# der: m/49'/1'/0' # der: m/49'/1'/0'
# NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh
@ -489,8 +489,8 @@ class TestWalletKeystoreAddressIntegrityForTestnet(TestCaseForTestnet):
self.assertEqual(w.get_change_addresses()[0], '2NFp9w8tbYYP9Ze2xQpeYBJQjx3gbXymHX7') self.assertEqual(w.get_change_addresses()[0], '2NFp9w8tbYYP9Ze2xQpeYBJQjx3gbXymHX7')
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_bip32_extended_version_bytes(self, mock_write): def test_bip32_extended_version_bytes(self, mock_save_db):
seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant' seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant'
self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True))
bip32_seed = keystore.bip39_to_seed(seed_words, '') bip32_seed = keystore.bip39_to_seed(seed_words, '')
@ -560,8 +560,8 @@ class TestWalletSending(TestCaseForTestnet):
return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=2, config=self.config) return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=2, config=self.config)
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_write): def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_save_db):
wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver') wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver')
wallet2 = self.create_standard_wallet_from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song') wallet2 = self.create_standard_wallet_from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song')
@ -617,8 +617,8 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual((0, 250000 - 5000 - 100000, 0), wallet2.get_balance()) self.assertEqual((0, 250000 - 5000 - 100000, 0), wallet2.get_balance())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_write): def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_save_db):
wallet1a = WalletIntegrityHelper.create_multisig_wallet( wallet1a = WalletIntegrityHelper.create_multisig_wallet(
[ [
keystore.from_seed('blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure', '', True), keystore.from_seed('blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure', '', True),
@ -698,8 +698,8 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual((0, 370000 - 5000 - 100000, 0), wallet2.get_balance()) self.assertEqual((0, 370000 - 5000 - 100000, 0), wallet2.get_balance())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_write): def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db):
wallet1a = WalletIntegrityHelper.create_multisig_wallet( wallet1a = WalletIntegrityHelper.create_multisig_wallet(
[ [
keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', '', True), keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', '', True),
@ -808,8 +808,8 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual((0, 165000 - 5000 - 100000, 0), wallet2a.get_balance()) self.assertEqual((0, 165000 - 5000 - 100000, 0), wallet2a.get_balance())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_write): def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_save_db):
wallet1a = WalletIntegrityHelper.create_multisig_wallet( wallet1a = WalletIntegrityHelper.create_multisig_wallet(
[ [
keystore.from_seed('phone guilt ancient scan defy gasp off rotate approve ill word exchange', '', True), keystore.from_seed('phone guilt ancient scan defy gasp off rotate approve ill word exchange', '', True),
@ -878,8 +878,8 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual((0, 1000000 - 5000 - 300000, 0), wallet2.get_balance()) self.assertEqual((0, 1000000 - 5000 - 300000, 0), wallet2.get_balance())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_rbf(self, mock_write): def test_rbf(self, mock_save_db):
self.maxDiff = None self.maxDiff = None
for simulate_moving_txs in (False, True): for simulate_moving_txs in (False, True):
with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs):
@ -959,8 +959,8 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual((0, 7484320, 0), wallet.get_balance()) self.assertEqual((0, 7484320, 0), wallet.get_balance())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_cpfp_p2pkh(self, mock_write): def test_cpfp_p2pkh(self, mock_save_db):
wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean') wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean')
# bootstrap wallet # bootstrap wallet
@ -1361,8 +1361,8 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual((0, 3_900_000, 0), wallet.get_balance()) self.assertEqual((0, 3_900_000, 0), wallet.get_balance())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_cpfp_p2wpkh(self, mock_write): def test_cpfp_p2wpkh(self, mock_save_db):
wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage')
# bootstrap wallet # bootstrap wallet
@ -1420,8 +1420,8 @@ class TestWalletSending(TestCaseForTestnet):
self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid()) self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_write): def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_save_db):
wallet1 = WalletIntegrityHelper.create_standard_wallet( wallet1 = WalletIntegrityHelper.create_standard_wallet(
keystore.from_seed('humor argue expand gain goat shiver remove morning security casual leopard degree', ''), keystore.from_seed('humor argue expand gain goat shiver remove morning security casual leopard degree', ''),
gap_limit=2, gap_limit=2,
@ -1512,8 +1512,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.config = SimpleConfig({'electrum_path': self.electrum_path})
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_old_electrum_seed_online_mpk(self, mock_write): def test_sending_offline_old_electrum_seed_online_mpk(self, mock_save_db):
wallet_offline = WalletIntegrityHelper.create_standard_wallet( wallet_offline = WalletIntegrityHelper.create_standard_wallet(
keystore.from_seed('alone body father children lead goodbye phone twist exist grass kick join', '', False), keystore.from_seed('alone body father children lead goodbye phone twist exist grass kick join', '', False),
gap_limit=4, gap_limit=4,
@ -1559,8 +1559,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('06032230d0bf6a277bc4f8c39e3311a712e0e614626d0dea7cc9f592abfae5d8', tx.wtxid()) self.assertEqual('06032230d0bf6a277bc4f8c39e3311a712e0e614626d0dea7cc9f592abfae5d8', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_write): def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_save_db):
wallet_offline = WalletIntegrityHelper.create_standard_wallet( wallet_offline = WalletIntegrityHelper.create_standard_wallet(
# bip39: "qwe", der: m/44'/1'/0' # bip39: "qwe", der: m/44'/1'/0'
keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'),
@ -1605,8 +1605,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.wtxid()) self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_write): def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_save_db):
wallet_offline = WalletIntegrityHelper.create_standard_wallet( wallet_offline = WalletIntegrityHelper.create_standard_wallet(
# bip39: "qwe", der: m/49'/1'/0' # bip39: "qwe", der: m/49'/1'/0'
keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'),
@ -1652,8 +1652,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('27b78ec072a403b0545258e7a1a8d494e4b6fd48bf77f4251a12160c92207cbc', tx.wtxid()) self.assertEqual('27b78ec072a403b0545258e7a1a8d494e4b6fd48bf77f4251a12160c92207cbc', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_write): def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_save_db):
wallet_offline = WalletIntegrityHelper.create_standard_wallet( wallet_offline = WalletIntegrityHelper.create_standard_wallet(
# bip39: "qwe", der: m/84'/1'/0' # bip39: "qwe", der: m/84'/1'/0'
keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'),
@ -1699,8 +1699,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid()) self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_offline_signing_beyond_gap_limit(self, mock_write): def test_offline_signing_beyond_gap_limit(self, mock_save_db):
wallet_offline = WalletIntegrityHelper.create_standard_wallet( wallet_offline = WalletIntegrityHelper.create_standard_wallet(
# bip39: "qwe", der: m/84'/1'/0' # bip39: "qwe", der: m/84'/1'/0'
keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'),
@ -1746,8 +1746,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid()) self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_wif_online_addr_p2pkh(self, mock_write): # compressed pubkey def test_sending_offline_wif_online_addr_p2pkh(self, mock_save_db): # compressed pubkey
wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config) wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config)
wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', password=None) wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', password=None)
wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)
@ -1785,8 +1785,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_write): def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_save_db):
wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config) wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config)
wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', password=None) wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', password=None)
wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)
@ -1824,8 +1824,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_wif_online_addr_p2wpkh(self, mock_write): def test_sending_offline_wif_online_addr_p2wpkh(self, mock_save_db):
wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config) wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config)
wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', password=None) wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', password=None)
wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config)
@ -1863,8 +1863,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_xprv_online_addr_p2pkh(self, mock_write): # compressed pubkey def test_sending_offline_xprv_online_addr_p2pkh(self, mock_save_db): # compressed pubkey
wallet_offline = WalletIntegrityHelper.create_standard_wallet( wallet_offline = WalletIntegrityHelper.create_standard_wallet(
# bip39: "qwe", der: m/44'/1'/0' # bip39: "qwe", der: m/44'/1'/0'
keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'),
@ -1906,8 +1906,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_write): def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_save_db):
wallet_offline = WalletIntegrityHelper.create_standard_wallet( wallet_offline = WalletIntegrityHelper.create_standard_wallet(
# bip39: "qwe", der: m/49'/1'/0' # bip39: "qwe", der: m/49'/1'/0'
keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'),
@ -1949,8 +1949,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_write): def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_save_db):
wallet_offline = WalletIntegrityHelper.create_standard_wallet( wallet_offline = WalletIntegrityHelper.create_standard_wallet(
# bip39: "qwe", der: m/84'/1'/0' # bip39: "qwe", der: m/84'/1'/0'
keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'),
@ -1992,8 +1992,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_write): def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_save_db):
# 2-of-3 legacy p2sh multisig # 2-of-3 legacy p2sh multisig
wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet(
[ [
@ -2059,8 +2059,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('0e8fdc8257a85ebe7eeab14a53c2c258c61a511f64176b7f8fc016bc2263d307', tx.wtxid()) self.assertEqual('0e8fdc8257a85ebe7eeab14a53c2c258c61a511f64176b7f8fc016bc2263d307', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_write): def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_save_db):
# 2-of-2 p2sh-embedded segwit multisig # 2-of-2 p2sh-embedded segwit multisig
wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet(
[ [
@ -2130,8 +2130,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet):
self.assertEqual('96d0bca1001778c54e4c3a07929fab5562c5b5a23fd1ca3aa3870cc5df2bf97d', tx.wtxid()) self.assertEqual('96d0bca1001778c54e4c3a07929fab5562c5b5a23fd1ca3aa3870cc5df2bf97d', tx.wtxid())
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_write): def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_save_db):
# 2-of-3 p2wsh multisig # 2-of-3 p2wsh multisig
wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet(
[ [
@ -2235,24 +2235,24 @@ class TestWalletHistory_SimpleRandomOrder(TestCaseForTestnet):
w.create_new_address(for_change=True) w.create_new_address(for_change=True)
return w return w
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_restoring_old_wallet_txorder1(self, mock_write): def test_restoring_old_wallet_txorder1(self, mock_save_db):
w = self.create_old_wallet() w = self.create_old_wallet()
for i in [2, 12, 7, 9, 11, 10, 16, 6, 17, 1, 13, 15, 5, 8, 4, 0, 14, 18, 3]: for i in [2, 12, 7, 9, 11, 10, 16, 6, 17, 1, 13, 15, 5, 8, 4, 0, 14, 18, 3]:
tx = Transaction(self.transactions[self.txid_list[i]]) tx = Transaction(self.transactions[self.txid_list[i]])
w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual(27633300, sum(w.get_balance())) self.assertEqual(27633300, sum(w.get_balance()))
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_restoring_old_wallet_txorder2(self, mock_write): def test_restoring_old_wallet_txorder2(self, mock_save_db):
w = self.create_old_wallet() w = self.create_old_wallet()
for i in [9, 18, 2, 0, 13, 3, 1, 11, 4, 17, 7, 14, 12, 15, 10, 8, 5, 6, 16]: for i in [9, 18, 2, 0, 13, 3, 1, 11, 4, 17, 7, 14, 12, 15, 10, 8, 5, 6, 16]:
tx = Transaction(self.transactions[self.txid_list[i]]) tx = Transaction(self.transactions[self.txid_list[i]])
w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual(27633300, sum(w.get_balance())) self.assertEqual(27633300, sum(w.get_balance()))
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_restoring_old_wallet_txorder3(self, mock_write): def test_restoring_old_wallet_txorder3(self, mock_save_db):
w = self.create_old_wallet() w = self.create_old_wallet()
for i in [5, 8, 17, 0, 9, 10, 12, 3, 15, 18, 2, 11, 14, 7, 16, 1, 4, 6, 13]: for i in [5, 8, 17, 0, 9, 10, 12, 3, 15, 18, 2, 11, 14, 7, 16, 1, 4, 6, 13]:
tx = Transaction(self.transactions[self.txid_list[i]]) tx = Transaction(self.transactions[self.txid_list[i]])
@ -2283,10 +2283,10 @@ class TestWalletHistory_EvilGapLimit(TestCaseForTestnet):
w = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=20, config=self.config) w = WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=20, config=self.config)
return w return w
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_restoring_wallet_txorder1(self, mock_write): def test_restoring_wallet_txorder1(self, mock_save_db):
w = self.create_wallet() w = self.create_wallet()
w.storage.put('stored_height', 1316917 + 100) w.db.put('stored_height', 1316917 + 100)
for txid in self.transactions: for txid in self.transactions:
tx = Transaction(self.transactions[txid]) tx = Transaction(self.transactions[txid])
w.add_transaction(tx) w.add_transaction(tx)
@ -2331,8 +2331,8 @@ class TestWalletHistory_DoubleSpend(TestCaseForTestnet):
super().setUp() super().setUp()
self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.config = SimpleConfig({'electrum_path': self.electrum_path})
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_restoring_wallet_without_manual_delete(self, mock_write): def test_restoring_wallet_without_manual_delete(self, mock_save_db):
w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel", w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel",
path='if_this_exists_mocking_failed_648151893', path='if_this_exists_mocking_failed_648151893',
gap_limit=5, gap_limit=5,
@ -2345,8 +2345,8 @@ class TestWalletHistory_DoubleSpend(TestCaseForTestnet):
# txn C is double-spending txn B, to a wallet address # txn C is double-spending txn B, to a wallet address
self.assertEqual(999890, sum(w.get_balance())) self.assertEqual(999890, sum(w.get_balance()))
@mock.patch.object(storage.WalletStorage, '_write') @mock.patch.object(wallet.Abstract_Wallet, 'save_db')
def test_restoring_wallet_with_manual_delete(self, mock_write): def test_restoring_wallet_with_manual_delete(self, mock_save_db):
w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel", w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel",
path='if_this_exists_mocking_failed_648151893', path='if_this_exists_mocking_failed_648151893',
gap_limit=5, gap_limit=5,

View file

@ -60,6 +60,7 @@ from . import keystore
from .keystore import load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, AddressIndexGeneric from .keystore import load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, AddressIndexGeneric
from .util import multisig_type from .util import multisig_type
from .storage import StorageEncryptionVersion, WalletStorage from .storage import StorageEncryptionVersion, WalletStorage
from .wallet_db import WalletDB
from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32 from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32
from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput, from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint) PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint)
@ -225,44 +226,49 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
txin_type: str txin_type: str
wallet_type: str wallet_type: str
def __init__(self, storage: WalletStorage, *, config: SimpleConfig): def __init__(self, db: WalletDB, storage: Optional[WalletStorage], *, config: SimpleConfig):
if not storage.is_ready_to_be_used_by_wallet(): if not db.is_ready_to_be_used_by_wallet():
raise Exception("storage not ready to be used by Abstract_Wallet") raise Exception("storage not ready to be used by Abstract_Wallet")
self.config = config self.config = config
assert self.config is not None, "config must not be None" assert self.config is not None, "config must not be None"
self.db = db
self.storage = storage self.storage = storage
# load addresses needs to be called before constructor for sanity checks # load addresses needs to be called before constructor for sanity checks
self.storage.db.load_addresses(self.wallet_type) db.load_addresses(self.wallet_type)
self.keystore = None # type: Optional[KeyStore] # will be set by load_keystore self.keystore = None # type: Optional[KeyStore] # will be set by load_keystore
AddressSynchronizer.__init__(self, storage.db) AddressSynchronizer.__init__(self, db)
# saved fields # saved fields
self.use_change = storage.get('use_change', True) self.use_change = db.get('use_change', True)
self.multiple_change = storage.get('multiple_change', False) self.multiple_change = db.get('multiple_change', False)
self.labels = storage.db.get_dict('labels') self.labels = db.get_dict('labels')
self.frozen_addresses = set(storage.get('frozen_addresses', [])) self.frozen_addresses = set(db.get('frozen_addresses', []))
self.frozen_coins = set(storage.get('frozen_coins', [])) # set of txid:vout strings self.frozen_coins = set(db.get('frozen_coins', [])) # set of txid:vout strings
self.fiat_value = storage.db.get_dict('fiat_value') self.fiat_value = db.get_dict('fiat_value')
self.receive_requests = storage.db.get_dict('payment_requests') self.receive_requests = db.get_dict('payment_requests')
self.invoices = storage.db.get_dict('invoices') self.invoices = db.get_dict('invoices')
self._prepare_onchain_invoice_paid_detection() self._prepare_onchain_invoice_paid_detection()
self.calc_unused_change_addresses() self.calc_unused_change_addresses()
# save wallet type the first time # save wallet type the first time
if self.storage.get('wallet_type') is None: if self.db.get('wallet_type') is None:
self.storage.put('wallet_type', self.wallet_type) self.db.put('wallet_type', self.wallet_type)
self.contacts = Contacts(self.storage) self.contacts = Contacts(self.db)
self._coin_price_cache = {} self._coin_price_cache = {}
# lightning # lightning
ln_xprv = self.storage.get('lightning_privkey2') ln_xprv = self.db.get('lightning_privkey2')
self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None
def save_db(self):
if self.storage:
self.db.write(self.storage)
def has_lightning(self): def has_lightning(self):
return bool(self.lnworker) return bool(self.lnworker)
def init_lightning(self): def init_lightning(self):
if self.storage.get('lightning_privkey2'): if self.db.get('lightning_privkey2'):
return return
if not is_using_fast_ecc(): if not is_using_fast_ecc():
raise Exception('libsecp256k1 library not available. ' raise Exception('libsecp256k1 library not available. '
@ -272,30 +278,30 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
seed = os.urandom(32) seed = os.urandom(32)
node = BIP32Node.from_rootseed(seed, xtype='standard') node = BIP32Node.from_rootseed(seed, xtype='standard')
ln_xprv = node.to_xprv() ln_xprv = node.to_xprv()
self.storage.put('lightning_privkey2', ln_xprv) self.db.put('lightning_privkey2', ln_xprv)
self.storage.write() self.save_db()
def remove_lightning(self): def remove_lightning(self):
if not self.storage.get('lightning_privkey2'): if not self.db.get('lightning_privkey2'):
return return
if bool(self.lnworker.channels): if bool(self.lnworker.channels):
raise Exception('Error: This wallet has channels') raise Exception('Error: This wallet has channels')
self.storage.put('lightning_privkey2', None) self.db.put('lightning_privkey2', None)
self.storage.write() self.save_db()
def stop_threads(self): def stop_threads(self):
super().stop_threads() super().stop_threads()
if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]): if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
self.save_keystore() self.save_keystore()
self.storage.write() self.save_db()
def set_up_to_date(self, b): def set_up_to_date(self, b):
super().set_up_to_date(b) super().set_up_to_date(b)
if b: self.storage.write() if b: self.save_db()
def clear_history(self): def clear_history(self):
super().clear_history() super().clear_history()
self.storage.write() self.save_db()
def start_network(self, network): def start_network(self, network):
AddressSynchronizer.start_network(self, network) AddressSynchronizer.start_network(self, network)
@ -325,7 +331,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return [] return []
def basename(self) -> str: def basename(self) -> str:
return self.storage.basename() return self.storage.basename() if self.storage else 'no name'
def test_addresses_sanity(self) -> None: def test_addresses_sanity(self) -> None:
addrs = self.get_receiving_addresses() addrs = self.get_receiving_addresses()
@ -615,11 +621,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
else: else:
raise Exception('Unsupported invoice type') raise Exception('Unsupported invoice type')
self.invoices[key] = invoice self.invoices[key] = invoice
self.storage.write() self.save_db()
def clear_invoices(self): def clear_invoices(self):
self.invoices = {} self.invoices = {}
self.storage.write() self.save_db()
def get_invoices(self): def get_invoices(self):
out = [self.get_invoice(key) for key in self.invoices.keys()] out = [self.get_invoice(key) for key in self.invoices.keys()]
@ -1094,7 +1100,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.frozen_addresses |= set(addrs) self.frozen_addresses |= set(addrs)
else: else:
self.frozen_addresses -= set(addrs) self.frozen_addresses -= set(addrs)
self.storage.put('frozen_addresses', list(self.frozen_addresses)) self.db.put('frozen_addresses', list(self.frozen_addresses))
return True return True
return False return False
@ -1106,7 +1112,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.frozen_coins |= set(utxos) self.frozen_coins |= set(utxos)
else: else:
self.frozen_coins -= set(utxos) self.frozen_coins -= set(utxos)
self.storage.put('frozen_coins', list(self.frozen_coins)) self.db.put('frozen_coins', list(self.frozen_coins))
def wait_until_synchronized(self, callback=None): def wait_until_synchronized(self, callback=None):
def wait_for_wallet(): def wait_for_wallet():
@ -1683,12 +1689,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
If True, e.g. signing a transaction will require a password. If True, e.g. signing a transaction will require a password.
""" """
if self.can_have_keystore_encryption(): if self.can_have_keystore_encryption():
return self.storage.get('use_encryption', False) return self.db.get('use_encryption', False)
return False return False
def has_storage_encryption(self): def has_storage_encryption(self):
"""Returns whether encryption is enabled for the wallet file on disk.""" """Returns whether encryption is enabled for the wallet file on disk."""
return self.storage.is_encrypted() return self.storage and self.storage.is_encrypted()
@classmethod @classmethod
def may_have_password(cls): def may_have_password(cls):
@ -1697,18 +1703,21 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def check_password(self, password): def check_password(self, password):
if self.has_keystore_encryption(): if self.has_keystore_encryption():
self.keystore.check_password(password) self.keystore.check_password(password)
self.storage.check_password(password) if self.has_storage_encryption():
self.storage.check_password(password)
def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True): def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True):
if old_pw is None and self.has_password(): if old_pw is None and self.has_password():
raise InvalidPassword() raise InvalidPassword()
self.check_password(old_pw) self.check_password(old_pw)
if self.storage:
if encrypt_storage: if encrypt_storage:
enc_version = self.get_available_storage_encryption_version() enc_version = self.get_available_storage_encryption_version()
else: else:
enc_version = StorageEncryptionVersion.PLAINTEXT enc_version = StorageEncryptionVersion.PLAINTEXT
self.storage.set_password(new_pw, enc_version) self.storage.set_password(new_pw, enc_version)
# make sure next storage.write() saves changes
self.db.set_modified(True)
# note: Encrypting storage with a hw device is currently only # note: Encrypting storage with a hw device is currently only
# allowed for non-multisig wallets. Further, # allowed for non-multisig wallets. Further,
@ -1717,8 +1726,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# extra care would need to be taken when encrypting keystores. # extra care would need to be taken when encrypting keystores.
self._update_password_for_keystore(old_pw, new_pw) self._update_password_for_keystore(old_pw, new_pw)
encrypt_keystore = self.can_have_keystore_encryption() encrypt_keystore = self.can_have_keystore_encryption()
self.storage.set_keystore_encryption(bool(new_pw) and encrypt_keystore) self.db.set_keystore_encryption(bool(new_pw) and encrypt_keystore)
self.storage.write() self.save_db()
@abstractmethod @abstractmethod
def _update_password_for_keystore(self, old_pw: Optional[str], new_pw: Optional[str]) -> None: def _update_password_for_keystore(self, old_pw: Optional[str], new_pw: Optional[str]) -> None:
@ -1840,7 +1849,7 @@ class Simple_Wallet(Abstract_Wallet):
self.save_keystore() self.save_keystore()
def save_keystore(self): def save_keystore(self):
self.storage.put('keystore', self.keystore.dump()) self.db.put('keystore', self.keystore.dump())
@abstractmethod @abstractmethod
def get_public_key(self, address: str) -> Optional[str]: def get_public_key(self, address: str) -> Optional[str]:
@ -1870,8 +1879,8 @@ class Imported_Wallet(Simple_Wallet):
wallet_type = 'imported' wallet_type = 'imported'
txin_type = 'address' txin_type = 'address'
def __init__(self, storage, *, config): def __init__(self, db, storage, *, config):
Abstract_Wallet.__init__(self, storage, config=config) Abstract_Wallet.__init__(self, db, storage, config=config)
def is_watching_only(self): def is_watching_only(self):
return self.keystore is None return self.keystore is None
@ -1880,10 +1889,10 @@ class Imported_Wallet(Simple_Wallet):
return bool(self.keystore) return bool(self.keystore)
def load_keystore(self): def load_keystore(self):
self.keystore = load_keystore(self.storage, 'keystore') if self.storage.get('keystore') else None self.keystore = load_keystore(self.db, 'keystore') if self.db.get('keystore') else None
def save_keystore(self): def save_keystore(self):
self.storage.put('keystore', self.keystore.dump()) self.db.put('keystore', self.keystore.dump())
def can_import_address(self): def can_import_address(self):
return self.is_watching_only() return self.is_watching_only()
@ -1931,7 +1940,7 @@ class Imported_Wallet(Simple_Wallet):
self.db.add_imported_address(address, {}) self.db.add_imported_address(address, {})
self.add_address(address) self.add_address(address)
if write_to_disk: if write_to_disk:
self.storage.write() self.save_db()
return good_addr, bad_addr return good_addr, bad_addr
def import_address(self, address: str) -> str: def import_address(self, address: str) -> str:
@ -1977,7 +1986,7 @@ class Imported_Wallet(Simple_Wallet):
else: else:
self.keystore.delete_imported_key(pubkey) self.keystore.delete_imported_key(pubkey)
self.save_keystore() self.save_keystore()
self.storage.write() self.save_db()
def is_mine(self, address) -> bool: def is_mine(self, address) -> bool:
return self.db.has_imported_address(address) return self.db.has_imported_address(address)
@ -2009,7 +2018,7 @@ class Imported_Wallet(Simple_Wallet):
self.add_address(addr) self.add_address(addr)
self.save_keystore() self.save_keystore()
if write_to_disk: if write_to_disk:
self.storage.write() self.save_db()
return good_addr, bad_keys return good_addr, bad_keys
def import_private_key(self, key: str, password: Optional[str]) -> str: def import_private_key(self, key: str, password: Optional[str]) -> str:
@ -2050,10 +2059,10 @@ class Imported_Wallet(Simple_Wallet):
class Deterministic_Wallet(Abstract_Wallet): class Deterministic_Wallet(Abstract_Wallet):
def __init__(self, storage, *, config): def __init__(self, db, storage, *, config):
self._ephemeral_addr_to_addr_index = {} # type: Dict[str, Sequence[int]] self._ephemeral_addr_to_addr_index = {} # type: Dict[str, Sequence[int]]
Abstract_Wallet.__init__(self, storage, config=config) Abstract_Wallet.__init__(self, db, storage, config=config)
self.gap_limit = storage.get('gap_limit', 20) self.gap_limit = db.get('gap_limit', 20)
# generate addresses now. note that without libsecp this might block # generate addresses now. note that without libsecp this might block
# for a few seconds! # for a few seconds!
self.synchronize() self.synchronize()
@ -2100,8 +2109,8 @@ class Deterministic_Wallet(Abstract_Wallet):
'''This method is not called in the code, it is kept for console use''' '''This method is not called in the code, it is kept for console use'''
if value >= self.min_acceptable_gap(): if value >= self.min_acceptable_gap():
self.gap_limit = value self.gap_limit = value
self.storage.put('gap_limit', self.gap_limit) self.db.put('gap_limit', self.gap_limit)
self.storage.write() self.save_db()
return True return True
else: else:
return False return False
@ -2232,8 +2241,8 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
""" Deterministic Wallet with a single pubkey per address """ """ Deterministic Wallet with a single pubkey per address """
def __init__(self, storage, *, config): def __init__(self, db, storage, *, config):
Deterministic_Wallet.__init__(self, storage, config=config) Deterministic_Wallet.__init__(self, db, storage, config=config)
def get_public_key(self, address): def get_public_key(self, address):
sequence = self.get_address_index(address) sequence = self.get_address_index(address)
@ -2241,7 +2250,7 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
return pubkeys[0] return pubkeys[0]
def load_keystore(self): def load_keystore(self):
self.keystore = load_keystore(self.storage, 'keystore') self.keystore = load_keystore(self.db, 'keystore')
try: try:
xtype = bip32.xpub_type(self.keystore.xpub) xtype = bip32.xpub_type(self.keystore.xpub)
except: except:
@ -2270,10 +2279,10 @@ class Standard_Wallet(Simple_Deterministic_Wallet):
class Multisig_Wallet(Deterministic_Wallet): class Multisig_Wallet(Deterministic_Wallet):
# generic m of n # generic m of n
def __init__(self, storage, *, config): def __init__(self, db, storage, *, config):
self.wallet_type = storage.get('wallet_type') self.wallet_type = db.get('wallet_type')
self.m, self.n = multisig_type(self.wallet_type) self.m, self.n = multisig_type(self.wallet_type)
Deterministic_Wallet.__init__(self, storage, config=config) Deterministic_Wallet.__init__(self, db, storage, config=config)
def get_public_keys(self, address): def get_public_keys(self, address):
return [pk.hex() for pk in self.get_public_keys_with_deriv_info(address)] return [pk.hex() for pk in self.get_public_keys_with_deriv_info(address)]
@ -2314,14 +2323,14 @@ class Multisig_Wallet(Deterministic_Wallet):
self.keystores = {} self.keystores = {}
for i in range(self.n): for i in range(self.n):
name = 'x%d/'%(i+1) name = 'x%d/'%(i+1)
self.keystores[name] = load_keystore(self.storage, name) self.keystores[name] = load_keystore(self.db, name)
self.keystore = self.keystores['x1/'] self.keystore = self.keystores['x1/']
xtype = bip32.xpub_type(self.keystore.xpub) xtype = bip32.xpub_type(self.keystore.xpub)
self.txin_type = 'p2sh' if xtype == 'standard' else xtype self.txin_type = 'p2sh' if xtype == 'standard' else xtype
def save_keystore(self): def save_keystore(self):
for name, k in self.keystores.items(): for name, k in self.keystores.items():
self.storage.put(name, k.dump()) self.db.put(name, k.dump())
def get_keystore(self): def get_keystore(self):
return self.keystores.get('x1/') return self.keystores.get('x1/')
@ -2336,13 +2345,14 @@ class Multisig_Wallet(Deterministic_Wallet):
for name, keystore in self.keystores.items(): for name, keystore in self.keystores.items():
if keystore.may_have_password(): if keystore.may_have_password():
keystore.update_password(old_pw, new_pw) keystore.update_password(old_pw, new_pw)
self.storage.put(name, keystore.dump()) self.db.put(name, keystore.dump())
def check_password(self, password): def check_password(self, password):
for name, keystore in self.keystores.items(): for name, keystore in self.keystores.items():
if keystore.may_have_password(): if keystore.may_have_password():
keystore.check_password(password) keystore.check_password(password)
self.storage.check_password(password) if self.has_storage_encryption():
self.storage.check_password(password)
def get_available_storage_encryption_version(self): def get_available_storage_encryption_version(self):
# multisig wallets are not offered hw device encryption # multisig wallets are not offered hw device encryption
@ -2385,10 +2395,10 @@ class Wallet(object):
This class is actually a factory that will return a wallet of the correct This class is actually a factory that will return a wallet of the correct
type when passed a WalletStorage instance.""" type when passed a WalletStorage instance."""
def __new__(self, storage: WalletStorage, *, config: SimpleConfig): def __new__(self, db, storage: WalletStorage, *, config: SimpleConfig):
wallet_type = storage.get('wallet_type') wallet_type = db.get('wallet_type')
WalletClass = Wallet.wallet_class(wallet_type) WalletClass = Wallet.wallet_class(wallet_type)
wallet = WalletClass(storage, config=config) wallet = WalletClass(db, storage, config=config)
return wallet return wallet
@staticmethod @staticmethod
@ -2406,19 +2416,20 @@ def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=N
storage = WalletStorage(path) storage = WalletStorage(path)
if storage.file_exists(): if storage.file_exists():
raise Exception("Remove the existing wallet first!") raise Exception("Remove the existing wallet first!")
db = WalletDB('', manual_upgrades=False)
seed = Mnemonic('en').make_seed(seed_type) seed = Mnemonic('en').make_seed(seed_type)
k = keystore.from_seed(seed, passphrase) k = keystore.from_seed(seed, passphrase)
storage.put('keystore', k.dump()) db.put('keystore', k.dump())
storage.put('wallet_type', 'standard') db.put('wallet_type', 'standard')
if gap_limit is not None: if gap_limit is not None:
storage.put('gap_limit', gap_limit) db.put('gap_limit', gap_limit)
wallet = Wallet(storage, config=config) wallet = Wallet(db, storage, config=config)
wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
wallet.synchronize() wallet.synchronize()
msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet." msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet."
wallet.storage.write() wallet.save_db()
return {'seed': seed, 'wallet': wallet, 'msg': msg} return {'seed': seed, 'wallet': wallet, 'msg': msg}
@ -2431,10 +2442,10 @@ def restore_wallet_from_text(text, *, path, config: SimpleConfig,
storage = WalletStorage(path) storage = WalletStorage(path)
if storage.file_exists(): if storage.file_exists():
raise Exception("Remove the existing wallet first!") raise Exception("Remove the existing wallet first!")
db = WalletDB('', manual_upgrades=False)
text = text.strip() text = text.strip()
if keystore.is_address_list(text): if keystore.is_address_list(text):
wallet = Imported_Wallet(storage, config=config) wallet = Imported_Wallet(db, storage, config=config)
addresses = text.split() addresses = text.split()
good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False) good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)
# FIXME tell user about bad_inputs # FIXME tell user about bad_inputs
@ -2442,8 +2453,8 @@ def restore_wallet_from_text(text, *, path, config: SimpleConfig,
raise Exception("None of the given addresses can be imported") 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()) db.put('keystore', k.dump())
wallet = Imported_Wallet(storage, config=config) wallet = Imported_Wallet(db, storage, config=config)
keys = keystore.get_private_keys(text, allow_spaces_inside_key=False) keys = keystore.get_private_keys(text, allow_spaces_inside_key=False)
good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False) good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)
# FIXME tell user about bad_inputs # FIXME tell user about bad_inputs
@ -2456,11 +2467,11 @@ def restore_wallet_from_text(text, *, path, config: SimpleConfig,
k = keystore.from_seed(text, passphrase) k = keystore.from_seed(text, passphrase)
else: else:
raise Exception("Seed or key not recognized") raise Exception("Seed or key not recognized")
storage.put('keystore', k.dump()) db.put('keystore', k.dump())
storage.put('wallet_type', 'standard') db.put('wallet_type', 'standard')
if gap_limit is not None: if gap_limit is not None:
storage.put('gap_limit', gap_limit) db.put('gap_limit', gap_limit)
wallet = Wallet(storage, config=config) wallet = Wallet(db, storage, config=config)
assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk" assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
@ -2468,5 +2479,5 @@ def restore_wallet_from_text(text, *, path, config: SimpleConfig,
msg = ("This wallet was restored offline. It may contain more addresses than displayed. " msg = ("This wallet was restored offline. It may contain more addresses than displayed. "
"Start a daemon and use load_wallet to sync its history.") "Start a daemon and use load_wallet to sync its history.")
wallet.storage.write() wallet.save_db()
return {'wallet': wallet, 'msg': msg} return {'wallet': wallet, 'msg': msg}

View file

@ -39,6 +39,7 @@ from .logging import Logger
from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, RevocationStore from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, RevocationStore
from .lnutil import ChannelConstraints, Outpoint, ShachainElement from .lnutil import ChannelConstraints, Outpoint, ShachainElement
from .json_db import StoredDict, JsonDB, locked, modifier from .json_db import StoredDict, JsonDB, locked, modifier
from .plugin import run_hook, plugin_loaders
# seed_version is now used for the version of the wallet file # seed_version is now used for the version of the wallet file
@ -62,6 +63,7 @@ class WalletDB(JsonDB):
self._called_after_upgrade_tasks = False self._called_after_upgrade_tasks = False
if raw: # loading existing db if raw: # loading existing db
self.load_data(raw) self.load_data(raw)
self.load_plugins()
else: # creating new db else: # creating new db
self.put('seed_version', FINAL_SEED_VERSION) self.put('seed_version', FINAL_SEED_VERSION)
self._after_upgrade_tasks() self._after_upgrade_tasks()
@ -99,7 +101,7 @@ class WalletDB(JsonDB):
d = self.get('accounts', {}) d = self.get('accounts', {})
return len(d) > 1 return len(d) > 1
def split_accounts(self): def get_split_accounts(self):
result = [] result = []
# backward compatibility with old wallets # backward compatibility with old wallets
d = self.get('accounts', {}) d = self.get('accounts', {})
@ -993,3 +995,45 @@ class WalletDB(JsonDB):
elif len(path) > 2 and path[-2] in ['local_config', 'remote_config'] and key in ["pubkey", "privkey"]: elif len(path) > 2 and path[-2] in ['local_config', 'remote_config'] and key in ["pubkey", "privkey"]:
v = binascii.unhexlify(v) if v is not None else None v = binascii.unhexlify(v) if v is not None else None
return v return v
def write(self, storage):
with self.lock:
self._write(storage)
def _write(self, storage):
if threading.currentThread().isDaemon():
self.logger.warning('daemon thread cannot write db')
return
if not self.modified():
return
storage.write(self.dump())
self.set_modified(False)
def is_ready_to_be_used_by_wallet(self):
return not self.requires_upgrade() and self._called_after_upgrade_tasks
def split_accounts(self, root_path):
from .storage import WalletStorage
out = []
result = self.get_split_accounts()
for data in result:
path = root_path + '.' + data['suffix']
storage = WalletStorage(path)
db = WalletDB(json.dumps(data), manual_upgrades=False)
db._called_after_upgrade_tasks = False
db.upgrade()
db.write(storage)
out.append(path)
return out
def get_action(self):
action = run_hook('get_action', self)
return action
def load_plugins(self):
wallet_type = self.get('wallet_type')
if wallet_type in plugin_loaders:
plugin_loaders[wallet_type]()
def set_keystore_encryption(self, enable):
self.put('use_encryption', enable)

View file

@ -88,6 +88,7 @@ from electrum.logging import get_logger, configure_logging
from electrum import util from electrum import util
from electrum import constants from electrum import constants
from electrum import SimpleConfig from electrum import SimpleConfig
from electrum.wallet_db import WalletDB
from electrum.wallet import Wallet from electrum.wallet import Wallet
from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption
from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled
@ -141,10 +142,17 @@ def init_cmdline(config_options, wallet_path, server):
print_stderr("Exposing a single private key can compromise your entire wallet!") print_stderr("Exposing a single private key can compromise your entire wallet!")
print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.") print_stderr("In particular, DO NOT use 'redeem private key' services proposed by third parties.")
# will we need a password
if not storage.is_encrypted():
db = WalletDB(storage.read(), manual_upgrades=False)
use_encryption = db.get('use_encryption')
else:
use_encryption = True
# commands needing password # commands needing password
if ( (cmd.requires_wallet and storage.is_encrypted() and server is False)\ if ( (cmd.requires_wallet and storage.is_encrypted() and server is False)\
or (cmdname == 'load_wallet' and storage.is_encrypted())\ or (cmdname == 'load_wallet' and storage.is_encrypted())\
or (cmd.requires_password and (storage.is_encrypted() or storage.get('use_encryption')))): or (cmd.requires_password and use_encryption)):
if storage.is_encrypted_with_hw_device(): if storage.is_encrypted_with_hw_device():
# this case is handled later in the control flow # this case is handled later in the control flow
password = None password = None
@ -218,7 +226,8 @@ async def run_offline_command(config, config_options, plugins):
password = get_password_for_hw_device_encrypted_storage(plugins) password = get_password_for_hw_device_encrypted_storage(plugins)
config_options['password'] = password config_options['password'] = password
storage.decrypt(password) storage.decrypt(password)
wallet = Wallet(storage, config=config) db = WalletDB(storage.read(), manual_upgrades=False)
wallet = Wallet(db, storage, config=config)
config_options['wallet'] = wallet config_options['wallet'] = wallet
else: else:
wallet = None wallet = None
@ -245,7 +254,7 @@ async def run_offline_command(config, config_options, plugins):
result = await func(*args, **kwargs) result = await func(*args, **kwargs)
# save wallet # save wallet
if wallet: if wallet:
wallet.storage.write() wallet.save_db()
return result return result