diff --git a/doc/release-notes.md b/doc/release-notes.md
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -3,3 +3,12 @@
This release includes the following features and fixes:
+
+Dynamic loading of wallets
+--------------------------
+
+Previously, wallets could only be loaded at startup, by specifying `-wallet` parameters on the command line or in the bitcoin.conf file. It is now possible to load wallets dynamically at runtime by calling the `loadwallet` RPC.
+
+The wallet can be specified as file/directory basename (which must be located in the `walletdir` directory), or as an absolute path to a file/directory.
+
+This feature is currently only available through the RPC interface. Wallets loaded in this way will not display in the bitcoin-qt GUI.
diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp
--- a/src/wallet/rpcwallet.cpp
+++ b/src/wallet/rpcwallet.cpp
@@ -3283,6 +3283,63 @@
return obj;
}
+UniValue loadwallet(const Config &config, const JSONRPCRequest &request) {
+ if (request.fHelp || request.params.size() != 1) {
+ throw std::runtime_error(
+ "loadwallet \"filename\"\n"
+ "\nLoads a wallet from a wallet file or directory."
+ "\nNote that all wallet command-line options used when starting "
+ "bitcoind will be"
+ "\napplied to the new wallet (eg -zapwallettxes, upgradewallet, "
+ "rescan, etc).\n"
+ "\nArguments:\n"
+ "1. \"filename\" (string, required) The wallet directory or "
+ ".dat file.\n"
+ "\nResult:\n"
+ "{\n"
+ " \"name\" : , (string) The wallet name if "
+ "loaded successfully.\n"
+ " \"warning\" : , (string) Warning message if "
+ "wallet was not loaded cleanly.\n"
+ "}\n"
+ "\nExamples:\n" +
+ HelpExampleCli("loadwallet", "\"test.dat\"") +
+ HelpExampleRpc("loadwallet", "\"test.dat\""));
+ }
+
+ const CChainParams &chainParams = config.GetChainParams();
+
+ std::string wallet_file = request.params[0].get_str();
+ std::string error;
+
+ fs::path wallet_path = fs::absolute(wallet_file, GetWalletDir());
+ if (fs::symlink_status(wallet_path).type() == fs::file_not_found) {
+ throw JSONRPCError(RPC_WALLET_NOT_FOUND,
+ "Wallet " + wallet_file + " not found.");
+ }
+
+ std::string warning;
+ if (!CWallet::Verify(chainParams, wallet_file, false, error, warning)) {
+ throw JSONRPCError(RPC_WALLET_ERROR,
+ "Wallet file verification failed: " + error);
+ }
+
+ CWallet *const wallet = CWallet::CreateWalletFromFile(
+ chainParams, wallet_file, fs::absolute(wallet_file, GetWalletDir()));
+ if (!wallet) {
+ throw JSONRPCError(RPC_WALLET_ERROR, "Wallet loading failed.");
+ }
+ AddWallet(wallet);
+
+ wallet->postInitProcess();
+
+ UniValue obj(UniValue::VOBJ);
+ obj.pushKV("name", wallet->GetName());
+ obj.pushKV("warning", warning);
+
+ return obj;
+}
+
static UniValue resendwallettransactions(const Config &config,
const JSONRPCRequest &request) {
CWallet *const pwallet = GetWalletForJSONRPCRequest(request);
@@ -4275,6 +4332,7 @@
{ "wallet", "listtransactions", listtransactions, {"account","count","skip","include_watchonly"} },
{ "wallet", "listunspent", listunspent, {"minconf","maxconf","addresses","include_unsafe","query_options"} },
{ "wallet", "listwallets", listwallets, {} },
+ { "wallet", "loadwallet", loadwallet, {"filename"} },
{ "wallet", "lockunspent", lockunspent, {"unlock","transactions"} },
{ "wallet", "move", movecmd, {"fromaccount","toaccount","amount","minconf","comment"} },
{ "wallet", "rescanblockchain", rescanblockchain, {"start_height", "stop_height"} },
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
@@ -176,6 +176,52 @@
assert_equal(w1.getwalletinfo()['paytxfee'], 0)
assert_equal(w2.getwalletinfo()['paytxfee'], 4.0)
+ self.log.info("Test dynamic wallet loading")
+
+ self.restart_node(0, ['-nowallet'])
+ assert_equal(node.listwallets(), [])
+ assert_raises_rpc_error(-32601, "Method not found", node.getwalletinfo)
+
+ self.log.info("Load first wallet")
+ loadwallet_name = node.loadwallet(wallet_names[0])
+ assert_equal(loadwallet_name['name'], wallet_names[0])
+ assert_equal(node.listwallets(), wallet_names[0:1])
+ node.getwalletinfo()
+ w1 = node.get_wallet_rpc(wallet_names[0])
+ w1.getwalletinfo()
+
+ self.log.info("Load second wallet")
+ loadwallet_name = node.loadwallet(wallet_names[1])
+ assert_equal(loadwallet_name['name'], wallet_names[1])
+ assert_equal(node.listwallets(), wallet_names[0:2])
+ assert_raises_rpc_error(-19,
+ "Wallet file not specified", node.getwalletinfo)
+ w2 = node.get_wallet_rpc(wallet_names[1])
+ w2.getwalletinfo()
+
+ self.log.info("Load remaining wallets")
+ for wallet_name in wallet_names[2:]:
+ loadwallet_name = self.nodes[0].loadwallet(wallet_name)
+ assert_equal(loadwallet_name['name'], wallet_name)
+
+ assert_equal(set(self.nodes[0].listwallets()), set(wallet_names))
+
+ # Fail to load if wallet doesn't exist
+ assert_raises_rpc_error(-18, 'Wallet wallets not found.',
+ self.nodes[0].loadwallet, 'wallets')
+
+ # Fail to load duplicate wallets
+ assert_raises_rpc_error(-4, 'Wallet file verification failed: Error loading wallet w1. Duplicate -wallet filename specified.',
+ self.nodes[0].loadwallet, wallet_names[0])
+
+ # Fail to load if one wallet is a copy of another
+ assert_raises_rpc_error(-1, "BerkeleyBatch: Can't open database w8_copy (duplicates fileid",
+ self.nodes[0].loadwallet, 'w8_copy')
+
+ # Fail to load if wallet file is a symlink
+ assert_raises_rpc_error(-4, "Wallet file verification failed: Invalid -wallet path 'w8_symlink'",
+ self.nodes[0].loadwallet, 'w8_symlink')
+
if __name__ == '__main__':
MultiWalletTest().main()