diff --git a/src/cryptoadvance/specter/devices/bitcoin_core.py b/src/cryptoadvance/specter/devices/bitcoin_core.py index 1a6facff6f..f70b887930 100644 --- a/src/cryptoadvance/specter/devices/bitcoin_core.py +++ b/src/cryptoadvance/specter/devices/bitcoin_core.py @@ -9,7 +9,7 @@ from ..device import Device from ..helpers import create_unique_id from ..key import Key -from ..rpc import get_default_datadir +from ..rpc import get_default_datadir, get_walletdir from ..specter_error import SpecterError from ..util.base58 import decode_base58, encode_base58_checksum from ..util.descriptor import AddChecksum @@ -254,16 +254,24 @@ def delete( wallet_manager.rpc.unloadwallet(wallet_rpc_path) # Try deleting wallet file 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: + bitcoin_datadir = os.path.expanduser(bitcoin_datadir) + # Check walletdir in bitcoin.conf; network-specific section takes precedence over default + walletdir = get_walletdir(bitcoin_datadir, chain) + if walletdir: + path = os.path.join(walletdir, wallet_rpc_path) if os.path.exists(path): shutil.rmtree(path) - break + else: + 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: + if os.path.exists(path): + shutil.rmtree(path) + break except: pass # We tried... diff --git a/src/cryptoadvance/specter/node.py b/src/cryptoadvance/specter/node.py index 464fe57617..32bb57e67c 100644 --- a/src/cryptoadvance/specter/node.py +++ b/src/cryptoadvance/specter/node.py @@ -21,6 +21,7 @@ RpcError, autodetect_rpc_confs, get_default_datadir, + get_walletdir, ) from .specter_error import SpecterError, BrokenCoreConnectionException from .device import Device @@ -668,7 +669,11 @@ def delete_wallet_file(self, wallet) -> bool: ) except RpcError: pass - if self.chain == "test": + # Check walletdir in bitcoin.conf; network-specific section takes precedence over default + walletdir = get_walletdir(datadir, self.chain) + if walletdir: + path = os.path.join(walletdir, wallet_rpc_path) + elif self.chain == "test": path = os.path.join(datadir, "testnet3/wallets", wallet_rpc_path) elif self.chain == "main": path = os.path.join(datadir, wallet_rpc_path) diff --git a/src/cryptoadvance/specter/rpc.py b/src/cryptoadvance/specter/rpc.py index 92b310c4df..a7edee1150 100644 --- a/src/cryptoadvance/specter/rpc.py +++ b/src/cryptoadvance/specter/rpc.py @@ -113,6 +113,26 @@ def get_rpcconfig(datadir=get_default_datadir()) -> dict: return config +def get_walletdir(datadir, chain): + """Returns the walletdir for the given chain as configured in bitcoin.conf, + or None if not set. + + The network-specific section (e.g. [regtest]) takes precedence over the + [default] section, matching Bitcoin Core's own config resolution order. + When set, walletdir is an absolute path and should be used directly as the + wallet base path without joining it with datadir. + + datadir is expanded (~ resolved) before use so callers can pass user-entered + paths without manually calling os.path.expanduser first. + """ + datadir = os.path.expanduser(datadir) + config = get_rpcconfig(datadir) + btc_conf = config.get("bitcoin.conf", {}) + return btc_conf.get(chain, {}).get("walletdir") or btc_conf.get("default", {}).get( + "walletdir" + ) + + def _detect_rpc_confs_via_datadir(config=None, datadir=get_default_datadir()): """returns the bitcoin.conf configuration for the network specified in bitcoin.conf with testnet=1, regtest=1, etc. as diff --git a/tests/misc_testdata/rpc_autodetection/example_walletdir_default/bitcoin.conf b/tests/misc_testdata/rpc_autodetection/example_walletdir_default/bitcoin.conf new file mode 100644 index 0000000000..a1324a501b --- /dev/null +++ b/tests/misc_testdata/rpc_autodetection/example_walletdir_default/bitcoin.conf @@ -0,0 +1,3 @@ +rpcuser=bitcoin +rpcpassword=CHANGEME +walletdir=/custom/wallets diff --git a/tests/misc_testdata/rpc_autodetection/example_walletdir_network/bitcoin.conf b/tests/misc_testdata/rpc_autodetection/example_walletdir_network/bitcoin.conf new file mode 100644 index 0000000000..f21ff8cd65 --- /dev/null +++ b/tests/misc_testdata/rpc_autodetection/example_walletdir_network/bitcoin.conf @@ -0,0 +1,5 @@ +rpcuser=bitcoin +rpcpassword=CHANGEME +walletdir=/default/wallets +[regtest] +walletdir=/regtest/wallets diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 733dcb7de0..ee3c13e90c 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -9,6 +9,7 @@ RpcError, _detect_rpc_confs_via_datadir, get_rpcconfig, + get_walletdir, ) from cryptoadvance.specter.specter_error import SpecterError @@ -94,7 +95,50 @@ def test_get_rpcconfig2(empty_data_folder): } -def test_detect_rpc_confs_via_datadir1(empty_data_folder): +def test_get_walletdir_not_configured(empty_data_folder): + """get_walletdir returns None when walletdir is not set in bitcoin.conf.""" + assert get_walletdir(empty_data_folder, "main") is None + assert get_walletdir(empty_data_folder, "regtest") is None + + +def test_get_walletdir_default_section(): + """get_walletdir returns the walletdir from the [default] section.""" + datadir = "./tests/misc_testdata/rpc_autodetection/example_walletdir_default" + assert get_walletdir(datadir, "main") == "/custom/wallets" + assert get_walletdir(datadir, "regtest") == "/custom/wallets" + assert get_walletdir(datadir, "test") == "/custom/wallets" + + +def test_get_walletdir_network_section_takes_precedence(): + """Network-specific walletdir takes precedence over the [default] walletdir.""" + datadir = "./tests/misc_testdata/rpc_autodetection/example_walletdir_network" + # regtest section overrides the default + assert get_walletdir(datadir, "regtest") == "/regtest/wallets" + # other chains fall back to default + assert get_walletdir(datadir, "main") == "/default/wallets" + assert get_walletdir(datadir, "test") == "/default/wallets" + + +def test_get_walletdir_dot_notation_network_override(tmp_path): + """Dot-notation network walletdir overrides the default walletdir.""" + bitcoin_conf = tmp_path / "bitcoin.conf" + bitcoin_conf.write_text( + "walletdir=/default/wallets\n" "regtest.walletdir=/regtest/wallets\n" + ) + assert get_walletdir(str(tmp_path), "regtest") == "/regtest/wallets" + assert get_walletdir(str(tmp_path), "main") == "/default/wallets" + assert get_walletdir(str(tmp_path), "test") == "/default/wallets" + + +def test_get_walletdir_expands_tilde(tmp_path, monkeypatch): + """get_walletdir expands ~ in the datadir path so user-entered paths work.""" + monkeypatch.setenv("HOME", str(tmp_path)) + bitcoin_conf = tmp_path / "bitcoin.conf" + bitcoin_conf.write_text("walletdir=/custom/wallets\n") + assert get_walletdir("~", "main") == "/custom/wallets" + +def test_detect_rpc_confs_via_datadir1(): +def test_detect_rpc_confs_via_datadir1(): c = _detect_rpc_confs_via_datadir( datadir="./tests/misc_testdata/rpc_autodetection/example1" )