diff --git a/src/interfaces/chain.cpp b/src/interfaces/chain.cpp index d1e04b114d40b..e579610b34197 100644 --- a/src/interfaces/chain.cpp +++ b/src/interfaces/chain.cpp @@ -372,6 +372,27 @@ class ChainImpl : public Chain RPCRunLater(name, std::move(fn), seconds); } int rpcSerializationFlags() override { return RPCSerializationFlags(); } + util::SettingsValue getSetting(const std::string& name) override + { + util::SettingsValue result; + gArgs.LockSettings([&](const util::Settings& settings) { + if (const util::SettingsValue* value = util::FindKey(settings.rw_settings, name)) { + result = *value; + } + }); + return result; + } + bool updateSetting(const std::string& name, const util::SettingsValue& value) override + { + gArgs.LockSettings([&](util::Settings& settings) { + if (value.isNull()) { + settings.rw_settings.erase(name); + } else { + settings.rw_settings[name] = value; + } + }); + return gArgs.WriteSettingsFile(); + } void requestMempoolTransactions(Notifications& notifications) override { LOCK2(::cs_main, ::mempool.cs); diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index 61d7ddb9340f6..6cb0b75d882bf 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -7,6 +7,7 @@ #include // For Optional and nullopt #include // For CTransactionRef +#include // For util::SettingsValue #include #include @@ -267,7 +268,19 @@ class Chain //! Current RPC serialization flags. virtual int rpcSerializationFlags() = 0; +<<<<<<< HEAD //! Synchronously send transactionAddedToMempool notifications about all +||||||| merged common ancestors + //! Synchronously send TransactionAddedToMempool notifications about all +======= + // Return /settings.json setting value. + virtual util::SettingsValue getSetting(const std::string& name) = 0; + + //! Write a setting to /settings.json. + virtual bool updateSetting(const std::string& name, const util::SettingsValue& value) = 0; + + //! Synchronously send TransactionAddedToMempool notifications about all +>>>>>>> Add loadwallet and createwallet RPC load_on_startup options //! current mempool transactions to the specified handler and return after //! the last one is sent. These notifications aren't coordinated with async //! notifications sent by handleNotifications, so out of date async diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 3045a74d7a52d..b56a5df6aabac 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -171,7 +171,13 @@ static const CRPCConvertParam vRPCConvertParams[] = { "createwallet", 1, "disable_private_keys"}, { "createwallet", 2, "blank"}, { "createwallet", 4, "avoid_reuse"}, +<<<<<<< HEAD { "createwallet", 5, "descriptors"}, +||||||| merged common ancestors +======= + { "createwallet", 5, "load_on_startup"}, + { "loadwallet", 1, "load_on_startup"}, +>>>>>>> Add loadwallet and createwallet RPC load_on_startup options { "getnodeaddresses", 0, "count"}, { "stop", 0, "wait" }, }; diff --git a/src/wallet/load.cpp b/src/wallet/load.cpp index 8df3e78215cde..0dba2e3e608cb 100644 --- a/src/wallet/load.cpp +++ b/src/wallet/load.cpp @@ -12,6 +12,8 @@ #include #include +#include + bool VerifyWallets(interfaces::Chain& chain, const std::vector& wallet_files) { if (gArgs.IsArgSet("-walletdir")) { @@ -117,3 +119,26 @@ void UnloadWallets() UnloadWallet(std::move(wallet)); } } + +bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name) +{ + util::SettingsValue setting_value = chain.getSetting("wallet"); + if (!setting_value.isArray()) setting_value.setArray(); + for (const util::SettingsValue& value : setting_value.getValues()) { + if (value.isStr() && value.get_str() == wallet_name) return true; + } + setting_value.push_back(wallet_name); + return chain.updateSetting("wallet", setting_value); +} + +bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name) +{ + util::SettingsValue setting_value = chain.getSetting("wallet"); + if (!setting_value.isArray()) return true; + util::SettingsValue new_value(util::SettingsValue::VARR); + for (const util::SettingsValue& value : setting_value.getValues()) { + if (!value.isStr() || value.get_str() != wallet_name) new_value.push_back(value); + } + if (new_value.size() == setting_value.size()) return true; + return chain.updateSetting("wallet", new_value); +} diff --git a/src/wallet/load.h b/src/wallet/load.h index e24b1f2e69241..5541465329021 100644 --- a/src/wallet/load.h +++ b/src/wallet/load.h @@ -33,4 +33,10 @@ void StopWallets(); //! Close all wallets. void UnloadWallets(); +//! Add wallet name to persistent configuration so it will be loaded on startup. +bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name); + +//! Remove wallet name from persistent configuration so it will not be loaded on startup. +bool RemoveWalletSetting(interfaces::Chain& chain, const std::string& wallet_name); + #endif // BITCOIN_WALLET_LOAD_H diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 2a9ac189ea0be..e677ee1f4dd09 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -28,6 +28,13 @@ #include #include #include +<<<<<<< HEAD +||||||| merged common ancestors +#include +======= +#include +#include +>>>>>>> Add loadwallet and createwallet RPC load_on_startup options #include #include #include @@ -2570,6 +2577,7 @@ static UniValue loadwallet(const JSONRPCRequest& request) "\napplied to the new wallet (eg -zapwallettxes, rescan, etc).\n", { {"filename", RPCArg::Type::STR, RPCArg::Optional::NO, "The wallet directory or .dat file."}, + {"load_on_startup", RPCArg::Type::BOOL, /* default */ "false", "Save wallet name to persistent settings and load on startup."}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -2596,10 +2604,32 @@ static UniValue loadwallet(const JSONRPCRequest& request) } } +<<<<<<< HEAD bilingual_str error; std::vector warnings; std::shared_ptr const wallet = LoadWallet(*g_rpc_chain, location, error, warnings); if (!wallet) throw JSONRPCError(RPC_WALLET_ERROR, error.original); +||||||| merged common ancestors + std::string error; + std::vector warning; + std::shared_ptr const wallet = LoadWallet(*g_rpc_chain, location, error, warning); + if (!wallet) throw JSONRPCError(RPC_WALLET_ERROR, error); +======= + bool load_on_startup = false; + if (!request.params[1].isNull() && request.params[1].isBool()) { + load_on_startup = request.params[1].get_bool(); + } + + std::string error; + std::vector warning; + std::shared_ptr const wallet = LoadWallet(*g_rpc_chain, location, error, warning); + if (!wallet) throw JSONRPCError(RPC_WALLET_ERROR, error); +>>>>>>> Add loadwallet and createwallet RPC load_on_startup options + + if (load_on_startup && !AddWalletSetting(wallet->chain(), location.GetName())) { + throw JSONRPCError(RPC_MISC_ERROR, "Wallet loaded, but load-on-startup list could not be written, so wallet " + "may be not loaded on node restart."); + } UniValue obj(UniValue::VOBJ); obj.pushKV("name", wallet->GetName()); @@ -2687,7 +2717,12 @@ static UniValue createwallet(const JSONRPCRequest& request) {"blank", RPCArg::Type::BOOL, /* default */ "false", "Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using sethdseed."}, {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Encrypt the wallet with this passphrase."}, {"avoid_reuse", RPCArg::Type::BOOL, /* default */ "false", "Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind."}, +<<<<<<< HEAD {"descriptors", RPCArg::Type::BOOL, /* default */ "false", "Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation"}, +||||||| merged common ancestors +======= + {"load_on_startup", RPCArg::Type::BOOL, /* default */ "false", "Save wallet name to persistent settings and load on startup."}, +>>>>>>> Add loadwallet and createwallet RPC load_on_startup options }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -2729,7 +2764,18 @@ static UniValue createwallet(const JSONRPCRequest& request) warnings.emplace_back(Untranslated("Wallet is an experimental descriptor wallet")); } +<<<<<<< HEAD bilingual_str error; +||||||| merged common ancestors + std::string error; +======= + bool load_on_startup = false; + if (!request.params[5].isNull() && request.params[5].isBool()) { + load_on_startup = request.params[5].get_bool(); + } + + std::string error; +>>>>>>> Add loadwallet and createwallet RPC load_on_startup options std::shared_ptr wallet; WalletCreationStatus status = CreateWallet(*g_rpc_chain, passphrase, flags, request.params[0].get_str(), error, warnings, wallet); switch (status) { @@ -2742,6 +2788,11 @@ static UniValue createwallet(const JSONRPCRequest& request) // no default case, so the compiler can warn about missing cases } + if (load_on_startup && !AddWalletSetting(wallet->chain(), wallet->GetName())) { + throw JSONRPCError(RPC_MISC_ERROR, "Wallet loaded, but load-on-startup list could not be written, so wallet " + "may be not loaded on node restart."); + } + UniValue obj(UniValue::VOBJ); obj.pushKV("name", wallet->GetName()); obj.pushKV("warning", Join(warnings, Untranslated("\n")).original); @@ -2785,7 +2836,12 @@ static UniValue unloadwallet(const JSONRPCRequest& request) throw JSONRPCError(RPC_MISC_ERROR, "Requested wallet already unloaded"); } + interfaces::Chain& chain = wallet->chain(); UnloadWallet(std::move(wallet)); + if (!RemoveWalletSetting(chain, wallet_name)) { + throw JSONRPCError(RPC_MISC_ERROR, "Wallet unloaded, but load-on-startup list could not be written, so wallet " + "may be reloaded on node restart."); + } return NullUniValue; } @@ -4275,7 +4331,13 @@ static const CRPCCommand commands[] = { "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label","address_type"} }, { "wallet", "backupwallet", &backupwallet, {"destination"} }, { "wallet", "bumpfee", &bumpfee, {"txid", "options"} }, +<<<<<<< HEAD { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "descriptors"} }, +||||||| merged common ancestors + { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse"} }, +======= + { "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "passphrase", "avoid_reuse", "load_on_startup"} }, +>>>>>>> Add loadwallet and createwallet RPC load_on_startup options { "wallet", "dumpprivkey", &dumpprivkey, {"address"} }, { "wallet", "dumpwallet", &dumpwallet, {"filename"} }, { "wallet", "encryptwallet", &encryptwallet, {"passphrase"} }, @@ -4308,7 +4370,7 @@ static const CRPCCommand commands[] = { "wallet", "listunspent", &listunspent, {"minconf","maxconf","addresses","include_unsafe","query_options"} }, { "wallet", "listwalletdir", &listwalletdir, {} }, { "wallet", "listwallets", &listwallets, {} }, - { "wallet", "loadwallet", &loadwallet, {"filename"} }, + { "wallet", "loadwallet", &loadwallet, {"filename", "load_on_startup"} }, { "wallet", "lockunspent", &lockunspent, {"unlock","transactions"} }, { "wallet", "removeprunedfunds", &removeprunedfunds, {"txid"} }, { "wallet", "rescanblockchain", &rescanblockchain, {"start_height", "stop_height"} }, diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 0ea65c68b87d5..9d38194f730cc 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -239,6 +239,7 @@ 'p2p_node_network_limited.py', 'p2p_permissions.py', 'feature_blocksdir.py', + 'wallet_startup.py', 'feature_config_args.py', 'rpc_getaddressinfo_labels_purpose_deprecation.py', 'rpc_getaddressinfo_label_deprecation.py', diff --git a/test/functional/wallet_startup.py b/test/functional/wallet_startup.py new file mode 100755 index 0000000000000..4188af15fa644 --- /dev/null +++ b/test/functional/wallet_startup.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017-2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test wallet load on startup. + +Verify that a bitcoind node can maintain list of wallets loading on startup +""" +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, +) + + +class WalletStartupTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.supports_cli = True + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + node = self.nodes[0] + + self.nodes[0].createwallet(wallet_name='w0', load_on_startup=True) + self.nodes[0].createwallet(wallet_name='w1', load_on_startup=False) + self.nodes[0].createwallet(wallet_name='w2', load_on_startup=True) + self.nodes[0].createwallet(wallet_name='w3', load_on_startup=False) + self.nodes[0].createwallet(wallet_name='w4', load_on_startup=False) + self.nodes[0].unloadwallet(wallet_name='w0') + self.nodes[0].unloadwallet(wallet_name='w4') + self.nodes[0].loadwallet(filename='w4', load_on_startup=True) + assert_equal(set(node.listwallets()), set(('', 'w1', 'w2', 'w3', 'w4'))) + self.stop_nodes() + self.start_node(0, []) + assert_equal(set(node.listwallets()), set(('w2', 'w4'))) + +if __name__ == '__main__': + WalletStartupTest().main()