Skip to content

Commit a729ae7

Browse files
moneymanolisk9ert
andauthored
Bugfix: Incomplete wallet deletion (#1950)
Also: * threading for testing via specter config * helpful output for flaky tests Co-authored-by: k9ert <[email protected]>
1 parent aba2afe commit a729ae7

File tree

19 files changed

+455
-114
lines changed

19 files changed

+455
-114
lines changed

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ markers =
66
slow: mark test as slow.
77
elm: mark test as elementsd dependent
88
bottleneck: mark a test as so ressource intensive that it can create a bottleneck where the test just fails due to a lack of ressources
9+
threading: test needs threading to work
10+
911
# If you need live logging to debug, uncomment the next line
1012
# log_cli = True
1113
# Then set the desired logging level on the command line, for example:

src/cryptoadvance/specter/managers/config_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ def __init__(self, data_folder, config={}):
6767
"hide_sensitive_info": False,
6868
"autohide_sensitive_info_timeout_minutes": None,
6969
"autologout_timeout_hours": 4,
70+
"testing": {
71+
"allow_threading_for_testing": False,
72+
},
7073
# TODO: remove
7174
"bitcoind": False,
7275
}

src/cryptoadvance/specter/managers/wallet_manager.py

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import logging
22
import os
3+
import sys
34
import pathlib
4-
import shutil
55
import threading
66
import traceback
77
from typing import Dict
88

99
from flask_babel import lazy_gettext as _
1010
from cryptoadvance.specter.rpc import BitcoinRPC
11-
1211
from cryptoadvance.specter.key import Key
1312

