Skip to content

Commit

Permalink
Add loadwallet and createwallet RPC load_on_startup options
Browse files Browse the repository at this point in the history
This maintains a persistent list of wallets stored in settings that will
automatically be loaded on startup. Being able to load a wallet automatically
on startup will be more useful in the GUI when the option to create wallets is
added in bitcoin#15006, but it's reasonable to expose this feature by RPC as well.
  • Loading branch information
ryanofsky committed May 7, 2019
1 parent 0e75907 commit 04c80c4
Showing 8 changed files with 135 additions and 4 deletions.
21 changes: 21 additions & 0 deletions src/interfaces/chain.cpp
Original file line number Diff line number Diff line change
@@ -357,6 +357,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);
7 changes: 7 additions & 0 deletions src/interfaces/chain.h
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@

#include <optional.h> // For Optional and nullopt
#include <primitives/transaction.h> // For CTransactionRef
#include <util/settings.h> // For util::SettingsValue

#include <memory>
#include <stddef.h>
@@ -263,6 +264,12 @@ class Chain
//! Current RPC serialization flags.
virtual int rpcSerializationFlags() = 0;

// Return <datadir>/settings.json setting value.
virtual util::SettingsValue getSetting(const std::string& name) = 0;

//! Write a setting to <datadir>/settings.json.
virtual bool updateSetting(const std::string& name, const util::SettingsValue& value) = 0;

//! Synchronously send TransactionAddedToMempool notifications about all
//! current mempool transactions to the specified handler and return after
//! the last one is sent. These notifications aren't coordinated with async
2 changes: 2 additions & 0 deletions src/rpc/client.cpp
Original file line number Diff line number Diff line change
@@ -162,6 +162,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "rescanblockchain", 1, "stop_height"},
{ "createwallet", 1, "disable_private_keys"},
{ "createwallet", 2, "blank"},
{ "createwallet", 3, "load_on_startup"},
{ "loadwallet", 1, "load_on_startup"},
{ "getnodeaddresses", 0, "count"},
{ "stop", 0, "wait" },
};
25 changes: 25 additions & 0 deletions src/wallet/load.cpp
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@
#include <util/system.h>
#include <wallet/wallet.h>

#include <univalue.h>

bool VerifyWallets(interfaces::Chain& chain, const std::vector<std::string>& wallet_files)
{
if (gArgs.IsArgSet("-walletdir")) {
@@ -110,3 +112,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);
}
6 changes: 6 additions & 0 deletions src/wallet/load.h
Original file line number Diff line number Diff line change
@@ -35,4 +35,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
36 changes: 32 additions & 4 deletions src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@
#include <validation.h>
#include <wallet/coincontrol.h>
#include <wallet/feebumper.h>
#include <wallet/load.h>
#include <wallet/psbtwallet.h>
#include <wallet/rpcwallet.h>
#include <wallet/wallet.h>
@@ -2594,14 +2595,15 @@ static UniValue listwallets(const JSONRPCRequest& request)

static UniValue loadwallet(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() != 1)
if (request.fHelp || request.params.size() > 2)
throw std::runtime_error(
RPCHelpMan{"loadwallet",
"\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",
{
{"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{
"{\n"
@@ -2627,10 +2629,20 @@ static UniValue loadwallet(const JSONRPCRequest& request)
}
}

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, warning;
std::shared_ptr<CWallet> const wallet = LoadWallet(*g_rpc_interfaces->chain, location, error, warning);
if (!wallet) throw JSONRPCError(RPC_WALLET_ERROR, error);

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());
obj.pushKV("warning", warning);
@@ -2640,14 +2652,15 @@ static UniValue loadwallet(const JSONRPCRequest& request)

static UniValue createwallet(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() < 1 || request.params.size() > 3) {
if (request.fHelp || request.params.size() < 1 || request.params.size() > 4) {
throw std::runtime_error(
RPCHelpMan{"createwallet",
"\nCreates and loads a new wallet.\n",
{
{"wallet_name", RPCArg::Type::STR, RPCArg::Optional::NO, "The name for the new wallet. If this is a path, the wallet will be created at the path location."},
{"disable_private_keys", RPCArg::Type::BOOL, /* default */ "false", "Disable the possibility of private keys (only watchonlys are possible in this mode)."},
{"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."},
{"load_on_startup", RPCArg::Type::BOOL, /* default */ "false", "Save wallet name to persistent settings and load on startup."},
},
RPCResult{
"{\n"
@@ -2673,6 +2686,11 @@ static UniValue createwallet(const JSONRPCRequest& request)
flags |= WALLET_FLAG_BLANK_WALLET;
}

bool load_on_startup = false;
if (!request.params[3].isNull() && request.params[3].isBool()) {
load_on_startup = request.params[3].get_bool();
}

WalletLocation location(request.params[0].get_str());
if (location.Exists()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet " + location.GetName() + " already exists.");
@@ -2691,6 +2709,11 @@ static UniValue createwallet(const JSONRPCRequest& request)

wallet->postInitProcess();

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());
obj.pushKV("warning", warning);
@@ -2737,7 +2760,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;
}
@@ -4137,7 +4165,7 @@ static const CRPCCommand commands[] =
{ "wallet", "addmultisigaddress", &addmultisigaddress, {"nrequired","keys","label","address_type"} },
{ "wallet", "backupwallet", &backupwallet, {"destination"} },
{ "wallet", "bumpfee", &bumpfee, {"txid", "options"} },
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank"} },
{ "wallet", "createwallet", &createwallet, {"wallet_name", "disable_private_keys", "blank", "load_on_startup"} },
{ "wallet", "dumpprivkey", &dumpprivkey, {"address"} },
{ "wallet", "dumpwallet", &dumpwallet, {"filename"} },
{ "wallet", "encryptwallet", &encryptwallet, {"passphrase"} },
@@ -4169,7 +4197,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"} },
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
@@ -195,6 +195,7 @@
'feature_logging.py',
'p2p_node_network_limited.py',
'feature_blocksdir.py',
'wallet_startup.py',
'feature_config_args.py',
'rpc_help.py',
'feature_help.py',
41 changes: 41 additions & 0 deletions test/functional/wallet_startup.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 04c80c4

Please sign in to comment.