Bugfix: Incomplete wallet deletion (#1950)

Also:
* threading for testing via specter config
* helpful output for flaky tests

Co-authored-by: k9ert <kim@swanbitcoin.com>
This commit is contained in:
Manolis Mandrapilias 2022-11-14 21:26:32 +01:00 committed by GitHub
parent aba2afe9bd
commit a729ae7ae2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 456 additions and 115 deletions

View file

@ -6,6 +6,8 @@ markers =
slow: mark test as slow.
elm: mark test as elementsd dependent
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
threading: test needs threading to work
# If you need live logging to debug, uncomment the next line
# log_cli = True
# Then set the desired logging level on the command line, for example:

View file

@ -67,6 +67,9 @@ class ConfigManager(GenericDataManager):
"hide_sensitive_info": False,
"autohide_sensitive_info_timeout_minutes": None,
"autologout_timeout_hours": 4,
"testing": {
"allow_threading_for_testing": False,
},
# TODO: remove
"bitcoind": False,
}

View file

@ -1,14 +1,13 @@
import logging
import os
import sys
import pathlib
import shutil
import threading
import traceback
from typing import Dict
from flask_babel import lazy_gettext as _
from cryptoadvance.specter.rpc import BitcoinRPC
from cryptoadvance.specter.key import Key
from ..helpers import add_dicts, alias, is_liquid, load_jsons
@ -37,7 +36,7 @@ class WalletManager:
chain,
device_manager,
path="specter",
allow_threading=True,
allow_threading_for_testing=False,
):
self.data_folder = data_folder
self.chain = chain
@ -52,7 +51,7 @@ class WalletManager:
self.wallets = {}
# A way to communicate failed wallets to the outside
self.bitcoin_core_version_raw = bitcoin_core_version_raw
self.allow_threading = allow_threading
self.allow_threading_for_testing = allow_threading_for_testing
# define different wallet classes for liquid and bitcoin
self.WalletClass = LWallet if is_liquid(chain) else Wallet
self.update(data_folder, rpc, chain)
@ -108,15 +107,29 @@ class WalletManager:
and self.rpc is not None
and self.chain is not None
):
if self.allow_threading and use_threading:
t = threading.Thread(
target=self._update,
args=(wallets_update_list,),
)
t.start()
if "pytest" in sys.modules:
if self.allow_threading_for_testing:
logger.info("Using threads in updating the wallet manager.")
t = threading.Thread(
target=self._update,
args=(wallets_update_list,),
)
t.start()
else:
logger.info("Not using threads in updating the wallet manager.")
self._update(wallets_update_list)
else:
self._update(wallets_update_list)
if use_threading:
logger.info("Using threads in updating the wallet manager.")
t = threading.Thread(
target=self._update,
args=(wallets_update_list,),
)
t.start()
else:
logger.info("Not using threads in updating the wallet manager.")
self._update(wallets_update_list)
else:
self.is_loading = False
logger.warning(
@ -134,7 +147,7 @@ class WalletManager:
* and, on the Specter side, the wallet objects of those unloaded wallets are reinitialised
"""
logger.info(
f"Started Updating Wallets with {len(wallets_update_list.values())} wallets"
f"Started updating wallets with {len(wallets_update_list.values())} wallets"
)
# list of wallets in the dict
existing_names = list(self.wallets.keys())
@ -147,7 +160,7 @@ class WalletManager:
wallet_alias = wallets_update_list[wallet]["alias"]
wallet_name = wallets_update_list[wallet]["name"]
logger.info(f" Updating wallet {wallet_name}")
logger.info(f"Updating wallet {wallet_name}")
# wallet from json not yet loaded in Bitcoin Core?!
if os.path.join(self.rpc_path, wallet_alias) not in loaded_wallets:
try:
@ -348,27 +361,43 @@ class WalletManager:
else:
raise ("Failed to create new wallet")
def delete_wallet(
self, wallet, bitcoin_datadir=get_default_datadir(), chain="main"
):
logger.info("Deleting {}".format(wallet.alias))
wallet_rpc_path = os.path.join(self.rpc_path, wallet.alias)
self.rpc.unloadwallet(wallet_rpc_path)
# Try deleting wallet folder
if bitcoin_datadir:
if chain != "main":
bitcoin_datadir = os.path.join(bitcoin_datadir, chain)
candidates = [
os.path.join(bitcoin_datadir, wallet_rpc_path),
os.path.join(bitcoin_datadir, "wallets", wallet_rpc_path),
]
for path in candidates:
shutil.rmtree(path, ignore_errors=True)
# Delete files
wallet.delete_files()
del self.wallets[wallet.name]
self.update()
def delete_wallet(self, wallet, node=None) -> tuple:
"""Returns a tuple with two Booleans, indicating whether the wallet was deleted on the Specter side and/or on the node side."""
logger.info(f"Deleting {wallet.alias}")
specter_wallet_deleted = False
node_wallet_file_deleted = False
# Make first sure that we can unload the wallet in Bitcoin Core
try:
wallet_rpc_path = os.path.join(
self.rpc_path, wallet.alias
) # e.g. specter/jade_wallet
logger.debug(f"The wallet_rpc_path is: {wallet_rpc_path}")
self.rpc.unloadwallet(wallet_rpc_path)
# Delete the wallet.json and backups
try:
wallet.delete_files()
# Remove the wallet instance
del self.wallets[wallet.name]
self.update()
specter_wallet_deleted = True
except KeyError:
raise SpecterError(
f"The wallet {wallet.name} has already been deleted."
)
except SpecterInternalException as sie:
logger.exception(
f"Could not delete the wallet {wallet.name} in Specter due to {sie}"
)
# Also delete the wallet file on the node if possible
if node:
if node.delete_wallet_file(wallet):
node_wallet_file_deleted = True
except RpcError:
raise SpecterError(
"Unable to unload the wallet on the node. Aborting the deletion of the wallet ..."
)
deleted = (specter_wallet_deleted, node_wallet_file_deleted)
return deleted
def rename_wallet(self, wallet, name):
logger.info("Renaming {}".format(wallet.alias))
@ -469,7 +498,7 @@ class WalletManager:
"""Deletes all the wallets"""
for w in list(self.wallets.keys()):
wallet = self.wallets[w]
self.delete_wallet(wallet, specter.bitcoin_datadir, specter.chain)
self.delete_wallet(wallet)
delete_folder(self.data_folder)
@classmethod

View file

@ -2,6 +2,7 @@ import json
import logging
import os
from os import path
import shutil
from embit.liquid.networks import get_network
from flask import render_template
@ -17,7 +18,7 @@ from .rpc import (
autodetect_rpc_confs,
get_default_datadir,
)
from .specter_error import BrokenCoreConnectionException
from .specter_error import SpecterError, BrokenCoreConnectionException
logger = logging.getLogger(__name__)
@ -534,6 +535,44 @@ class Node(AbstractNode):
def is_liquid(self):
return is_liquid(self.chain)
def delete_wallet_file(self, wallet) -> bool:
"""Deleting the wallet file located on the node. This only works if the node is on the same machine as Specter.
Returns True if the wallet file could be deleted, otherwise returns False."""
datadir = ""
if self.datadir == "":
# In case someone did not toggle the auto-detect but still used the default location.
# When you set up a new node and deactivate the auto-detect, the datadir is set to an empty string.
logger.debug(
f"The node datadir before get_default_datadir is: {self.datadir}"
)
datadir = get_default_datadir(self.node_type)
logger.debug(f"The node datadir after get_default_datadir is: {datadir}")
else:
datadir = self.datadir
wallet_file_removed = False
path = ""
# Check whether wallet was really unloaded
wallet_rpc_path = os.path.join(wallet.manager.rpc_path, wallet.alias)
# If we can unload the wallet via RPC it had not been unloaded properly before by the wallet manager
try:
self.rpc.unloadwallet(wallet_rpc_path)
raise SpecterError(
"Trying to delete the wallet file on the node but the wallet had not been unloaded properly."
)
except RpcError:
pass
if self.chain != "main":
path = os.path.join(datadir, f"{self.chain}/wallets", wallet_rpc_path)
else:
path = os.path.join(datadir, wallet_rpc_path)
try:
shutil.rmtree(path, ignore_errors=False)
logger.debug(f"Removing wallet file at: {path}")
wallet_file_removed = True
except FileNotFoundError:
logger.debug(f"Could not find any wallet file at: {path}")
return wallet_file_removed
@property
def is_running(self):
if self._network_info["version"] == 999999:
@ -619,6 +658,10 @@ class Node(AbstractNode):
return self._node_type
return "BTC"
@property
def default_datadir(self):
return get_default_datadir(self.node_type)
@rpc.setter
def rpc(self, value):
if hasattr(self, "_rpc") and self._rpc != value:

View file

@ -850,11 +850,28 @@ def settings(wallet_alias):
)
wallet.getdata()
elif action == "deletewallet":
app.specter.wallet_manager.delete_wallet(
wallet, app.specter.bitcoin_datadir, app.specter.chain
)
response = redirect(url_for("index"))
return response
deleted = app.specter.wallet_manager.delete_wallet(wallet, app.specter.node)
# deleted is a tuple: (specter_wallet_deleted, core_wallet_file_deleted)
if deleted == (True, True):
flash(
_("Wallet in Specter and wallet file on node deleted successfully.")
)
elif deleted == (True, False):
flash(
_(
"Wallet in Specter deleted successfully but wallet file on node could not be removed automatically."
)
)
elif deleted == (False, True):
flash(
_(
"Deletion of wallet in Specter failed, but wallet on node was removed."
),
"error",
)
else:
flash(_("Deletion of wallet failed."), "error")
return redirect(url_for("index"))
elif action == "rename":
wallet_name = request.form["newtitle"]
if not wallet_name:

View file

@ -41,7 +41,7 @@
</div>
<span id="datadir-container">
<br>
{{ _("Bitcoin Core data directory path:") }}<br><input type="text" id="datadir" name="datadir" type="text" value="{{ node.datadir }}" style="margin-bottom: 15px;">
{{ _("Bitcoin Core data directory path:") }}<br><input type="text" id="datadir" name="datadir" type="text" value="{{ node.default_datadir }}" style="margin-bottom: 15px;">
</span>
<br>
<div id="rpc_settings">

View file

@ -299,6 +299,9 @@ class User(UserMixin):
self.specter.chain,
self.device_manager,
path=wallets_rpcpath,
allow_threading_for_testing=self.specter.config["testing"][
"allow_threading_for_testing"
],
)
self._wallet_manager = wallet_manager
else:

View file

@ -10,7 +10,9 @@ import traceback
import pytest
from cryptoadvance.specter.config import TestConfig
from cryptoadvance.specter.node import Node
from cryptoadvance.specter.managers.device_manager import DeviceManager
from cryptoadvance.specter.managers.node_manager import NodeManager
from cryptoadvance.specter.managers.user_manager import UserManager
from cryptoadvance.specter.process_controller.bitcoind_controller import (
BitcoindPlainController,
@ -20,7 +22,11 @@ from cryptoadvance.specter.process_controller.elementsd_controller import (
)
from cryptoadvance.specter.server import SpecterFlask, create_app, init_app
from cryptoadvance.specter.specter import Specter
from cryptoadvance.specter.specter_error import SpecterError
from cryptoadvance.specter.specter_error import (
BrokenCoreConnectionException,
SpecterError,
handle_exception,
)
from cryptoadvance.specter.user import User, hash_password
from cryptoadvance.specter.util.common import str2bool
from cryptoadvance.specter.util.shell import which
@ -29,6 +35,7 @@ from cryptoadvance.specter.util.wallet_importer import WalletImporter
logger = logging.getLogger(__name__)
pytest_plugins = [
# "conftest_visibility",
"fix_ghost_machine",
"fix_keys_and_seeds",
"fix_devices_and_wallets",
@ -186,6 +193,7 @@ def bitcoin_regtest(docker, request):
try:
assert bitcoind_regtest.get_rpc().test_connection()
assert not bitcoind_regtest.datadir is None
assert bitcoind_regtest.datadir is not ""
yield bitcoind_regtest
finally:
bitcoind_regtest.stop_bitcoind()
@ -205,6 +213,48 @@ def bitcoin_regtest2(docker, request):
bitcoind_regtest.stop_bitcoind()
@pytest.fixture
def node(empty_data_folder, bitcoin_regtest):
nodes_folder = empty_data_folder + "/nodes"
if not os.path.isdir(nodes_folder):
os.makedirs(nodes_folder)
node = Node.from_json(
{
"autodetect": False,
"datadir": bitcoin_regtest.datadir,
"user": bitcoin_regtest.rpcconn.rpcuser,
"password": bitcoin_regtest.rpcconn.rpcpassword,
"port": bitcoin_regtest.rpcconn.rpcport,
"host": bitcoin_regtest.rpcconn.ipaddress,
"protocol": "http",
},
manager=NodeManager(data_folder=nodes_folder),
default_fullpath=os.path.join(nodes_folder, "standard_node.json"),
)
return node
@pytest.fixture
def node_with_empty_datadir(empty_data_folder, bitcoin_regtest):
nodes_folder = empty_data_folder + "/nodes"
if not os.path.isdir(nodes_folder):
os.makedirs(nodes_folder)
node = Node.from_json(
{
"autodetect": False,
"datadir": "",
"user": bitcoin_regtest.rpcconn.rpcuser,
"password": bitcoin_regtest.rpcconn.rpcpassword,
"port": bitcoin_regtest.rpcconn.rpcport,
"host": bitcoin_regtest.rpcconn.ipaddress,
"protocol": "http",
},
manager=NodeManager(data_folder=nodes_folder),
default_fullpath=os.path.join(nodes_folder, "node_with_empty_datadir.json"),
)
return node
@pytest.fixture(scope="session")
def elements_elreg(request):
elements_elreg = instantiate_elementsd_controller(request, extra_args=None)
@ -218,9 +268,7 @@ def elements_elreg(request):
@pytest.fixture
def empty_data_folder():
# Make sure that this folder never ever gets a reasonable non-testing use-case
with tempfile.TemporaryDirectory(
prefix="specter_home_tmp_", ignore_cleanup_errors=True
) as data_folder:
with tempfile.TemporaryDirectory(prefix="specter_home_tmp_") as data_folder:
yield data_folder
@ -458,12 +506,12 @@ def device_manager(devices_filled_data_folder):
@pytest.fixture
def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder, node):
assert bitcoin_regtest.get_rpc().test_connection()
config = {
"rpc": {
"autodetect": False,
"datadir": "",
"datadir": bitcoin_regtest.datadir,
"user": bitcoin_regtest.rpcconn.rpcuser,
"password": bitcoin_regtest.rpcconn.rpcpassword,
"port": bitcoin_regtest.rpcconn.rpcport,
@ -473,6 +521,61 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
"auth": {
"method": "rpcpasswordaspin",
},
"testing": {
"allow_threading_for_testing": False,
},
}
specter = Specter(data_folder=devices_filled_data_folder, config=config)
assert specter.chain == "regtest"
# Create a User
someuser = specter.user_manager.add_user(
User.from_json(
user_dict={
"id": "someuser",
"username": "someuser",
"password": hash_password("somepassword"),
"config": {},
"is_admin": False,
"services": None,
},
specter=specter,
)
)
specter.user_manager.save()
specter.check()
assert not specter.wallet_manager.working_folder is None
try:
yield specter
finally:
# End all threads
# Deleting all Wallets (this will also purge them on core)
for user in specter.user_manager.users:
for wallet in list(user.wallet_manager.wallets.values()):
user.wallet_manager.delete_wallet(wallet, node)
@pytest.fixture
def specter_regtest_configured_with_threading(
bitcoin_regtest, devices_filled_data_folder, node
):
assert bitcoin_regtest.get_rpc().test_connection()
config = {
"rpc": {
"autodetect": False,
"datadir": bitcoin_regtest.datadir,
"user": bitcoin_regtest.rpcconn.rpcuser,
"password": bitcoin_regtest.rpcconn.rpcpassword,
"port": bitcoin_regtest.rpcconn.rpcport,
"host": bitcoin_regtest.rpcconn.ipaddress,
"protocol": "http",
},
"auth": {
"method": "rpcpasswordaspin",
},
"testing": {
"allow_threading_for_testing": True,
},
}
specter = Specter(data_folder=devices_filled_data_folder, config=config)
assert specter.chain == "regtest"
@ -500,9 +603,7 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
# Deleting all Wallets (this will also purge them on core)
for user in specter.user_manager.users:
for wallet in list(user.wallet_manager.wallets.values()):
user.wallet_manager.delete_wallet(
wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest"
)
user.wallet_manager.delete_wallet(wallet, node)
def specter_app_with_config(config={}, specter=None):

View file

@ -0,0 +1,83 @@
""" A toolkit for hunting flaky tests. For now, this way of running pytest gave more or less consistent
bad results:
clear && pytest tests/test_managers_wallet.py tests/test_cli_server.py tests/test_managers_device.py \
tests/test_managers_wallet.py tests/test_node.py tests/test_node_controller.py tests/test_rest.py
"""
import pytest
import traceback
import threading
import sys
from cryptoadvance.specter.specter_error import (
BrokenCoreConnectionException,
SpecterError,
)
from cryptoadvance.specter.process_controller.bitcoind_controller import (
BitcoindPlainController,
)
from cryptoadvance.specter.rpc import RpcError
from cryptoadvance.specter.specter_error import SpecterError
def should_intercept(call):
"""Should return a boolean whether the visibility output should be done.
This needs to be more and more restricted overtime as we hopefully
have less and less flaky tests in the future and the normal output is enough.
"""
return not (
isinstance(call.excinfo.value, RpcError)
or isinstance(call.excinfo.value, SpecterError)
)
@pytest.hookimpl(hookwrapper=True)
def pytest_exception_interact(node, call, report):
"""Making more clever investigations in case of Exceptions"""
if should_intercept(call):
print()
print("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
print(
f"intercepting: {call.excinfo.value.__class__.__name__} while {report.when}"
)
print_exception(node, call, report)
print_threaddump(node, call, report)
print_debug_logs(node, call, report)
print("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
yield
def print_exception(node, call, report):
"""prints a stacktrace of the intercepted Exception"""
print("XXXXXXXXXXXXXXXXXX_EXCEPTION_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
traceback.print_exception(call.excinfo.value)
print("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
def print_threaddump(node, call, report):
"""prints a threaddump, a list of stacktraces from all threads"""
print("XXXXXXXXXXXXXXXXXX_THREADDUMP_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
for th in threading.enumerate():
print(th)
traceback.print_stack(sys._current_frames()[th.ident])
print()
print("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
def print_debug_logs(node, call, report):
"""prints the debug-logs of all the Regtest instances"""
print(node)
if isinstance(call.excinfo.value, BrokenCoreConnectionException):
print("XXXXXXXXXXXXXXXXXX_DEBUG.LOG_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
regtests = []
for fixture_name in node.fixturenames:
regtest = node.funcargs.get(fixture_name)
if isinstance(regtest, BitcoindPlainController):
regtests.append(regtest)
for regtest in regtests:
print("---------------------------------------------------------")
print(
f"{regtest} in DATADIR {regtest.datadir} on PORT {regtest.rpcconn.rpcport}"
)
print(regtest.get_debug_log())
print("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")

View file

@ -4,6 +4,7 @@ import time
from cryptoadvance.specter.util.mnemonic import generate_mnemonic
from cryptoadvance.specter.process_controller.node_controller import NodeController
from cryptoadvance.specter.managers.wallet_manager import WalletManager
from cryptoadvance.specter.specter import Specter
from cryptoadvance.specter.wallet import Wallet, Device
@ -180,3 +181,19 @@ def funded_taproot_wallet(
funded_taproot_wallet.getnewaddress()
) # default value are 20 BTC
return funded_taproot_wallet
@pytest.fixture
def wallet(devices_filled_data_folder, device_manager, node):
"""An ordinary wallet without private keys"""
wm = WalletManager(
200100,
devices_filled_data_folder,
node._get_rpc(),
"regtest",
device_manager,
)
device = device_manager.get_by_alias("trezor")
wm.create_wallet("test_wallet", 1, "wpkh", [device.keys[5]], [device])
wallet = wm.wallets["test_wallet"]
return wallet

View file

@ -54,6 +54,4 @@ def specter_testnet_configured(bitcoin_regtest, devices_filled_data_folder):
# Deleting all Wallets (this will also purge them on core)
for user in specter.user_manager.users:
for wallet in list(user.wallet_manager.wallets.values()):
user.wallet_manager.delete_wallet(
wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest"
)
user.wallet_manager.delete_wallet(wallet)

View file

@ -111,7 +111,6 @@ def test_device_wallets(
bitcoin_regtest.get_rpc(),
"regtest",
device_manager,
allow_threading=False,
)
device = device_manager.get_by_alias("trezor")
assert len(device.wallets(wm)) == 0

View file

@ -18,6 +18,8 @@ from cryptoadvance.specter.util.descriptor import AddChecksum, Descriptor
from cryptoadvance.specter.util.wallet_importer import WalletImporter
from conftest import instantiate_bitcoind_controller
logger = logging.getLogger(__name__)
@pytest.mark.slow
def test_WalletManager(
@ -26,6 +28,8 @@ def test_WalletManager(
devices_filled_data_folder,
device_manager,
bitcoin_regtest,
node,
node_with_empty_datadir,
):
wm = WalletManager(
200100,
@ -33,8 +37,8 @@ def test_WalletManager(
bitcoin_regtest.get_rpc(),
"regtest",
device_manager,
allow_threading=False,
)
assert wm.rpc_path == "specter"
# A wallet-creation needs a device
device = device_manager.get_by_alias("trezor")
assert device != None
@ -80,19 +84,40 @@ def test_WalletManager(
assert multisig_wallet.name == "new_name_test_wallet"
assert wm.wallets_names == ["a_test_wallet", "new_name_test_wallet"]
# you can also delete a wallet by passing it to the wallet manager's `delete_wallet` method
# it will delete the json and attempt to remove it from Bitcoin Core
# You can also delete a wallet by passing it to the wallet manager's `delete_wallet` method
# It will delete the json and attempt to remove it from Bitcoin Core
wallet_fullpath = multisig_wallet.fullpath
assert os.path.exists(wallet_fullpath)
wm.delete_wallet(multisig_wallet)
assert not os.path.exists(wallet_fullpath)
# This deletion should also remove the wallet file on the node
assert wm.delete_wallet(multisig_wallet, node) == (True, True)
assert len(wm.wallets) == 1
wm.update()
assert not os.path.exists(wallet_fullpath)
# Let's artificially unload the wallet in Core before trying to delete it via the Wallet Manager, should raise a SpecterError
wallet_rpc_path = os.path.join(wm.rpc_path, wallet.alias)
wm.rpc.unloadwallet(wallet_rpc_path)
with pytest.raises(
SpecterError,
match="Unable to unload the wallet on the node. Aborting the deletion of the wallet ...",
):
wm.delete_wallet(wallet, node)
# Check that the wallet wasn't deleted in Specter because of the RpcError
assert wm.wallets_names == ["a_test_wallet"]
# The following deletion should not remove the wallet file on the node
assert node_with_empty_datadir.datadir == ""
wm.rpc.loadwallet(wallet_rpc_path) # we need to load the wallet again
assert wm.delete_wallet(wallet, node_with_empty_datadir) == (True, False)
assert len(wm.wallets) == 0
# The wallet in Specter was already deleted, so trying to delete it again should raise a SpecterError
wm.rpc.loadwallet(wallet_rpc_path) # we need to load the wallet again
with pytest.raises(
SpecterError, match="The wallet a_test_wallet has already been deleted."
):
assert wm.delete_wallet(wallet)
@pytest.mark.slow
@pytest.mark.bottleneck
@pytest.mark.threading
def test_WalletManager_2_nodes(
docker,
request,
@ -109,35 +134,34 @@ def test_WalletManager_2_nodes(
bitcoin_regtest.get_rpc(),
"regtest",
device_manager,
allow_threading=True,
allow_threading_for_testing=True,
)
# A wallet-creation needs a device
# Wallet creation needs a device
device = device_manager.get_by_alias("trezor")
assert device != None
# Lets's create a wallet with the WalletManager
wm.create_wallet("a_test_wallet", 1, "wpkh", [device.keys[5]], [device])
assert wm.wallets_names == ["a_test_wallet"]
# A WalletManager implicitely uses the chain as a kind of index
wm.update(chain="regtest2", rpc=bitcoin_regtest2.get_rpc(), use_threading=False)
assert wm.wallets_names == []
wm.create_wallet("a_regtest2_test_wallet", 1, "wpkh", [device.keys[5]], [device])
assert wm.wallets_names == ["a_regtest2_test_wallet"]
wm.create_wallet(
"a_second_regtest2_test_wallet", 1, "wpkh", [device.keys[5]], [device]
first_wallet = wm.create_wallet(
"a_test_wallet", 1, "wpkh", [device.keys[5]], [device]
)
assert wm.wallets_names == [
"a_regtest2_test_wallet",
"a_second_regtest2_test_wallet",
]
# you can switch bettween chains witht he update-method
wm.update(chain="regtest", rpc=bitcoin_regtest.get_rpc())
assert wm.wallets_names == ["a_test_wallet"]
# Should also use with threading
wm.update(chain="regtest2", rpc=bitcoin_regtest2.get_rpc(), use_threading=True)
assert wm.wallets_names == ["a_test_wallet"]
assert wm.chain == "regtest"
assert wm.working_folder.endswith("regtest")
assert wm.rpc.port == 18543
# Change the rpc - this only works with a different chain!
wm.update(rpc=bitcoin_regtest2.get_rpc(), chain="regtest2")
# A WalletManager uses the chain as an index
assert list(wm.rpcs.keys()) == [
"regtest",
"regtest2",
] # wm.rpcs looks like this: {'regtest': <BitcoinRpc http://localhost:18543>, 'regtest2': <BitcoinRpc http://localhost:18544>}
assert wm.rpc.port == 18544
assert wm.wallets_names == []
assert wm.chain == "regtest2"
assert wm.working_folder.endswith("regtest2")
second_wallet = wm.create_wallet(
"a_regtest2_test_wallet", 1, "wpkh", [device.keys[5]], [device]
)
# Note: "regtest2" is recognised by the get_network() from embit as Liquid, that is why there is an error in the logs saying the Bitcoin address is not valid since a Liquid address is derived.
assert wm.wallets_names == ["a_regtest2_test_wallet"]
def test_WalletManager_check_duplicate_keys(empty_data_folder):
@ -147,7 +171,6 @@ def test_WalletManager_check_duplicate_keys(empty_data_folder):
MagicMock(), # needs rpc
"regtest",
None,
allow_threading=False,
)
key1 = Key(
"[f3e6eaff/84h/0h/0h]xpub6C5cCQfycZrPJnNg6cDdUU5efJrab8thRQDBxSSB4gP2J3xGdWu8cqiLvPZkejtuaY9LursCn6Es9PqHgLhBktW8217BomGDVBAJjUms8iG",
@ -215,7 +238,6 @@ def test_wallet_sortedmulti(
bitcoin_regtest.get_rpc(),
"regtest",
device_manager,
allow_threading=False,
)
device = device_manager.get_by_alias("trezor")
second_device = device_manager.get_by_alias("specter")
@ -270,7 +292,6 @@ def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_man
bitcoin_regtest.get_rpc(),
"regtest",
device_manager,
allow_threading=False,
)
# A wallet-creation needs a device
device = device_manager.get_by_alias("specter")
@ -330,7 +351,6 @@ def test_wallet_change_addresses(
bitcoin_regtest.get_rpc(),
"regtest",
device_manager,
allow_threading=False,
)
# A wallet-creation needs a device
device = device_manager.get_by_alias("specter")
@ -359,7 +379,7 @@ def test_wallet_change_addresses(
# See: https://github.com/bitcoin/bitcoin/issues/14654
def test_singlesig_wallet_backup_and_restore(caplog, specter_regtest_configured):
def test_singlesig_wallet_backup_and_restore(caplog, specter_regtest_configured, node):
"""
Single-sig wallets should be able to be backed up and re-imported with or without
the "devices" attr in the json backup.
@ -394,7 +414,7 @@ def test_singlesig_wallet_backup_and_restore(caplog, specter_regtest_configured)
assert "devices" in wallet_backup
# Clear everything out as if we've never seen this wallet or device before
wallet_manager.delete_wallet(wallet)
wallet_manager.delete_wallet(wallet, node)
device_manager.remove_device(device, wallet_manager=wallet_manager)
assert wallet.name not in wallet_manager.wallets_names
assert device.name not in device_manager.devices_names
@ -451,7 +471,7 @@ def test_singlesig_wallet_backup_and_restore(caplog, specter_regtest_configured)
del wallet_backup["devices"]
# Clear everything out as if we've never seen this wallet or device before
wallet_manager.delete_wallet(wallet)
wallet_manager.delete_wallet(wallet, node)
device_manager.remove_device(device, wallet_manager=wallet_manager)
assert wallet.name not in wallet_manager.wallets_names
assert device.name not in device_manager.devices_names
@ -505,7 +525,7 @@ def test_singlesig_wallet_backup_and_restore(caplog, specter_regtest_configured)
def test_multisig_wallet_backup_and_restore(
bitcoin_regtest, caplog, specter_regtest_configured
bitcoin_regtest, caplog, specter_regtest_configured, node
):
"""
Multisig wallets should be able to be backed up and re-imported
@ -574,7 +594,7 @@ def test_multisig_wallet_backup_and_restore(
assert "devices" in wallet_backup
# Clear everything out as if we've never seen this wallet or device before
wallet_manager.delete_wallet(wallet)
wallet_manager.delete_wallet(wallet, node)
device_manager.remove_device(device, wallet_manager=wallet_manager)
assert wallet.name not in wallet_manager.wallets_names
assert device.name not in device_manager.devices_names
@ -634,7 +654,7 @@ def test_multisig_wallet_backup_and_restore(
del wallet_backup["devices"]
# Clear everything out as if we've never seen this wallet or device before
wallet_manager.delete_wallet(wallet)
wallet_manager.delete_wallet(wallet, node)
for device_names in device_manager.devices:
device = device_manager.devices[device_names]
device_manager.remove_device(device, wallet_manager=wallet_manager)
@ -689,3 +709,19 @@ def test_multisig_wallet_backup_and_restore(
# We restored the wallet's utxos
assert wallet.amount_total == 3.3
def test_threading(specter_regtest_configured_with_threading):
assert (
specter_regtest_configured_with_threading.config["testing"][
"allow_threading_for_testing"
]
== True
)
device = specter_regtest_configured_with_threading.device_manager.get_by_alias(
"trezor"
)
wm = specter_regtest_configured_with_threading.wallet_manager
wallet = wm.create_wallet("test_wallet", 1, "wpkh", [device.keys[5]], [device])
assert wm.wallets_names == ["test_wallet"]
assert wm.data_folder.endswith("wallets")

View file

@ -5,10 +5,12 @@ import tempfile
from cryptoadvance.specter.node import Node
from cryptoadvance.specter.managers.node_manager import NodeManager
from cryptoadvance.specter.helpers import is_liquid
from cryptoadvance.specter.specter_error import SpecterError
from mock import MagicMock, call, patch
def test_Node_btc(bitcoin_regtest):
def test_Node_btc(bitcoin_regtest, wallet):
with tempfile.TemporaryDirectory("_some_datafolder_tmp") as data_folder:
node = Node.from_json(
{
@ -66,6 +68,18 @@ def test_Node_btc(bitcoin_regtest):
# {'version': 200100, 'subversion': '/Satoshi:0.20.1/', 'protocolversion': 70015, 'localservices': '0000000000000409', 'localservicesnames': ['NETWORK', 'WITNESS', 'NETWORK_LIMITED'], 'localrelay': True, 'timeoffset': 0, 'networkactive': True, 'connections': 0, 'networks': [{'name': 'ipv4', 'limited': False, 'reachable': True, 'proxy': '', 'proxy_randomize_credentials': False}, {'name': 'ipv6', 'limited': False, 'reachable': True, 'proxy': '', 'proxy_randomize_credentials': False}, {'name': 'onion', 'limited': True, 'reachable': False, 'proxy': '', 'proxy_randomize_credentials': False}], 'relayfee': 1e-05, 'incrementalfee': 1e-05, 'localaddresses': [{'address': '2a02:810d:d00:7700:233e:a7e:ded8:f2da', 'port': 18542, 'score': 1}, {'address': '2a02:810d:d00:7700:29ec:5c5b:196b:78b2', 'port': 18542, 'score': 1}], 'warnings': ''}
assert node.network_info["connections"] == 0
assert node.network_info["warnings"] == ""
# Testing deleting the wallet file on the node
with pytest.raises(
SpecterError,
match="Trying to delete the wallet file on the node but the wallet had not been unloaded properly.",
):
node.delete_wallet_file(wallet)
# No error raised now anymore since the wallet was unloaded
# Should return False because of datadir being "" which translates to the default datadir which the tests are not using.
node.delete_wallet_file(wallet) == False
# Update the datadir to the correct one
node.update_rpc(datadir=bitcoin_regtest.datadir)
node.delete_wallet_file(wallet) == True
@pytest.mark.elm

View file

@ -30,6 +30,10 @@ def almost_equal(a: Number, b: Number, precision: float = 0.01) -> bool:
def test_rr_psbt_get(client, specter_regtest_configured, bitcoin_regtest, caplog):
create_a_simple_wallet(specter_regtest_configured, bitcoin_regtest)
assert (
specter_regtest_configured.config["testing"]["allow_threading_for_testing"]
== False
) # Tests fail with threading
caplog.set_level(logging.DEBUG)
""" testing the registration """
# Unauthorized
@ -172,18 +176,17 @@ def create_a_simple_wallet(specter: Specter, bitcoin_regtest):
)
username = payload["username"]
someuser = specter.user_manager.get_user_by_username(username)
assert not someuser.wallet_manager.working_folder is None
wm = someuser.wallet_manager
assert not wm.working_folder is None
# Create a Wallet
wallet_json = '{"label": "a_simple_wallet", "blockheight": 0, "descriptor": "wpkh([1ef4e492/84h/1h/0h]tpubDC5EUwdy9WWpzqMWKNhVmXdMgMbi4ywxkdysRdNr1MdM4SCfVLbNtsFvzY6WKSuzsaVAitj6FmP6TugPuNT6yKZDLsHrSwMd816TnqX7kuc/0/*)#xp8lv5nr", "devices": [{"type": "trezor", "label": "trezor"}]} '
wallet_importer = WalletImporter(
wallet_json, specter, device_manager=someuser.device_manager
)
dm = someuser.device_manager
wallet_importer = WalletImporter(wallet_json, specter, device_manager=dm)
wallet_importer.create_nonexisting_signers(
someuser.device_manager,
dm,
{"unknown_cosigner_0_name": "trezor", "unknown_cosigner_0_type": "trezor"},
)
dm: DeviceManager = someuser.device_manager
wallet = wallet_importer.create_wallet(someuser.wallet_manager)
wallet = wallet_importer.create_wallet(wm)
try:
# fund it with some coins
bitcoin_regtest.testcoin_faucet(address=wallet.getnewaddress())

View file

@ -97,7 +97,6 @@ def test_abandon_purged_tx(
rpc,
"regtest",
device_manager,
allow_threading=False,
)
# Create a new device that can sign psbts (Bitcoin Core hot wallet)

View file

@ -511,9 +511,7 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
# Deleting all Wallets (this will also purge them on core)
for user in specter.user_manager.users:
for wallet in list(user.wallet_manager.wallets.values()):
user.wallet_manager.delete_wallet(
wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest"
)
user.wallet_manager.delete_wallet(wallet)
@pytest.fixture

View file

@ -470,9 +470,7 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
# Deleting all Wallets (this will also purge them on core)
for user in specter.user_manager.users:
for wallet in list(user.wallet_manager.wallets.values()):
user.wallet_manager.delete_wallet(
wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest"
)
user.wallet_manager.delete_wallet(wallet)
@pytest.fixture

View file

@ -470,9 +470,7 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder):
# Deleting all Wallets (this will also purge them on core)
for user in specter.user_manager.users:
for wallet in list(user.wallet_manager.wallets.values()):
user.wallet_manager.delete_wallet(
wallet, bitcoin_datadir=bitcoin_regtest.datadir, chain="regtest"
)
user.wallet_manager.delete_wallet(wallet)
@pytest.fixture