diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -9,6 +9,11 @@ Additionally, Bitcoin ABC does not yet change appearance when macOS "dark mode" is activated. +New RPC methods +------------ + - `listwalletdir` returns a list of wallets in the wallet directory which is + configured with `-walletdir` parameter. + Low-level RPC changes ---------------------- - `-usehd` was removed in version 0.16. From that version onwards, all new diff --git a/src/dummywallet.cpp b/src/dummywallet.cpp --- a/src/dummywallet.cpp +++ b/src/dummywallet.cpp @@ -32,6 +32,14 @@ const WalletInitInterface &g_wallet_init_interface = DummyWalletInit(); +fs::path GetWalletDir() { + throw std::logic_error("Wallet function called in non-wallet build."); +} + +std::vector ListWalletDir() { + throw std::logic_error("Wallet function called in non-wallet build."); +} + std::vector> GetWallets() { throw std::logic_error("Wallet function called in non-wallet build."); } diff --git a/src/interfaces/node.h b/src/interfaces/node.h --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -184,6 +184,12 @@ //! Get unspent outputs associated with a transaction. virtual bool getUnspentOutput(const COutPoint &output, Coin &coin) = 0; + //! Return default wallet directory. + virtual std::string getWalletDir() = 0; + + //! Return available wallets in wallet directory. + virtual std::vector listWalletDir() = 0; + //! Return interfaces for accessing wallets (if any). virtual std::vector> getWallets() = 0; diff --git a/src/interfaces/node.cpp b/src/interfaces/node.cpp --- a/src/interfaces/node.cpp +++ b/src/interfaces/node.cpp @@ -41,6 +41,8 @@ class HTTPRPCRequestProcessor; class CWallet; +fs::path GetWalletDir(); +std::vector ListWalletDir(); std::vector> GetWallets(); namespace interfaces { @@ -243,6 +245,14 @@ LOCK(::cs_main); return ::pcoinsTip->GetCoin(output, coin); } + std::string getWalletDir() override { return GetWalletDir().string(); } + std::vector listWalletDir() override { + std::vector paths; + for (auto &path : ListWalletDir()) { + paths.push_back(path.string()); + } + return paths; + } std::vector> getWallets() override { std::vector> wallets; for (const std::shared_ptr &wallet : GetWallets()) { diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2879,6 +2879,37 @@ return obj; } +static UniValue listwalletdir(const Config &config, + const JSONRPCRequest &request) { + if (request.fHelp || request.params.size() != 0) { + throw std::runtime_error( + "listwalletdir\n" + "Returns a list of wallets in the wallet directory.\n" + "{\n" + " \"wallets\" : [ (json array of objects)\n" + " {\n" + " \"name\" : \"name\" (string) The wallet name\n" + " }\n" + " ,...\n" + " ]\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("listwalletdir", "") + + HelpExampleRpc("listwalletdir", "")); + } + + UniValue wallets(UniValue::VARR); + for (const auto &path : ListWalletDir()) { + UniValue wallet(UniValue::VOBJ); + wallet.pushKV("name", path.string()); + wallets.push_back(wallet); + } + + UniValue result(UniValue::VOBJ); + result.pushKV("wallets", wallets); + return result; +} + static UniValue listwallets(const Config &config, const JSONRPCRequest &request) { if (request.fHelp || request.params.size() != 0) { @@ -4705,6 +4736,7 @@ { "wallet", "listsinceblock", listsinceblock, {"blockhash","target_confirmations","include_watchonly","include_removed"} }, { "wallet", "listtransactions", listtransactions, {"label|dummy","count","skip","include_watchonly"} }, { "wallet", "listunspent", listunspent, {"minconf","maxconf","addresses","include_unsafe","query_options"} }, + { "wallet", "listwalletdir", listwalletdir, {} }, { "wallet", "listwallets", listwallets, {} }, { "wallet", "loadwallet", loadwallet, {"filename"} }, { "wallet", "lockunspent", lockunspent, {"unlock","transactions"} }, diff --git a/src/wallet/walletutil.h b/src/wallet/walletutil.h --- a/src/wallet/walletutil.h +++ b/src/wallet/walletutil.h @@ -5,11 +5,16 @@ #ifndef BITCOIN_WALLET_WALLETUTIL_H #define BITCOIN_WALLET_WALLETUTIL_H -#include +#include + +#include //! Get the path of the wallet directory. fs::path GetWalletDir(); +//! Get wallets in wallet directory. +std::vector ListWalletDir(); + //! The WalletLocation class provides wallet information. class WalletLocation final { std::string m_name; diff --git a/src/wallet/walletutil.cpp b/src/wallet/walletutil.cpp --- a/src/wallet/walletutil.cpp +++ b/src/wallet/walletutil.cpp @@ -4,6 +4,8 @@ #include +#include + fs::path GetWalletDir() { fs::path path; @@ -26,6 +28,64 @@ return path; } +static bool IsBerkeleyBtree(const fs::path &path) { + // A Berkeley DB Btree file has at least 4K. + // This check also prevents opening lock files. + boost::system::error_code ec; + if (fs::file_size(path, ec) < 4096) { + return false; + } + + fs::ifstream file(path.string(), std::ios::binary); + if (!file.is_open()) { + return false; + } + + // Magic bytes start at offset 12 + file.seekg(12, std::ios::beg); + uint32_t data = 0; + // Read 4 bytes of file to compare against magic + file.read((char *)&data, sizeof(data)); + + // Berkeley DB Btree magic bytes, from: + // https://github.com/file/file/blob/5824af38469ec1ca9ac3ffd251e7afe9dc11e227/magic/Magdir/database#L74-L75 + // - big endian systems - 00 05 31 62 + // - little endian systems - 62 31 05 00 + return data == 0x00053162 || data == 0x62310500; +} + +std::vector ListWalletDir() { + const fs::path wallet_dir = GetWalletDir(); + std::vector paths; + + for (auto it = fs::recursive_directory_iterator(wallet_dir); it != end(it); + ++it) { + if (it->status().type() == fs::directory_file && + IsBerkeleyBtree(it->path() / "wallet.dat")) { + // Found a directory which contains wallet.dat btree file, add it as + // a wallet. + paths.emplace_back(fs::relative(it->path(), wallet_dir)); + } else if (it.level() == 0 && + it->symlink_status().type() == fs::regular_file && + IsBerkeleyBtree(it->path())) { + if (it->path().filename() == "wallet.dat") { + // Found top-level wallet.dat btree file, add top level + // directory "" as a wallet. + paths.emplace_back(); + } else { + // Found top-level btree file not called wallet.dat. Current + // bitcoin software will never create these files but will allow + // them to be opened in a shared database environment for + // backwards compatibility. Add it to the list of available + // wallets. + paths.emplace_back(fs::relative(it->path(), wallet_dir)); + } + } + } + + return paths; +} + WalletLocation::WalletLocation(const std::string &name) : m_name(name), m_path(fs::absolute(name, GetWalletDir())) {} diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -40,6 +40,9 @@ return wallet_dir(name, "wallet.dat") return wallet_dir(name) + assert_equal(self.nodes[0].listwalletdir(), + {'wallets': [{'name': ''}]}) + # check wallet.dat is created self.stop_nodes() assert_equal(os.path.isfile(wallet_dir('wallet.dat')), True) @@ -71,6 +74,9 @@ os.path.join(self.options.tmpdir, 'extern/w6'), 'w7_symlink', 'w8', ''] extra_args = ['-wallet={}'.format(n) for n in wallet_names] self.start_node(0, extra_args) + assert_equal(set(map(lambda w: w['name'], self.nodes[0].listwalletdir()[ + 'wallets'])), set(['', 'w3', 'w2', 'sub/w5', 'w7', 'w7', 'w1', 'w8', 'w'])) + assert_equal(set(node.listwallets()), set(wallet_names)) # check that all requested wallets were created @@ -190,6 +196,9 @@ self.restart_node(0, extra_args) + assert_equal(set(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), set( + ['', 'w3', 'w2', 'sub/w5', 'w7', 'w7', 'w8_copy', 'w1', 'w8', 'w'])) + wallets = [wallet(w) for w in wallet_names] wallet_bad = wallet("bad") @@ -355,6 +364,9 @@ assert_equal(self.nodes[0].listwallets(), ['w1']) assert_equal(w1.getwalletinfo()['walletname'], 'w1') + assert_equal(set(map(lambda w: w['name'], self.nodes[0].listwalletdir()['wallets'])), set( + ['', 'w3', 'w2', 'sub/w5', 'w7', 'w9', 'w7', 'w8_copy', 'w1', 'w8', 'w'])) + # Test backing up and restoring wallets self.log.info("Test wallet backup") self.restart_node(0, ['-nowallet'])