electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit e1ce3aace7e3143da24115890d9bae78a9f5bcaf
parent c61e5db6a951dd26fda1913ad09622a67e03ecbb
Author: ThomasV <thomasv@electrum.org>
Date:   Wed,  5 Feb 2020 15:13:37 +0100

Separate db from storage
 - storage is content-agnostic
 - db and storage are passed to wallet contructor

Diffstat:
Melectrum/base_wizard.py | 29++++++++++++++++-------------
Melectrum/commands.py | 8++++----
Melectrum/contacts.py | 8++++----
Melectrum/daemon.py | 13++++++++-----
Melectrum/gui/kivy/main_window.py | 18++++++++++--------
Melectrum/gui/kivy/uix/dialogs/installwizard.py | 10+++++-----
Melectrum/gui/qt/__init__.py | 10++++++----
Melectrum/gui/qt/installwizard.py | 27+++++++++++++++------------
Melectrum/gui/qt/main_window.py | 12++++++------
Melectrum/gui/qt/settings_dialog.py | 4++--
Melectrum/keystore.py | 4++--
Melectrum/lnpeer.py | 2+-
Melectrum/lnworker.py | 26+++++++++++++-------------
Melectrum/plugins/labels/labels.py | 4++--
Melectrum/plugins/trustedcoin/qt.py | 2+-
Melectrum/plugins/trustedcoin/trustedcoin.py | 32++++++++++++++++----------------
Melectrum/storage.py | 87+++++++++++++------------------------------------------------------------------
Melectrum/tests/test_commands.py | 14+++++++-------
Melectrum/tests/test_lnpeer.py | 14++------------
Melectrum/tests/test_storage_upgrade.py | 66++++++++++++++++++++++++++++--------------------------------------
Melectrum/tests/test_wallet.py | 12+++++++-----
Melectrum/tests/test_wallet_vertical.py | 208++++++++++++++++++++++++++++++++++++++++----------------------------------------
Melectrum/wallet.py | 173++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Melectrum/wallet_db.py | 46+++++++++++++++++++++++++++++++++++++++++++++-
Mrun_electrum | 15++++++++++++---
25 files changed, 422 insertions(+), 422 deletions(-)

diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py @@ -39,6 +39,7 @@ from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet, Abstract_Wallet) from .storage import (WalletStorage, StorageEncryptionVersion, get_derivation_used_for_hw_device_encryption) +from .wallet_db import WalletDB from .i18n import _ from .util import UserCancelled, InvalidPassword, WalletFileException from .simple_config import SimpleConfig @@ -64,7 +65,7 @@ class WizardStackItem(NamedTuple): action: Any args: Any kwargs: Dict[str, Any] - storage_data: dict + db_data: dict class WizardWalletPasswordSetting(NamedTuple): @@ -95,8 +96,8 @@ class BaseWizard(Logger): def run(self, *args, **kwargs): action = args[0] args = args[1:] - storage_data = copy.deepcopy(self.data) - self._stack.append(WizardStackItem(action, args, kwargs, storage_data)) + db_data = copy.deepcopy(self.data) + self._stack.append(WizardStackItem(action, args, kwargs, db_data)) if not action: return if type(action) is tuple: @@ -122,7 +123,7 @@ class BaseWizard(Logger): stack_item = self._stack.pop() # try to undo side effects since we last entered 'previous' frame # 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 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] 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 def on_finished(): if exc is None: - self.terminate(storage=storage) + self.terminate(storage=storage, db=db) else: raise exc def do_upgrade(): nonlocal exc try: - storage.upgrade() + db.upgrade() except Exception as e: exc = e self.waiting_dialog(do_upgrade, _('Upgrading wallet format...'), on_finished=on_finished) @@ -592,6 +593,7 @@ class BaseWizard(Logger): encrypt_keystore=encrypt_keystore) self.terminate() + def create_storage(self, path): if os.path.exists(path): raise Exception('file already exists at path') @@ -600,16 +602,17 @@ class BaseWizard(Logger): pw_args = self.pw_args self.pw_args = None # clean-up so that it can get GC-ed storage = WalletStorage(path) - storage.set_keystore_encryption(bool(pw_args.password) and pw_args.encrypt_keystore) if pw_args.encrypt_storage: 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(): - storage.put(key, value) - storage.write() - storage.load_plugins() - return storage + db.put(key, value) + db.load_plugins() + db.write(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 def show_xpub_and_add_cosigners(self, xpub): diff --git a/electrum/commands.py b/electrum/commands.py @@ -262,13 +262,13 @@ class Commands: raise Exception("Can't change the password of a wallet encrypted with a hw device.") b = wallet.storage.is_encrypted() wallet.update_password(password, new_password, encrypt_storage=b) - wallet.storage.write() + wallet.save_db() return {'password':wallet.has_password()} @command('w') async def get(self, key, wallet: Abstract_Wallet = None): """Return item from wallet storage""" - return wallet.storage.get(key) + return wallet.db.get(key) @command('') async def getconfig(self, key): @@ -830,7 +830,7 @@ class Commands: tx = Transaction(tx) if not wallet.add_transaction(tx): return False - wallet.storage.write() + wallet.save_db() return tx.txid() @command('wp') @@ -906,7 +906,7 @@ class Commands: to_delete |= wallet.get_depending_transactions(txid) for tx_hash in to_delete: wallet.remove_transaction(tx_hash) - wallet.storage.write() + wallet.save_db() @command('wn') async def get_tx_status(self, txid, wallet: Abstract_Wallet = None): diff --git a/electrum/contacts.py b/electrum/contacts.py @@ -33,10 +33,10 @@ from .logging import Logger class Contacts(dict, Logger): - def __init__(self, storage): + def __init__(self, db): Logger.__init__(self) - self.storage = storage - d = self.storage.get('contacts', {}) + self.db = db + d = self.db.get('contacts', {}) try: self.update(d) except: @@ -49,7 +49,7 @@ class Contacts(dict, Logger): self[n] = ('address', k) def save(self): - self.storage.put('contacts', dict(self)) + self.db.put('contacts', dict(self)) def import_file(self, path): import_meta(path, self._validate, self.load_meta) diff --git a/electrum/daemon.py b/electrum/daemon.py @@ -47,6 +47,7 @@ from .util import PR_PAID, PR_EXPIRED, get_request_status from .util import log_exceptions, ignore_exceptions from .wallet import Wallet, Abstract_Wallet from .storage import WalletStorage +from .wallet_db import WalletDB from .commands import known_commands, Commands from .simple_config import SimpleConfig from .exchange_rate import FxThread @@ -401,20 +402,22 @@ class Daemon(Logger): if path in self._wallets: wallet = self._wallets[path] return wallet - storage = WalletStorage(path, manual_upgrades=manual_upgrades) + storage = WalletStorage(path) if not storage.file_exists(): return if storage.is_encrypted(): if not password: return 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 - if storage.requires_upgrade(): + if db.requires_upgrade(): return - if storage.get_action(): + if db.get_action(): return - wallet = Wallet(storage, config=self.config) + wallet = Wallet(db, storage, config=self.config) wallet.start_network(self.network) self._wallets[path] = wallet self.wallet = wallet diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -10,6 +10,7 @@ import asyncio from typing import TYPE_CHECKING, Optional, Union, Callable from electrum.storage import WalletStorage, StorageReadWriteError +from electrum.wallet_db import WalletDB from electrum.wallet import Wallet, InternalAddressCorruption, Abstract_Wallet from electrum.plugin import run_hook 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): if self.wallet: self.wallet.use_change = self.use_change - self.wallet.storage.put('use_change', self.use_change) - self.wallet.storage.write() + self.wallet.db.put('use_change', self.use_change) + self.wallet.save_db() use_unconfirmed = BooleanProperty(False) def on_use_unconfirmed(self, instance, x): @@ -588,9 +589,9 @@ class ElectrumWindow(App): else: return '' - def on_wizard_complete(self, wizard, storage): + def on_wizard_complete(self, wizard, storage, db): if storage: - wallet = Wallet(storage, config=self.electrum_config) + wallet = Wallet(db, storage, config=self.electrum_config) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) self.load_wallet(wallet) @@ -602,13 +603,14 @@ class ElectrumWindow(App): def _on_decrypted_storage(self, storage: WalletStorage): 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.path = storage.path wizard.bind(on_wizard_complete=self.on_wizard_complete) - wizard.upgrade_storage(storage) + wizard.upgrade_storage(storage, db) 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): if not path: @@ -624,7 +626,7 @@ class ElectrumWindow(App): self.load_wallet(wallet) else: def launch_wizard(): - storage = WalletStorage(path, manual_upgrades=True) + storage = WalletStorage(path) if not storage.file_exists(): wizard = Factory.InstallWizard(self.electrum_config, self.plugins) wizard.path = path diff --git a/electrum/gui/kivy/uix/dialogs/installwizard.py b/electrum/gui/kivy/uix/dialogs/installwizard.py @@ -633,7 +633,7 @@ class WizardDialog(EventsDialog): self._on_release = True self.close() if not button: - self.parent.dispatch('on_wizard_complete', None) + self.parent.dispatch('on_wizard_complete', None, None) return if button is self.ids.back: self.wizard.go_back() @@ -1055,7 +1055,7 @@ class InstallWizard(BaseWizard, Widget): __events__ = ('on_wizard_complete', ) - def on_wizard_complete(self, wallet): + def on_wizard_complete(self, storage, db): """overriden by main_window""" pass @@ -1086,10 +1086,10 @@ class InstallWizard(BaseWizard, Widget): t = threading.Thread(target = target) 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: - storage = self.create_storage(self.path) - self.dispatch('on_wizard_complete', storage) + storage, db = self.create_storage(self.path) + self.dispatch('on_wizard_complete', storage, db) def choice_dialog(self, **kwargs): choices = kwargs['choices'] diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py @@ -48,6 +48,7 @@ from electrum.base_wizard import GoBack from electrum.util import (UserCancelled, profiler, WalletFileException, BitcoinException, get_new_wallet_name) from electrum.wallet import Wallet, Abstract_Wallet +from electrum.wallet_db import WalletDB from electrum.logging import Logger from .installwizard import InstallWizard, WalletAlreadyOpenInMemory @@ -306,9 +307,10 @@ class ElectrumGui(Logger): if storage is None: wizard.path = path # needed by trustedcoin plugin wizard.run('new') - storage = wizard.create_storage(path) + storage, db = wizard.create_storage(path) else: - wizard.run_upgrades(storage) + db = WalletDB(storage.read(), manual_upgrades=False) + wizard.run_upgrades(storage, db) except (UserCancelled, GoBack): return except WalletAlreadyOpenInMemory as e: @@ -316,9 +318,9 @@ class ElectrumGui(Logger): finally: wizard.terminate() # return if wallet creation is not complete - if storage is None or storage.get_action(): + if storage is None or db.get_action(): return - wallet = Wallet(storage, config=self.config) + wallet = Wallet(db, storage, config=self.config) wallet.start_network(self.daemon.network) self.daemon.add_wallet(wallet) return wallet diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py @@ -3,6 +3,7 @@ # file LICENCE or http://www.opensource.org/licenses/mit-license.php import os +import json import sys import threading import traceback @@ -225,7 +226,7 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): if wallet_from_memory: temp_storage = wallet_from_memory.storage # type: Optional[WalletStorage] else: - temp_storage = WalletStorage(path, manual_upgrades=True) + temp_storage = WalletStorage(path) except (StorageReadWriteError, WalletFileException) as e: msg = _('Cannot read file') + f'\n{repr(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) - def run_upgrades(self, storage): + def run_upgrades(self, storage, db): path = storage.path - if storage.requires_split(): + if db.requires_split(): self.hide() 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) if not self.question(msg): return - file_list = '\n'.join(storage.split_accounts()) - msg = _('Your accounts have been moved to') + ':\n' + file_list + '\n\n'+ _('Do you want to delete the old file') + ':\n' + path + file_list = db.split_accounts(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): os.remove(path) self.show_warning(_('The file was removed')) # raise now, to avoid having the old storage opened raise UserCancelled() - action = storage.get_action() - if action and storage.requires_upgrade(): + action = db.get_action() + if action and db.requires_upgrade(): raise WalletFileException('Incomplete wallet files cannot be upgraded.') if action: self.hide() @@ -345,15 +346,17 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard): self.show_warning(_('The file was removed')) return self.show() - self.data = storage.db.data # FIXME + self.data = json.loads(storage.read()) self.run(action) for k, v in self.data.items(): - storage.put(k, v) - storage.write() + db.put(k, v) + db.write(storage) return - if storage.requires_upgrade(): - self.upgrade_storage(storage) + if db.requires_upgrade(): + self.upgrade_db(storage, db) + + return db def finished(self): """Called in hardware client wrapper, in order to close popups.""" diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -456,7 +456,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): send_exception_to_crash_reporter(e) def init_geometry(self): - winpos = self.wallet.storage.get("winpos-qt") + winpos = self.wallet.db.get("winpos-qt") try: screen = self.app.desktop().screenGeometry() assert screen.contains(QRect(*winpos)) @@ -469,7 +469,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): name = "Electrum Testnet" if constants.net.TESTNET else "Electrum" title = '%s %s - %s' % (name, ELECTRUM_VERSION, self.wallet.basename()) - extra = [self.wallet.storage.get('wallet_type', '?')] + extra = [self.wallet.db.get('wallet_type', '?')] if self.wallet.is_watching_only(): extra.append(_('watching only')) title += ' [%s]'% ', '.join(extra) @@ -1958,7 +1958,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def update_console(self): 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.updateNamespace({ @@ -2154,7 +2154,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): dialog.setMinimumSize(500, 100) mpk_list = self.wallet.get_master_public_keys() vbox = QVBoxLayout() - wallet_type = self.wallet.storage.get('wallet_type', '') + wallet_type = self.wallet.db.get('wallet_type', '') if self.wallet.is_watching_only(): wallet_type += ' [{}]'.format(_('watching-only')) 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()) if not self.isMaximized(): 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()]) - 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: self.qr_window.close() self.close_wallet() diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py @@ -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 if 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) usechange_cb.stateChanged.connect(on_usechange) 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 if 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_cb = QCheckBox(_('Use multiple change addresses')) multiple_cb.setEnabled(self.wallet.use_change) diff --git a/electrum/keystore.py b/electrum/keystore.py @@ -864,8 +864,8 @@ def hardware_keystore(d) -> Hardware_KeyStore: raise WalletFileException(f'unknown hardware type: {hw_type}. ' f'hw_keystores: {list(hw_keystores)}') -def load_keystore(storage, name) -> KeyStore: - d = storage.get(name, {}) +def load_keystore(db, name) -> KeyStore: + d = db.get(name, {}) t = d.get('type') if not t: raise WalletFileException( diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -617,7 +617,7 @@ class Peer(Logger): "revocation_store": {}, } 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 return channels.get(channel_id) diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -341,25 +341,25 @@ class LNWallet(LNWorker): def __init__(self, wallet: 'Abstract_Wallet', xprv): Logger.__init__(self) self.wallet = wallet - self.storage = wallet.storage + self.db = wallet.db self.config = wallet.config LNWorker.__init__(self, xprv) self.ln_keystore = keystore.from_xprv(xprv) self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ - self.payments = self.storage.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid - self.preimages = self.storage.db.get_dict('lightning_preimages') # RHASH -> preimage + self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid + self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage self.sweep_address = wallet.get_receiving_address() self.lock = threading.RLock() self.logs = defaultdict(list) # type: Dict[str, List[PaymentAttemptLog]] # key is RHASH # note: accessing channels (besides simple lookup) needs self.lock! self.channels = {} - channels = self.storage.db.get_dict("channels") + channels = self.db.get_dict("channels") for channel_id, c in channels.items(): self.channels[bfh(channel_id)] = Channel(c, sweep_address=self.sweep_address, lnworker=self) # 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) @ignore_exceptions @@ -585,10 +585,10 @@ class LNWallet(LNWorker): def get_and_inc_counter_for_channel_keys(self): 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 - self.storage.put('lightning_channel_key_der_ctr', ctr) - self.storage.write() + self.db.put('lightning_channel_key_der_ctr', ctr) + self.wallet.save_db() return ctr def suggest_peer(self): @@ -610,7 +610,7 @@ class LNWallet(LNWorker): assert type(chan) is Channel 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") - self.wallet.storage.write() + self.wallet.save_db() self.network.trigger_callback('channel', chan) def save_short_chan_id(self, chan): @@ -1127,7 +1127,7 @@ class LNWallet(LNWorker): def save_preimage(self, payment_hash: bytes, preimage: bytes): assert sha256(preimage) == payment_hash self.preimages[bh2u(payment_hash)] = bh2u(preimage) - self.storage.write() + self.wallet.save_db() def get_preimage(self, payment_hash: bytes) -> bytes: 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] with self.lock: self.payments[key] = info.amount, info.direction, info.status - self.storage.write() + self.wallet.save_db() def get_payment_status(self, payment_hash): try: @@ -1230,7 +1230,7 @@ class LNWallet(LNWorker): del self.payments[payment_hash_hex] except KeyError: return - self.storage.write() + self.wallet.save_db() def get_balance(self): with self.lock: @@ -1276,7 +1276,7 @@ class LNWallet(LNWorker): with self.lock: self.channels.pop(chan_id) 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('wallet_updated', self.wallet) diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py @@ -46,7 +46,7 @@ class LabelsPlugin(BasePlugin): def get_nonce(self, wallet): # 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: nonce = 1 self.set_nonce(wallet, nonce) @@ -54,7 +54,7 @@ class LabelsPlugin(BasePlugin): def set_nonce(self, wallet, nonce): self.logger.info(f"set {wallet.basename()} nonce to {nonce}") - wallet.storage.put("wallet_nonce", nonce) + wallet.db.put("wallet_nonce", nonce) @hook def set_label(self, wallet, item, label): diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py @@ -227,7 +227,7 @@ class Plugin(TrustedCoinPlugin): wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use')) except GoBack: # user clicked 'Cancel' and decided to move wallet file manually - wizard.create_storage(wizard.path) + storage, db = wizard.create_storage(wizard.path) raise def accept_terms_of_use(self, window): diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py @@ -264,17 +264,17 @@ class Wallet_2fa(Multisig_Wallet): wallet_type = '2fa' - def __init__(self, storage, *, config): + def __init__(self, db, *, config): 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.billing_info = None self._load_billing_addresses() def _load_billing_addresses(self): billing_addresses = { - 'legacy': self.storage.get('trustedcoin_billing_addresses', {}), - 'segwit': self.storage.get('trustedcoin_billing_addresses_segwit', {}) + 'legacy': self.db.get('trustedcoin_billing_addresses', {}), + 'segwit': self.db.get('trustedcoin_billing_addresses_segwit', {}) } self._billing_addresses = {} # type: Dict[str, Dict[int, str]] # addr_type -> index -> addr 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() def get_user_id(self): - return get_user_id(self.storage) + return get_user_id(self.db) def min_prepay(self): return min(self.price_per_tx.keys()) @@ -383,10 +383,10 @@ class Wallet_2fa(Multisig_Wallet): billing_addresses_of_this_type[billing_index] = address self._billing_addresses_set.add(address) self._billing_addresses[addr_type] = billing_addresses_of_this_type - self.storage.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', self._billing_addresses['legacy']) + self.db.put('trustedcoin_billing_addresses_segwit', self._billing_addresses['segwit']) # 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: return addr in self._billing_addresses_set @@ -394,11 +394,11 @@ class Wallet_2fa(Multisig_Wallet): # Utility functions -def get_user_id(storage): +def get_user_id(db): def make_long_id(xpub_hot, xpub_cold): return sha256(''.join(sorted([xpub_hot, xpub_cold]))) - xpub1 = storage.get('x1/')['xpub'] - xpub2 = storage.get('x2/')['xpub'] + xpub1 = db.get('x1/')['xpub'] + xpub2 = db.get('x2/')['xpub'] long_id = make_long_id(xpub1, xpub2) short_id = hashlib.sha256(long_id).hexdigest() return long_id, short_id @@ -753,12 +753,12 @@ class TrustedCoinPlugin(BasePlugin): self.request_otp_dialog(wizard, short_id, new_secret, xpub3) @hook - def get_action(self, storage): - if storage.get('wallet_type') != '2fa': + def get_action(self, db): + if db.get('wallet_type') != '2fa': return - if not storage.get('x1/'): + if not db.get('x1/'): return self, 'show_disclaimer' - if not storage.get('x2/'): + if not db.get('x2/'): return self, 'show_disclaimer' - if not storage.get('x3/'): + if not db.get('x3/'): return self, 'accept_terms_of_use' diff --git a/electrum/storage.py b/electrum/storage.py @@ -32,7 +32,6 @@ from enum import IntEnum from . import ecc from .util import profiler, InvalidPassword, WalletFileException, bfh, standardize_path -from .plugin import run_hook, plugin_loaders from .wallet_db import WalletDB from .logging import Logger @@ -53,28 +52,27 @@ class StorageEncryptionVersion(IntEnum): class StorageReadWriteError(Exception): pass +# TODO: Rename to Storage class WalletStorage(Logger): - def __init__(self, path, *, manual_upgrades: bool = False): + def __init__(self, path): Logger.__init__(self) self.path = standardize_path(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.pubkey = None + self.decrypted = '' self._test_read_write_permissions(self.path) if self.file_exists(): with open(self.path, "r", encoding='utf-8') as f: self.raw = f.read() 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: + self.raw = '' 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 def _test_read_write_permissions(cls, path): @@ -98,29 +96,9 @@ class WalletStorage(Logger): if echo != echo2: 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 - def write(self): - with self.db.lock: - 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()) + def write(self, data): + s = self.encrypt_before_writing(data) temp_path = "%s.tmp.%s" % (self.path, os.getpid()) with open(temp_path, "w", encoding='utf-8') as f: f.write(s) @@ -135,7 +113,6 @@ class WalletStorage(Logger): os.chmod(self.path, mode) self._file_exists = True self.logger.info(f"saved {self.path}") - self.db.set_modified(False) def file_exists(self) -> bool: return self._file_exists @@ -148,7 +125,7 @@ class WalletStorage(Logger): or if encryption is enabled but the contents have already been decrypted. """ try: - return bool(self.db.data) + return not self.is_encrypted() or bool(self.decrypted) except AttributeError: return False @@ -207,12 +184,12 @@ class WalletStorage(Logger): if self.raw: enc_magic = self._get_encryption_magic() s = zlib.decompress(ec_key.decrypt_message(self.raw, enc_magic)) + s = s.decode('utf8') else: - s = None + s = '' self.pubkey = ec_key.get_public_key_hex() - s = s.decode('utf8') - self.db = WalletDB(s, manual_upgrades=self._manual_upgrades) - self.load_plugins() + self.decrypted = s + return s def encrypt_before_writing(self, plaintext: str) -> str: 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(): raise InvalidPassword() - def set_keystore_encryption(self, enable): - self.put('use_encryption', enable) - def set_password(self, password, enc_version=None): """Set a password to be used for encrypting this storage.""" if enc_version is None: @@ -248,40 +222,7 @@ class WalletStorage(Logger): else: self.pubkey = None self._encryption_version = StorageEncryptionVersion.PLAINTEXT - # make sure next storage.write() saves changes - self.db.set_modified(True) def basename(self) -> str: 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 diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py @@ -4,7 +4,7 @@ from decimal import Decimal from electrum.util import create_and_start_event_loop 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.simple_config import SimpleConfig @@ -77,8 +77,8 @@ class TestCommands(ElectrumTestCase): for xkey2, xtype2 in xprvs: self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2))) - @mock.patch.object(storage.WalletStorage, '_write') - def test_encrypt_decrypt(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_encrypt_decrypt(self, mock_save_db): wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN', path='if_this_exists_mocking_failed_648151893', config=self.config)['wallet'] @@ -88,8 +88,8 @@ class TestCommands(ElectrumTestCase): ciphertext = cmds._run('encrypt', (pubkey, cleartext)) self.assertEqual(cleartext, cmds._run('decrypt', (pubkey, ciphertext), wallet=wallet)) - @mock.patch.object(storage.WalletStorage, '_write') - def test_export_private_key_imported(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_export_private_key_imported(self, mock_save_db): wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', path='if_this_exists_mocking_failed_648151893', config=self.config)['wallet'] @@ -107,8 +107,8 @@ class TestCommands(ElectrumTestCase): self.assertEqual(['p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'], cmds._run('getprivatekeys', (['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'], ), wallet=wallet)) - @mock.patch.object(storage.WalletStorage, '_write') - def test_export_private_key_deterministic(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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', gap_limit=2, path='if_this_exists_mocking_failed_648151893', diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py @@ -68,23 +68,13 @@ class MockNetwork: if self.tx_queue: 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: - storage = MockStorage() def set_label(self, x, y): pass + def save_db(self): + pass class MockLNWallet: - storage = MockStorage() def __init__(self, remote_keypair, local_keypair, chan, tx_queue): self.chan = chan self.remote_keypair = remote_keypair diff --git a/electrum/tests/test_storage_upgrade.py b/electrum/tests/test_storage_upgrade.py @@ -1,8 +1,9 @@ import shutil import tempfile import os +import json -from electrum.storage import WalletStorage +from electrum.wallet_db import WalletDB from electrum.wallet import Wallet from electrum import constants @@ -293,44 +294,33 @@ class TestStorageUpgrade(WalletTestCase): def _upgrade_storage(self, wallet_json, accounts=1): if accounts == 1: # test manual upgrades - storage = self._load_storage_from_json_string(wallet_json=wallet_json, - path=self.wallet_path, - manual_upgrades=True) - self.assertFalse(storage.requires_split()) - if storage.requires_upgrade(): - storage.upgrade() - self._sanity_check_upgraded_storage(storage) + db = self._load_db_from_json_string(wallet_json=wallet_json, + manual_upgrades=True) + self.assertFalse(db.requires_split()) + if db.requires_upgrade(): + db.upgrade() + self._sanity_check_upgraded_db(db) # test automatic upgrades - path2 = os.path.join(self.user_dir, "somewallet2") - storage2 = self._load_storage_from_json_string(wallet_json=wallet_json, - path=path2, - 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) + db2 = self._load_db_from_json_string(wallet_json=wallet_json, + manual_upgrades=False) + self._sanity_check_upgraded_db(db2) else: - storage = self._load_storage_from_json_string(wallet_json=wallet_json, - path=self.wallet_path, - manual_upgrades=True) - self.assertTrue(storage.requires_split()) - new_paths = storage.split_accounts() - self.assertEqual(accounts, len(new_paths)) - for new_path in new_paths: - new_storage = WalletStorage(new_path, manual_upgrades=False) - self._sanity_check_upgraded_storage(new_storage) - - def _sanity_check_upgraded_storage(self, storage): - self.assertFalse(storage.requires_split()) - self.assertFalse(storage.requires_upgrade()) - w = Wallet(storage, config=self.config) + db = self._load_db_from_json_string(wallet_json=wallet_json, + manual_upgrades=True) + self.assertTrue(db.requires_split()) + split_data = db.get_split_accounts() + self.assertEqual(accounts, len(split_data)) + for item in split_data: + data = json.dumps(item) + new_db = WalletDB(data, manual_upgrades=False) + self._sanity_check_upgraded_db(new_db) + + def _sanity_check_upgraded_db(self, db): + self.assertFalse(db.requires_split()) + self.assertFalse(db.requires_upgrade()) + w = Wallet(db, None, config=self.config) @staticmethod - def _load_storage_from_json_string(*, wallet_json, path, manual_upgrades): - with open(path, "w") as f: - f.write(wallet_json) - storage = WalletStorage(path, manual_upgrades=manual_upgrades) - return storage + def _load_db_from_json_string(*, wallet_json, manual_upgrades): + db = WalletDB(wallet_json, manual_upgrades=manual_upgrades) + return db diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py @@ -58,13 +58,15 @@ class TestWalletStorage(WalletTestCase): with open(self.wallet_path, "w") as f: contents = f.write(contents) - storage = WalletStorage(self.wallet_path, manual_upgrades=True) - self.assertEqual("b", storage.get("a")) - self.assertEqual("d", storage.get("c")) + storage = WalletStorage(self.wallet_path) + db = WalletDB(storage.read(), manual_upgrades=True) + self.assertEqual("b", db.get("a")) + self.assertEqual("d", db.get("c")) def test_write_dictionary_to_file(self): storage = WalletStorage(self.wallet_path) + db = WalletDB('', manual_upgrades=True) some_dict = { u"a": u"b", @@ -72,8 +74,8 @@ class TestWalletStorage(WalletTestCase): u"seed_version": FINAL_SEED_VERSION} for key, value in some_dict.items(): - storage.put(key, value) - storage.write() + db.put(key, value) + db.write(storage) with open(self.wallet_path, "r") as f: contents = f.read() diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py @@ -5,7 +5,7 @@ import tempfile from typing import Sequence import asyncio -from electrum import storage, bitcoin, keystore, bip32 +from electrum import storage, bitcoin, keystore, bip32, wallet from electrum import Transaction from electrum import SimpleConfig from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT @@ -46,33 +46,33 @@ class WalletIntegrityHelper: @classmethod def create_standard_wallet(cls, ks, *, config: SimpleConfig, gap_limit=None): - store = storage.WalletStorage('if_this_exists_mocking_failed_648151893') - store.put('keystore', ks.dump()) - store.put('gap_limit', gap_limit or cls.gap_limit) - w = Standard_Wallet(store, config=config) + db = storage.WalletDB('', manual_upgrades=False) + db.put('keystore', ks.dump()) + db.put('gap_limit', gap_limit or cls.gap_limit) + w = Standard_Wallet(db, None, config=config) w.synchronize() return w @classmethod 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: k = keystore.Imported_KeyStore({}) - store.put('keystore', k.dump()) - w = Imported_Wallet(store, config=config) + db.put('keystore', k.dump()) + w = Imported_Wallet(db, None, config=config) return w @classmethod def create_multisig_wallet(cls, keystores: Sequence, multisig_type: str, *, config: SimpleConfig, gap_limit=None): """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): cosigner_index = i + 1 - store.put('x%d/' % cosigner_index, ks.dump()) - store.put('wallet_type', multisig_type) - store.put('gap_limit', gap_limit or cls.gap_limit) - w = Multisig_Wallet(store, config=config) + db.put('x%d/' % cosigner_index, ks.dump()) + db.put('wallet_type', multisig_type) + db.put('gap_limit', gap_limit or cls.gap_limit) + w = Multisig_Wallet(db, None, config=config) w.synchronize() return w @@ -84,8 +84,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.config = SimpleConfig({'electrum_path': self.electrum_path}) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_standard(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_electrum_seed_standard(self, mock_save_db): seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song' self.assertEqual(seed_type(seed_words), 'standard') @@ -104,8 +104,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], '1KSezYMhAJMWqFbVFB2JshYg69UpmEXR4D') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_segwit(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_electrum_seed_segwit(self, mock_save_db): seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' self.assertEqual(seed_type(seed_words), 'segwit') @@ -124,8 +124,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], 'bc1qdy94n2q5qcp0kg7v9yzwe6wvfkhnvyzje7nx2p') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_segwit_passphrase(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_electrum_seed_segwit_passphrase(self, mock_save_db): seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' self.assertEqual(seed_type(seed_words), 'segwit') @@ -144,8 +144,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], 'bc1qcywwsy87sdp8vz5rfjh3sxdv6rt95kujdqq38g') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_old(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_electrum_seed_old(self, mock_save_db): seed_words = 'powerful random nobody notice nothing important anyway look away hidden message over' self.assertEqual(seed_type(seed_words), 'old') @@ -163,8 +163,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], '1KRW8pH6HFHZh889VDq6fEKvmrsmApwNfe') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_2fa_legacy(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_electrum_seed_2fa_legacy(self, mock_save_db): seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove' self.assertEqual(seed_type(seed_words), '2fa') @@ -198,8 +198,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_seed_2fa_segwit(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_electrum_seed_2fa_segwit(self, mock_save_db): seed_words = 'universe topic remind silver february ranch shine worth innocent cattle enhance wise' self.assertEqual(seed_type(seed_words), '2fa_segwit') @@ -233,8 +233,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], 'bc1qd4q50nft7kxm9yglfnpup9ed2ukj3tkxp793y0zya8dc9m39jcwq308dxz') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_seed_bip44_standard(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_bip39_seed_bip44_standard(self, mock_save_db): 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)) @@ -252,8 +252,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], '1GG5bVeWgAp5XW7JLCphse14QaC4qiHyWn') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_seed_bip44_standard_passphrase(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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' 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') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_seed_bip49_p2sh_segwit(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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' 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') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_seed_bip84_native_segwit(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_bip39_seed_bip84_native_segwit(self, mock_save_db): # test case from bip84 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)) @@ -310,8 +310,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_multisig_seed_standard(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_electrum_multisig_seed_standard(self, mock_save_db): seed_words = 'blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure' self.assertEqual(seed_type(seed_words), 'standard') @@ -333,8 +333,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], '36XWwEHrrVCLnhjK5MrVVGmUHghr9oWTN1') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_electrum_multisig_seed_segwit(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_electrum_multisig_seed_segwit(self, mock_save_db): seed_words = 'snow nest raise royal more walk demise rotate smooth spirit canyon gun' self.assertEqual(seed_type(seed_words), 'segwit') @@ -356,8 +356,8 @@ class TestWalletKeystoreAddressIntegrityForMainnet(ElectrumTestCase): self.assertEqual(w.get_change_addresses()[0], 'bc1qxqf840dqswcmu7a8v82fj6ej0msx08flvuy6kngr7axstjcaq6us9hrehd') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_multisig_seed_bip45_standard(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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' 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') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_multisig_seed_p2sh_segwit(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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 # der: m/49'/0'/0' # 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') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip32_extended_version_bytes(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_bip32_extended_version_bytes(self, mock_save_db): 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)) bip32_seed = keystore.bip39_to_seed(seed_words, '') @@ -467,8 +467,8 @@ class TestWalletKeystoreAddressIntegrityForTestnet(TestCaseForTestnet): super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip39_multisig_seed_p2sh_segwit_testnet(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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 # der: m/49'/1'/0' # 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') @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_bip32_extended_version_bytes(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_bip32_extended_version_bytes(self, mock_save_db): 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)) 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) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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') 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()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_save_db): wallet1a = WalletIntegrityHelper.create_multisig_wallet( [ 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()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db): wallet1a = WalletIntegrityHelper.create_multisig_wallet( [ 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()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_save_db): wallet1a = WalletIntegrityHelper.create_multisig_wallet( [ 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()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_rbf(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_rbf(self, mock_save_db): self.maxDiff = None 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): @@ -959,8 +959,8 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, 7484320, 0), wallet.get_balance()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_cpfp_p2pkh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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') # bootstrap wallet @@ -1361,8 +1361,8 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, 3_900_000, 0), wallet.get_balance()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_cpfp_p2wpkh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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') # bootstrap wallet @@ -1420,8 +1420,8 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_save_db): wallet1 = WalletIntegrityHelper.create_standard_wallet( keystore.from_seed('humor argue expand gain goat shiver remove morning security casual leopard degree', ''), gap_limit=2, @@ -1512,8 +1512,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.config = SimpleConfig({'electrum_path': self.electrum_path}) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_old_electrum_seed_online_mpk(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_old_electrum_seed_online_mpk(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( keystore.from_seed('alone body father children lead goodbye phone twist exist grass kick join', '', False), gap_limit=4, @@ -1559,8 +1559,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('06032230d0bf6a277bc4f8c39e3311a712e0e614626d0dea7cc9f592abfae5d8', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/44'/1'/0' keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), @@ -1605,8 +1605,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/49'/1'/0' keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), @@ -1652,8 +1652,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('27b78ec072a403b0545258e7a1a8d494e4b6fd48bf77f4251a12160c92207cbc', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/84'/1'/0' keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), @@ -1699,8 +1699,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_offline_signing_beyond_gap_limit(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_offline_signing_beyond_gap_limit(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/84'/1'/0' keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), @@ -1746,8 +1746,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_wif_online_addr_p2pkh(self, mock_write): # compressed pubkey + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config) @@ -1785,8 +1785,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config) @@ -1824,8 +1824,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_wif_online_addr_p2wpkh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config) @@ -1863,8 +1863,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_addr_p2pkh(self, mock_write): # compressed pubkey + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_xprv_online_addr_p2pkh(self, mock_save_db): # compressed pubkey wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/44'/1'/0' keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), @@ -1906,8 +1906,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/49'/1'/0' keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), @@ -1949,8 +1949,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/84'/1'/0' keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), @@ -1992,8 +1992,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_save_db): # 2-of-3 legacy p2sh multisig wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( [ @@ -2059,8 +2059,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('0e8fdc8257a85ebe7eeab14a53c2c258c61a511f64176b7f8fc016bc2263d307', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_save_db): # 2-of-2 p2sh-embedded segwit multisig wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( [ @@ -2130,8 +2130,8 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('96d0bca1001778c54e4c3a07929fab5562c5b5a23fd1ca3aa3870cc5df2bf97d', tx.wtxid()) @needs_test_with_all_ecc_implementations - @mock.patch.object(storage.WalletStorage, '_write') - def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_save_db): # 2-of-3 p2wsh multisig wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( [ @@ -2235,24 +2235,24 @@ class TestWalletHistory_SimpleRandomOrder(TestCaseForTestnet): w.create_new_address(for_change=True) return w - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_old_wallet_txorder1(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_restoring_old_wallet_txorder1(self, mock_save_db): 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]: tx = Transaction(self.transactions[self.txid_list[i]]) w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual(27633300, sum(w.get_balance())) - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_old_wallet_txorder2(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_restoring_old_wallet_txorder2(self, mock_save_db): 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]: tx = Transaction(self.transactions[self.txid_list[i]]) w.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual(27633300, sum(w.get_balance())) - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_old_wallet_txorder3(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_restoring_old_wallet_txorder3(self, mock_save_db): 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]: 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) return w - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_wallet_txorder1(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_restoring_wallet_txorder1(self, mock_save_db): w = self.create_wallet() - w.storage.put('stored_height', 1316917 + 100) + w.db.put('stored_height', 1316917 + 100) for txid in self.transactions: tx = Transaction(self.transactions[txid]) w.add_transaction(tx) @@ -2331,8 +2331,8 @@ class TestWalletHistory_DoubleSpend(TestCaseForTestnet): super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_wallet_without_manual_delete(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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", path='if_this_exists_mocking_failed_648151893', gap_limit=5, @@ -2345,8 +2345,8 @@ class TestWalletHistory_DoubleSpend(TestCaseForTestnet): # txn C is double-spending txn B, to a wallet address self.assertEqual(999890, sum(w.get_balance())) - @mock.patch.object(storage.WalletStorage, '_write') - def test_restoring_wallet_with_manual_delete(self, mock_write): + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + 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", path='if_this_exists_mocking_failed_648151893', gap_limit=5, diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -60,6 +60,7 @@ from . import keystore from .keystore import load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, AddressIndexGeneric from .util import multisig_type from .storage import StorageEncryptionVersion, WalletStorage +from .wallet_db import WalletDB from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32 from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint) @@ -225,44 +226,49 @@ class Abstract_Wallet(AddressSynchronizer, ABC): txin_type: str wallet_type: str - def __init__(self, storage: WalletStorage, *, config: SimpleConfig): - if not storage.is_ready_to_be_used_by_wallet(): + def __init__(self, db: WalletDB, storage: Optional[WalletStorage], *, config: SimpleConfig): + if not db.is_ready_to_be_used_by_wallet(): raise Exception("storage not ready to be used by Abstract_Wallet") self.config = config assert self.config is not None, "config must not be None" + self.db = db self.storage = storage # 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 - AddressSynchronizer.__init__(self, storage.db) + AddressSynchronizer.__init__(self, db) # saved fields - self.use_change = storage.get('use_change', True) - self.multiple_change = storage.get('multiple_change', False) - self.labels = storage.db.get_dict('labels') - self.frozen_addresses = set(storage.get('frozen_addresses', [])) - self.frozen_coins = set(storage.get('frozen_coins', [])) # set of txid:vout strings - self.fiat_value = storage.db.get_dict('fiat_value') - self.receive_requests = storage.db.get_dict('payment_requests') - self.invoices = storage.db.get_dict('invoices') + self.use_change = db.get('use_change', True) + self.multiple_change = db.get('multiple_change', False) + self.labels = db.get_dict('labels') + self.frozen_addresses = set(db.get('frozen_addresses', [])) + self.frozen_coins = set(db.get('frozen_coins', [])) # set of txid:vout strings + self.fiat_value = db.get_dict('fiat_value') + self.receive_requests = db.get_dict('payment_requests') + self.invoices = db.get_dict('invoices') self._prepare_onchain_invoice_paid_detection() self.calc_unused_change_addresses() # save wallet type the first time - if self.storage.get('wallet_type') is None: - self.storage.put('wallet_type', self.wallet_type) - self.contacts = Contacts(self.storage) + if self.db.get('wallet_type') is None: + self.db.put('wallet_type', self.wallet_type) + self.contacts = Contacts(self.db) self._coin_price_cache = {} # 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 + def save_db(self): + if self.storage: + self.db.write(self.storage) + def has_lightning(self): return bool(self.lnworker) def init_lightning(self): - if self.storage.get('lightning_privkey2'): + if self.db.get('lightning_privkey2'): return if not is_using_fast_ecc(): raise Exception('libsecp256k1 library not available. ' @@ -272,30 +278,30 @@ class Abstract_Wallet(AddressSynchronizer, ABC): seed = os.urandom(32) node = BIP32Node.from_rootseed(seed, xtype='standard') ln_xprv = node.to_xprv() - self.storage.put('lightning_privkey2', ln_xprv) - self.storage.write() + self.db.put('lightning_privkey2', ln_xprv) + self.save_db() def remove_lightning(self): - if not self.storage.get('lightning_privkey2'): + if not self.db.get('lightning_privkey2'): return if bool(self.lnworker.channels): raise Exception('Error: This wallet has channels') - self.storage.put('lightning_privkey2', None) - self.storage.write() + self.db.put('lightning_privkey2', None) + self.save_db() def stop_threads(self): super().stop_threads() if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]): self.save_keystore() - self.storage.write() + self.save_db() def set_up_to_date(self, b): super().set_up_to_date(b) - if b: self.storage.write() + if b: self.save_db() def clear_history(self): super().clear_history() - self.storage.write() + self.save_db() def start_network(self, network): AddressSynchronizer.start_network(self, network) @@ -325,7 +331,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): return [] def basename(self) -> str: - return self.storage.basename() + return self.storage.basename() if self.storage else 'no name' def test_addresses_sanity(self) -> None: addrs = self.get_receiving_addresses() @@ -615,11 +621,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC): else: raise Exception('Unsupported invoice type') self.invoices[key] = invoice - self.storage.write() + self.save_db() def clear_invoices(self): self.invoices = {} - self.storage.write() + self.save_db() def get_invoices(self): 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) else: 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 False @@ -1106,7 +1112,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): self.frozen_coins |= set(utxos) else: 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_for_wallet(): @@ -1683,12 +1689,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC): If True, e.g. signing a transaction will require a password. """ if self.can_have_keystore_encryption(): - return self.storage.get('use_encryption', False) + return self.db.get('use_encryption', False) return False def has_storage_encryption(self): """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 def may_have_password(cls): @@ -1697,18 +1703,21 @@ class Abstract_Wallet(AddressSynchronizer, ABC): def check_password(self, password): if self.has_keystore_encryption(): 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): if old_pw is None and self.has_password(): raise InvalidPassword() self.check_password(old_pw) - - if encrypt_storage: - enc_version = self.get_available_storage_encryption_version() - else: - enc_version = StorageEncryptionVersion.PLAINTEXT - self.storage.set_password(new_pw, enc_version) + if self.storage: + if encrypt_storage: + enc_version = self.get_available_storage_encryption_version() + else: + enc_version = StorageEncryptionVersion.PLAINTEXT + 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 # 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. self._update_password_for_keystore(old_pw, new_pw) encrypt_keystore = self.can_have_keystore_encryption() - self.storage.set_keystore_encryption(bool(new_pw) and encrypt_keystore) - self.storage.write() + self.db.set_keystore_encryption(bool(new_pw) and encrypt_keystore) + self.save_db() @abstractmethod 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() def save_keystore(self): - self.storage.put('keystore', self.keystore.dump()) + self.db.put('keystore', self.keystore.dump()) @abstractmethod def get_public_key(self, address: str) -> Optional[str]: @@ -1870,8 +1879,8 @@ class Imported_Wallet(Simple_Wallet): wallet_type = 'imported' txin_type = 'address' - def __init__(self, storage, *, config): - Abstract_Wallet.__init__(self, storage, config=config) + def __init__(self, db, storage, *, config): + Abstract_Wallet.__init__(self, db, storage, config=config) def is_watching_only(self): return self.keystore is None @@ -1880,10 +1889,10 @@ class Imported_Wallet(Simple_Wallet): return bool(self.keystore) 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): - self.storage.put('keystore', self.keystore.dump()) + self.db.put('keystore', self.keystore.dump()) def can_import_address(self): return self.is_watching_only() @@ -1931,7 +1940,7 @@ class Imported_Wallet(Simple_Wallet): self.db.add_imported_address(address, {}) self.add_address(address) if write_to_disk: - self.storage.write() + self.save_db() return good_addr, bad_addr def import_address(self, address: str) -> str: @@ -1977,7 +1986,7 @@ class Imported_Wallet(Simple_Wallet): else: self.keystore.delete_imported_key(pubkey) self.save_keystore() - self.storage.write() + self.save_db() def is_mine(self, address) -> bool: return self.db.has_imported_address(address) @@ -2009,7 +2018,7 @@ class Imported_Wallet(Simple_Wallet): self.add_address(addr) self.save_keystore() if write_to_disk: - self.storage.write() + self.save_db() return good_addr, bad_keys 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): - def __init__(self, storage, *, config): + def __init__(self, db, storage, *, config): self._ephemeral_addr_to_addr_index = {} # type: Dict[str, Sequence[int]] - Abstract_Wallet.__init__(self, storage, config=config) - self.gap_limit = storage.get('gap_limit', 20) + Abstract_Wallet.__init__(self, db, storage, config=config) + self.gap_limit = db.get('gap_limit', 20) # generate addresses now. note that without libsecp this might block # for a few seconds! 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''' if value >= self.min_acceptable_gap(): self.gap_limit = value - self.storage.put('gap_limit', self.gap_limit) - self.storage.write() + self.db.put('gap_limit', self.gap_limit) + self.save_db() return True else: return False @@ -2232,8 +2241,8 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet): """ Deterministic Wallet with a single pubkey per address """ - def __init__(self, storage, *, config): - Deterministic_Wallet.__init__(self, storage, config=config) + def __init__(self, db, storage, *, config): + Deterministic_Wallet.__init__(self, db, storage, config=config) def get_public_key(self, address): sequence = self.get_address_index(address) @@ -2241,7 +2250,7 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet): return pubkeys[0] def load_keystore(self): - self.keystore = load_keystore(self.storage, 'keystore') + self.keystore = load_keystore(self.db, 'keystore') try: xtype = bip32.xpub_type(self.keystore.xpub) except: @@ -2270,10 +2279,10 @@ class Standard_Wallet(Simple_Deterministic_Wallet): class Multisig_Wallet(Deterministic_Wallet): # generic m of n - def __init__(self, storage, *, config): - self.wallet_type = storage.get('wallet_type') + def __init__(self, db, storage, *, config): + self.wallet_type = db.get('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): 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 = {} for i in range(self.n): 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/'] xtype = bip32.xpub_type(self.keystore.xpub) self.txin_type = 'p2sh' if xtype == 'standard' else xtype def save_keystore(self): for name, k in self.keystores.items(): - self.storage.put(name, k.dump()) + self.db.put(name, k.dump()) def get_keystore(self): return self.keystores.get('x1/') @@ -2336,13 +2345,14 @@ class Multisig_Wallet(Deterministic_Wallet): for name, keystore in self.keystores.items(): if keystore.may_have_password(): keystore.update_password(old_pw, new_pw) - self.storage.put(name, keystore.dump()) + self.db.put(name, keystore.dump()) def check_password(self, password): for name, keystore in self.keystores.items(): if keystore.may_have_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): # 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 type when passed a WalletStorage instance.""" - def __new__(self, storage: WalletStorage, *, config: SimpleConfig): - wallet_type = storage.get('wallet_type') + def __new__(self, db, storage: WalletStorage, *, config: SimpleConfig): + wallet_type = db.get('wallet_type') WalletClass = Wallet.wallet_class(wallet_type) - wallet = WalletClass(storage, config=config) + wallet = WalletClass(db, storage, config=config) return wallet @staticmethod @@ -2406,19 +2416,20 @@ def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=N storage = WalletStorage(path) if storage.file_exists(): raise Exception("Remove the existing wallet first!") + db = WalletDB('', manual_upgrades=False) seed = Mnemonic('en').make_seed(seed_type) k = keystore.from_seed(seed, passphrase) - storage.put('keystore', k.dump()) - storage.put('wallet_type', 'standard') + db.put('keystore', k.dump()) + db.put('wallet_type', 'standard') if gap_limit is not None: - storage.put('gap_limit', gap_limit) - wallet = Wallet(storage, config=config) + db.put('gap_limit', gap_limit) + wallet = Wallet(db, storage, config=config) wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) wallet.synchronize() 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} @@ -2431,10 +2442,10 @@ def restore_wallet_from_text(text, *, path, config: SimpleConfig, storage = WalletStorage(path) if storage.file_exists(): raise Exception("Remove the existing wallet first!") - + db = WalletDB('', manual_upgrades=False) text = text.strip() if keystore.is_address_list(text): - wallet = Imported_Wallet(storage, config=config) + wallet = Imported_Wallet(db, storage, config=config) addresses = text.split() good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False) # 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") elif keystore.is_private_key_list(text, allow_spaces_inside_key=False): k = keystore.Imported_KeyStore({}) - storage.put('keystore', k.dump()) - wallet = Imported_Wallet(storage, config=config) + db.put('keystore', k.dump()) + wallet = Imported_Wallet(db, storage, config=config) 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) # 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) else: raise Exception("Seed or key not recognized") - storage.put('keystore', k.dump()) - storage.put('wallet_type', 'standard') + db.put('keystore', k.dump()) + db.put('wallet_type', 'standard') if gap_limit is not None: - storage.put('gap_limit', gap_limit) - wallet = Wallet(storage, config=config) + db.put('gap_limit', gap_limit) + wallet = Wallet(db, storage, config=config) 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) @@ -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. " "Start a daemon and use load_wallet to sync its history.") - wallet.storage.write() + wallet.save_db() return {'wallet': wallet, 'msg': msg} diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py @@ -39,6 +39,7 @@ from .logging import Logger from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, RevocationStore from .lnutil import ChannelConstraints, Outpoint, ShachainElement 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 @@ -62,6 +63,7 @@ class WalletDB(JsonDB): self._called_after_upgrade_tasks = False if raw: # loading existing db self.load_data(raw) + self.load_plugins() else: # creating new db self.put('seed_version', FINAL_SEED_VERSION) self._after_upgrade_tasks() @@ -99,7 +101,7 @@ class WalletDB(JsonDB): d = self.get('accounts', {}) return len(d) > 1 - def split_accounts(self): + def get_split_accounts(self): result = [] # backward compatibility with old wallets 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"]: v = binascii.unhexlify(v) if v is not None else None 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) diff --git a/run_electrum b/run_electrum @@ -88,6 +88,7 @@ from electrum.logging import get_logger, configure_logging from electrum import util from electrum import constants from electrum import SimpleConfig +from electrum.wallet_db import WalletDB from electrum.wallet import Wallet 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 @@ -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("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 if ( (cmd.requires_wallet and storage.is_encrypted() and server is False)\ 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(): # this case is handled later in the control flow password = None @@ -218,7 +226,8 @@ async def run_offline_command(config, config_options, plugins): password = get_password_for_hw_device_encrypted_storage(plugins) config_options['password'] = 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 else: wallet = None @@ -245,7 +254,7 @@ async def run_offline_command(config, config_options, plugins): result = await func(*args, **kwargs) # save wallet if wallet: - wallet.storage.write() + wallet.save_db() return result