1413
from ..helpers import add_dicts, alias, is_liquid, load_jsons
@@ -37,7 +36,7 @@ def __init__(
3736
chain,
3837
device_manager,
3938
path="specter",
40-
allow_threading=True,
39+
allow_threading_for_testing=False,
4140
):
4241
self.data_folder = data_folder
4342
self.chain = chain
@@ -52,7 +51,7 @@ def __init__(
5251
self.wallets = {}
5352
# A way to communicate failed wallets to the outside
5453
self.bitcoin_core_version_raw = bitcoin_core_version_raw
55-
self.allow_threading = allow_threading
54+
self.allow_threading_for_testing = allow_threading_for_testing
5655
# define different wallet classes for liquid and bitcoin
5756
self.WalletClass = LWallet if is_liquid(chain) else Wallet
5857
self.update(data_folder, rpc, chain)
@@ -108,15 +107,29 @@ def update(
108107
and self.rpc is not None
109108
and self.chain is not None
110109
):
111-
if self.allow_threading and use_threading:
112-
t = threading.Thread(
113-
target=self._update,
114-
args=(wallets_update_list,),
115-
)
116-
117-
t.start()
110+
if "pytest" in sys.modules:
111+
if self.allow_threading_for_testing:
112+
logger.info("Using threads in updating the wallet manager.")
113+
t = threading.Thread(
114+
target=self._update,
115+
args=(wallets_update_list,),
116+
)
117+
t.start()
118+
else:
119+
logger.info("Not using threads in updating the wallet manager.")
120+
self._update(wallets_update_list)
118121
else:
119-
self._update(wallets_update_list)
122+
if use_threading:
123+
logger.info("Using threads in updating the wallet manager.")
124+
t = threading.Thread(
125+
target=self._update,
126+
args=(wallets_update_list,),
127+
)
128+
129+
t.start()
130+
else:
131+
logger.info("Not using threads in updating the wallet manager.")
132+
self._update(wallets_update_list)
120133
else:
121134
self.is_loading = False
122135
logger.warning(
@@ -134,7 +147,7 @@ def _update(self, wallets_update_list: Dict):
134147
* and, on the Specter side, the wallet objects of those unloaded wallets are reinitialised
135148
"""
136149
logger.info(
137-
f"Started Updating Wallets with {len(wallets_update_list.values())} wallets"
150+
f"Started updating wallets with {len(wallets_update_list.values())} wallets"
138151
)
139152
# list of wallets in the dict
140153
existing_names = list(self.wallets.keys())
@@ -147,7 +160,7 @@ def _update(self, wallets_update_list: Dict):
147160

148161
wallet_alias = wallets_update_list[wallet]["alias"]
149162
wallet_name = wallets_update_list[wallet]["name"]
150-
logger.info(f" Updating wallet {wallet_name}")
163+
logger.info(f"Updating wallet {wallet_name}")
151164
# wallet from json not yet loaded in Bitcoin Core?!
152165
if os.path.join(self.rpc_path, wallet_alias) not in loaded_wallets:
153166
try:
@@ -348,27 +361,43 @@ def create_wallet(self, name, sigs_required, key_type, keys, devices, **kwargs):
348361
else:
349362
raise ("Failed to create new wallet")
350363

351-
def delete_wallet(
352-
self, wallet, bitcoin_datadir=get_default_datadir(), chain="main"
353-
):
354-
logger.info("Deleting {}".format(wallet.alias))
355-
wallet_rpc_path = os.path.join(self.rpc_path, wallet.alias)
356-
self.rpc.unloadwallet(wallet_rpc_path)
357-
# Try deleting wallet folder
358-
if bitcoin_datadir:
359-
if chain != "main":
360-
bitcoin_datadir = os.path.join(bitcoin_datadir, chain)
361-
candidates = [
362-
os.path.join(bitcoin_datadir, wallet_rpc_path),
363-
os.path.join(bitcoin_datadir, "wallets", wallet_rpc_path),
364-
]
365-
for path in candidates:
366-
shutil.rmtree(path, ignore_errors=True)
367-
368-
# Delete files
369-
wallet.delete_files()
370-
del self.wallets[wallet.name]
371-
self.update()
364+
def delete_wallet(self, wallet, node=None) -> tuple:
365+
"""Returns a tuple with two Booleans, indicating whether the wallet was deleted on the Specter side and/or on the node side."""
366+
logger.info(f"Deleting {wallet.alias}")
367+
specter_wallet_deleted = False
368+
node_wallet_file_deleted = False
369+
# Make first sure that we can unload the wallet in Bitcoin Core
370+
try:
371+
wallet_rpc_path = os.path.join(
372+
self.rpc_path, wallet.alias
373+
) # e.g. specter/jade_wallet
374+
logger.debug(f"The wallet_rpc_path is: {wallet_rpc_path}")
375+
self.rpc.unloadwallet(wallet_rpc_path)
376+
# Delete the wallet.json and backups
377+
try:
378+
wallet.delete_files()
379+
# Remove the wallet instance
380+
del self.wallets[wallet.name]
381+
self.update()
382+
specter_wallet_deleted = True
383+
except KeyError:
384+
raise SpecterError(
385+
f"The wallet {wallet.name} has already been deleted."
386+
)
387+
except SpecterInternalException as sie:
388+
logger.exception(
389+
f"Could not delete the wallet {wallet.name} in Specter due to {sie}"
390+
)
391+
# Also delete the wallet file on the node if possible
392+
if node:
393+
if node.delete_wallet_file(wallet):
394+
node_wallet_file_deleted = True
395+
except RpcError:
396+
raise SpecterError(
397+
"Unable to unload the wallet on the node. Aborting the deletion of the wallet ..."
398+
)
399+
deleted = (specter_wallet_deleted, node_wallet_file_deleted)
400+
return deleted
372401

373402
def rename_wallet(self, wallet, name):
374403
logger.info("Renaming {}".format(wallet.alias))
@@ -469,7 +498,7 @@ def delete(self, specter):
469498
"""Deletes all the wallets"""
470499
for w in list(self.wallets.keys()):
471500
wallet = self.wallets[w]
472-
self.delete_wallet(wallet, specter.bitcoin_datadir, specter.chain)
501+
self.delete_wallet(wallet)
473502
delete_folder(self.data_folder)
474503

475504
@classmethod

src/cryptoadvance/specter/node.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import os
44
from os import path
5+
import shutil
56

67
from embit.liquid.networks import get_network
78
from flask import render_template
@@ -17,7 +18,7 @@
1718
autodetect_rpc_confs,
1819
get_default_datadir,
1920
)
20-
from .specter_error import BrokenCoreConnectionException
21+
from .specter_error import SpecterError, BrokenCoreConnectionException
2122

2223
logger = logging.getLogger(__name__)
2324

@@ -534,6 +535,44 @@ def check_blockheight(self):
534535
def is_liquid(self):
535536
return is_liquid(self.chain)
536537

538+
def delete_wallet_file(self, wallet) -> bool:
539+
"""Deleting the wallet file located on the node. This only works if the node is on the same machine as Specter.
540+
Returns True if the wallet file could be deleted, otherwise returns False."""
541+
datadir = ""
542+
if self.datadir == "":
543+
# In case someone did not toggle the auto-detect but still used the default location.
544+
# When you set up a new node and deactivate the auto-detect, the datadir is set to an empty string.
545+
logger.debug(
546+
f"The node datadir before get_default_datadir is: {self.datadir}"
547+
)
548+
datadir = get_default_datadir(self.node_type)
549+
logger.debug(f"The node datadir after get_default_datadir is: {datadir}")
550+
else:
551+
datadir = self.datadir
552+
wallet_file_removed = False
553+
path = ""
554+
# Check whether wallet was really unloaded
555+
wallet_rpc_path = os.path.join(wallet.manager.rpc_path, wallet.alias)
556+
# If we can unload the wallet via RPC it had not been unloaded properly before by the wallet manager
557+
try:
558+
self.rpc.unloadwallet(wallet_rpc_path)
559+
raise SpecterError(
560+
"Trying to delete the wallet file on the node but the wallet had not been unloaded properly."
561+
)
562+
except RpcError:
563+
pass
564+
if self.chain != "main":
565+
path = os.path.join(datadir, f"{self.chain}/wallets", wallet_rpc_path)
566+
else:
567+
path = os.path.join(datadir, wallet_rpc_path)
568+
try:
569+
shutil.rmtree(path, ignore_errors=False)
570+
logger.debug(f"Removing wallet file at: {path}")
571+
wallet_file_removed = True
572+
except FileNotFoundError:
573+
logger.debug(f"Could not find any wallet file at: {path}")
574+
return wallet_file_removed
575+
537576
@property
538577
def is_running(self):
539578
if self._network_info["version"] == 999999:
@@ -619,6 +658,10 @@ def node_type(self):
619658
return self._node_type
620659
return "BTC"
621660

661+
@property
662+
def default_datadir(self):
663+
return get_default_datadir(self.node_type)
664+
622665
@rpc.setter
623666
def rpc(self, value):
624667
if hasattr(self, "_rpc") and self._rpc != value:

src/cryptoadvance/specter/server_endpoints/wallets/wallets.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -850,11 +850,28 @@ def settings(wallet_alias):
850850
)
851851
wallet.getdata()
852852
elif action == "deletewallet":
853-
app.specter.wallet_manager.delete_wallet(
854-
wallet, app.specter.bitcoin_datadir, app.specter.chain
855-
)
856-
response = redirect(url_for("index"))
857-
return response
853+
deleted = app.specter.wallet_manager.delete_wallet(wallet, app.specter.node)
854+
# deleted is a tuple: (specter_wallet_deleted, core_wallet_file_deleted)
855+
if deleted == (True, True):
856+
flash(
857+
_("Wallet in Specter and wallet file on node deleted successfully.")
858+
)
859+
elif deleted == (True, False):
860+
flash(
861+
_(
862+
"Wallet in Specter deleted successfully but wallet file on node could not be removed automatically."
863+
)
864+
)
865+
elif deleted == (False, True):
866+
flash(
867+
_(
868+
"Deletion of wallet in Specter failed, but wallet on node was removed."
869+
),
870+
"error",
871+
)
872+
else:
873+
flash(_("Deletion of wallet failed."), "error")
874+
return redirect(url_for("index"))
858875
elif action == "rename":
859876
wallet_name = request.form["newtitle"]
860877
if not wallet_name:

src/cryptoadvance/specter/templates/node/node_settings.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
</div>
4242
<span id="datadir-container">
4343
<br>
44-
{{ _("Bitcoin Core data directory path:") }}<br><input type="text" id="datadir" name="datadir" type="text" value="{{ node.datadir }}" style="margin-bottom: 15px;">
44+
{{ _("Bitcoin Core data directory path:") }}<br><input type="text" id="datadir" name="datadir" type="text" value="{{ node.default_datadir }}" style="margin-bottom: 15px;">
4545
</span>
4646
<br>
4747
<div id="rpc_settings">

src/cryptoadvance/specter/user.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,9 @@ def check_wallet_manager(self):
299299
self.specter.chain,
300300
self.device_manager,
301301
path=wallets_rpcpath,
302+
allow_threading_for_testing=self.specter.config["testing"][
303+
"allow_threading_for_testing"
304+
],
302305
)
303306
self._wallet_manager = wallet_manager
304307
else:

0 commit comments

Comments
 (0)