diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -17,3 +17,12 @@ -------------- Support for Python 2 has been discontinued for all test files and tools. + +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 @@ -3282,6 +3282,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); @@ -4274,6 +4331,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()