From 36ecc16ba7df491690a4723540ccaabbe217b1b6 Mon Sep 17 00:00:00 2001 From: Russell Yanofsky Date: Thu, 22 Jun 2017 19:30:45 -0400 Subject: [PATCH] Simple, backwards compatible RPC multiwallet support. This change allows existing RPCs to work on multiple wallets by calling those RPCs with a wallet=filename named argument. Example usage: bitcoind -regtest -wallet=w1.dat -wallet=w2.dat bitcoin-cli -regtest -named getwalletinfo wallet=w1.dat bitcoin-cli -regtest -named getwalletinfo wallet=w2.dat bitcoin-cli -regtest -named getbalance wallet=w2.dat Individual RPCs can override handling of the wallet named argument, but if they don't, the `GetWalletForJSONRPCRequest` function will automatically chose the right wallet based on the argument value. The wallet= parameter is mandatory if multiple wallets are loaded, and this change only allows JSON-RPC calls made with named arguments to access multiple wallets, so wallet RPC calls made positional arguments will not work if more than one wallet is loaded. Multiwallet python test based on code originally written by Jonas Schnelli --- src/rpc/server.cpp | 11 +++++++++++ src/rpc/server.h | 14 +++++++++++++ src/wallet/rpcwallet.cpp | 11 +++++++++-- test/functional/multiwallet.py | 45 ++++++++++++++++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 5 files changed, 80 insertions(+), 2 deletions(-) create mode 100755 test/functional/multiwallet.py diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp index 63e4e9c630b..fdd05de1c7c 100644 --- a/src/rpc/server.cpp +++ b/src/rpc/server.cpp @@ -213,6 +213,12 @@ std::string CRPCTable::help(const std::string& strCommand, const JSONRPCRequest& std::string firstLetter = category.substr(0,1); boost::to_upper(firstLetter); strRet += "== " + firstLetter + category.substr(1) + " ==\n"; + if (category == "wallet") { + strRet += "\nWhen more than one wallet is loaded (multiple -`wallet=filename` options passed\n" + "to bitcoind), wallet RPCs must be called with an extra named JSON-RPC `wallet`\n" + "parameter containing the wallet filename to disambiguate which wallet file the\n" + "RPC is intended for. Failure to specify will result in method disabled errors.\n\n"; + } } } strRet += strHelp + "\n"; @@ -472,6 +478,11 @@ static inline JSONRPCRequest transformNamedArguments(const JSONRPCRequest& in, c hole += 1; } } + auto wallet = argsIn.find("wallet"); + if (wallet != argsIn.end() && wallet->second->isStr()) { + out.wallet = wallet->second->getValStr(); + argsIn.erase(wallet); + } // If there are still arguments in the argsIn map, this is an error. if (!argsIn.empty()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Unknown named parameter " + argsIn.begin()->first); diff --git a/src/rpc/server.h b/src/rpc/server.h index b20c8277274..48d9cb68b1d 100644 --- a/src/rpc/server.h +++ b/src/rpc/server.h @@ -42,11 +42,25 @@ class JSONRPCRequest public: UniValue id; std::string strMethod; + /** + * Parameters from JSON-RPC request. + * This will be either an object or an array when the JSONRPCRequest object is + * originally created and parsed. But it will be transformed into an + * array before being passed to the RPC method implementation (using the + * list of named arguments provided by the implementation). + */ UniValue params; bool fHelp; std::string URI; std::string authUser; + /** + * Optional wallet name, set for backwards compatibility if the RPC method + * was called with a named "wallet" parameter and the RPC method + * implementation doesn't handle it itself. + */ + std::string wallet; + JSONRPCRequest() : id(NullUniValue), params(NullUniValue), fHelp(false) {} void parse(const UniValue& valRequest); }; diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index e0c7ab9f0f9..0e6db892b37 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -32,8 +32,15 @@ CWallet *GetWalletForJSONRPCRequest(const JSONRPCRequest& request) { - // TODO: Some way to access secondary wallets - return vpwallets.empty() ? nullptr : vpwallets[0]; + if (!request.wallet.empty()) { + for (const auto& wallet : ::vpwallets) { + if (request.wallet == wallet->GetName()) { + return wallet; + } + } + throw JSONRPCError(RPC_INVALID_PARAMETER, "Requested wallet does not exist or is not loaded"); + } + return ::vpwallets.size() == 1 || (request.fHelp && ::vpwallets.size() > 0) ? ::vpwallets[0] : nullptr; } std::string HelpRequiringPassphrase(CWallet * const pwallet) diff --git a/test/functional/multiwallet.py b/test/functional/multiwallet.py new file mode 100755 index 00000000000..2269864fd76 --- /dev/null +++ b/test/functional/multiwallet.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 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 the wallet.""" +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +class MultiWalletTest(BitcoinTestFramework): + + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [['-wallet=w1', '-wallet=w2','-wallet=w3']] + + def setup_network(self): + self.nodes = self.start_nodes(1, self.options.tmpdir, self.extra_args[:1]) + + def run_test(self): + self.nodes[0].generate(nblocks=1, wallet="w1") + + #check default wallet balance + assert_raises_jsonrpc(-32601, "Method not found (disabled)", self.nodes[0].getwalletinfo) + + #check w1 wallet balance + walletinfo = self.nodes[0].getwalletinfo(wallet="w1") + assert_equal(walletinfo['immature_balance'], 50) + + #check w1 wallet balance + walletinfo = self.nodes[0].getwalletinfo(wallet="w2") + assert_equal(walletinfo['immature_balance'], 0) + + self.nodes[0].generate(nblocks=101, wallet="w1") + assert_equal(self.nodes[0].getbalance(wallet="w1"), 100) + assert_equal(self.nodes[0].getbalance(wallet="w2"), 0) + assert_equal(self.nodes[0].getbalance(wallet="w3"), 0) + + huh=self.nodes[0].getnewaddress(wallet="w2") + self.nodes[0].sendtoaddress(address=huh, amount=1, wallet="w1") + self.nodes[0].generate(nblocks=1, wallet="w1") + assert_equal(self.nodes[0].getbalance(wallet="w2"), 1) + +if __name__ == '__main__': + MultiWalletTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 54f625514bd..a3085ca96e2 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -63,6 +63,7 @@ 'segwit.py', # vv Tests less than 2m vv 'wallet.py', + 'multiwallet.py', 'wallet-accounts.py', 'p2p-segwit.py', 'wallet-dump.py',