Browse Source

Separate db from storage

- storage is content-agnostic
 - db and storage are passed to wallet contructor
hard-fail-on-bad-server-string
ThomasV 5 years ago
parent
commit
e1ce3aace7
  1. 29
      electrum/base_wizard.py
  2. 8
      electrum/commands.py
  3. 8
      electrum/contacts.py
  4. 13
      electrum/daemon.py
  5. 18
      electrum/gui/kivy/main_window.py
  6. 10
      electrum/gui/kivy/uix/dialogs/installwizard.py
  7. 10
      electrum/gui/qt/__init__.py
  8. 27
      electrum/gui/qt/installwizard.py
  9. 12
      electrum/gui/qt/main_window.py
  10. 4
      electrum/gui/qt/settings_dialog.py
  11. 4
      electrum/keystore.py
  12. 2
      electrum/lnpeer.py
  13. 26
      electrum/lnworker.py
  14. 4
      electrum/plugins/labels/labels.py
  15. 2
      electrum/plugins/trustedcoin/qt.py
  16. 32
      electrum/plugins/trustedcoin/trustedcoin.py
  17. 87
      electrum/storage.py
  18. 14
      electrum/tests/test_commands.py
  19. 14
      electrum/tests/test_lnpeer.py
  20. 66
      electrum/tests/test_storage_upgrade.py
  21. 12
      electrum/tests/test_wallet.py
  22. 208
      electrum/tests/test_wallet_vertical.py
  23. 173
      electrum/wallet.py
  24. 46
      electrum/wallet_db.py
  25. 15
      run_electrum

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

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

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

13
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

18
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

10
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']

10
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

27
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."""

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

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

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

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

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

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

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

32
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'

87
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

14
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',

14
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

66
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

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

208
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,

173
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}

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

15
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

Loading…
Cancel
Save