Skip to content

Commit 24f7029

Browse files
committed
Merge #18594: cli: display multiwallet balances in -getinfo
5edad5c test: add -getinfo multiwallet functional tests (Jon Atack) 903b6c1 rpc: drop unused JSONRPCProcessBatchReply size arg, refactor (Jon Atack) afce85e cli: use GetWalletBalances() functionality for -getinfo (Jon Atack) 9f01849 cli: create GetWalletBalances() to fetch multiwallet balances (Jon Atack) 7430775 cli: lift -rpcwallet logic up to CommandLineRPC() (Jon Atack) 29f2cbd cli: extract connection exception handler, -rpcwait logic (Jon Atack) Pull request description: This PR is a client-side version of #18453, per review feedback there and [review club discussions](https://bitcoincore.reviews/18453#meeting-log). It updates `bitcoin-cli -getinfo` on the client side to display wallet name and balance for the loaded wallets when more than one is loaded (e.g. you are in "multiwallet mode") and `-rpcwallet=` is not passed; otherwise, behavior is unchanged. before ```json $ bitcoin-cli -getinfo -regtest { "version": 199900, "blocks": 15599, "headers": 15599, "verificationprogress": 1, "timeoffset": 0, "connections": 0, "proxy": "", "difficulty": 4.656542373906925e-10, "chain": "regtest", "balance": 0.00001000, "relayfee": 0.00001000 } ``` after ```json $ bitcoin-cli -getinfo -regtest { "version": 199900, "blocks": 15599, "headers": 15599, "verificationprogress": 1, "timeoffset": 0, "connections": 0, "proxy": "", "difficulty": 4.656542373906925e-10, "chain": "regtest", "balances": { "": 0.00001000, "Encrypted": 0.00003500, "day-to-day": 0.00000120, "side project": 0.00000094 } } ``` ----- `Review club` discussion about this PR is here: https://bitcoincore.reviews/18453 This PR can be manually tested by building, creating/loading/unloading several wallets with `bitcoin-cli createwallet/loadwallet/unloadwallet` and running `bitcoin-cli -getinfo` and `bitcoin-cli -rpcwallet=<wallet-name> -getinfo`. `wallet_multiwallet.py --usecli` provides regression test coverage on this change, along with `interface_bitcoin_cli.py` where this PR adds test coverage. Credit to Wladimir J. van der Laan for the idea in bitcoin/bitcoin#17314 and bitcoin/bitcoin#18453 (comment). ACKs for top commit: promag: Tested ACK 5edad5c. jnewbery: utACK 5edad5c meshcollider: Code review ACK 5edad5c Tree-SHA512: 4ca36c5f6c49936b40afb605c44459c1d5b80b5bd84df634007ca276b3f6c102a0cb382f9d528370363ee32c94b0d7ffa15184578eaf8de74179e566c5c5cee5
2 parents 793e0ff + 5edad5c commit 24f7029

File tree

4 files changed

+143
-83
lines changed

4 files changed

+143
-83
lines changed

src/bitcoin-cli.cpp

Lines changed: 102 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <chainparamsbase.h>
1111
#include <clientversion.h>
12+
#include <optional.h>
1213
#include <rpc/client.h>
1314
#include <rpc/protocol.h>
1415
#include <rpc/request.h>
@@ -250,7 +251,7 @@ class GetinfoRequestHandler: public BaseRequestHandler
250251
UniValue ProcessReply(const UniValue &batch_in) override
251252
{
252253
UniValue result(UniValue::VOBJ);
253-
std::vector<UniValue> batch = JSONRPCProcessBatchReply(batch_in, batch_in.size());
254+
const std::vector<UniValue> batch = JSONRPCProcessBatchReply(batch_in);
254255
// Errors in getnetworkinfo() and getblockchaininfo() are fatal, pass them on;
255256
// getwalletinfo() and getbalances() are allowed to fail if there is no wallet.
256257
if (!batch[ID_NETWORKINFO]["error"].isNull()) {
@@ -304,7 +305,7 @@ class DefaultRequestHandler: public BaseRequestHandler {
304305
}
305306
};
306307

307-
static UniValue CallRPC(BaseRequestHandler *rh, const std::string& strMethod, const std::vector<std::string>& args)
308+
static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& args, const Optional<std::string>& rpcwallet = {})
308309
{
309310
std::string host;
310311
// In preference order, we choose the following for the port:
@@ -369,14 +370,12 @@ static UniValue CallRPC(BaseRequestHandler *rh, const std::string& strMethod, co
369370

370371
// check if we should use a special wallet endpoint
371372
std::string endpoint = "/";
372-
if (!gArgs.GetArgs("-rpcwallet").empty()) {
373-
std::string walletName = gArgs.GetArg("-rpcwallet", "");
374-
char *encodedURI = evhttp_uriencode(walletName.data(), walletName.size(), false);
373+
if (rpcwallet) {
374+
char* encodedURI = evhttp_uriencode(rpcwallet->data(), rpcwallet->size(), false);
375375
if (encodedURI) {
376-
endpoint = "/wallet/"+ std::string(encodedURI);
376+
endpoint = "/wallet/" + std::string(encodedURI);
377377
free(encodedURI);
378-
}
379-
else {
378+
} else {
380379
throw CConnectionFailed("uri-encode failed");
381380
}
382381
}
@@ -418,6 +417,65 @@ static UniValue CallRPC(BaseRequestHandler *rh, const std::string& strMethod, co
418417
return reply;
419418
}
420419

420+
/**
421+
* ConnectAndCallRPC wraps CallRPC with -rpcwait and an exception handler.
422+
*
423+
* @param[in] rh Pointer to RequestHandler.
424+
* @param[in] strMethod Reference to const string method to forward to CallRPC.
425+
* @param[in] rpcwallet Reference to const optional string wallet name to forward to CallRPC.
426+
* @returns the RPC response as a UniValue object.
427+
* @throws a CConnectionFailed std::runtime_error if connection failed or RPC server still in warmup.
428+
*/
429+
static UniValue ConnectAndCallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& args, const Optional<std::string>& rpcwallet = {})
430+
{
431+
UniValue response(UniValue::VOBJ);
432+
// Execute and handle connection failures with -rpcwait.
433+
const bool fWait = gArgs.GetBoolArg("-rpcwait", false);
434+
do {
435+
try {
436+
response = CallRPC(rh, strMethod, args, rpcwallet);
437+
if (fWait) {
438+
const UniValue& error = find_value(response, "error");
439+
if (!error.isNull() && error["code"].get_int() == RPC_IN_WARMUP) {
440+
throw CConnectionFailed("server in warmup");
441+
}
442+
}
443+
break; // Connection succeeded, no need to retry.
444+
} catch (const CConnectionFailed&) {
445+
if (fWait) {
446+
UninterruptibleSleep(std::chrono::milliseconds{1000});
447+
} else {
448+
throw;
449+
}
450+
}
451+
} while (fWait);
452+
return response;
453+
}
454+
455+
/**
456+
* GetWalletBalances calls listwallets; if more than one wallet is loaded, it then
457+
* fetches mine.trusted balances for each loaded wallet and pushes them to `result`.
458+
*
459+
* @param result Reference to UniValue object the wallet names and balances are pushed to.
460+
*/
461+
static void GetWalletBalances(UniValue& result)
462+
{
463+
std::unique_ptr<BaseRequestHandler> rh{MakeUnique<DefaultRequestHandler>()};
464+
const UniValue listwallets = ConnectAndCallRPC(rh.get(), "listwallets", /* args=*/{});
465+
if (!find_value(listwallets, "error").isNull()) return;
466+
const UniValue& wallets = find_value(listwallets, "result");
467+
if (wallets.size() <= 1) return;
468+
469+
UniValue balances(UniValue::VOBJ);
470+
for (const UniValue& wallet : wallets.getValues()) {
471+
const std::string wallet_name = wallet.get_str();
472+
const UniValue getbalances = ConnectAndCallRPC(rh.get(), "getbalances", /* args=*/{}, wallet_name);
473+
const UniValue& balance = find_value(getbalances, "result")["mine"]["trusted"];
474+
balances.pushKV(wallet_name, balance);
475+
}
476+
result.pushKV("balances", balances);
477+
}
478+
421479
static int CommandLineRPC(int argc, char *argv[])
422480
{
423481
std::string strPrint;
@@ -474,9 +532,8 @@ static int CommandLineRPC(int argc, char *argv[])
474532
}
475533
std::unique_ptr<BaseRequestHandler> rh;
476534
std::string method;
477-
if (gArgs.GetBoolArg("-getinfo", false)) {
535+
if (gArgs.IsArgSet("-getinfo")) {
478536
rh.reset(new GetinfoRequestHandler());
479-
method = "";
480537
} else {
481538
rh.reset(new DefaultRequestHandler());
482539
if (args.size() < 1) {
@@ -485,62 +542,46 @@ static int CommandLineRPC(int argc, char *argv[])
485542
method = args[0];
486543
args.erase(args.begin()); // Remove trailing method name from arguments vector
487544
}
488-
489-
// Execute and handle connection failures with -rpcwait
490-
const bool fWait = gArgs.GetBoolArg("-rpcwait", false);
491-
do {
492-
try {
493-
const UniValue reply = CallRPC(rh.get(), method, args);
494-
495-
// Parse reply
496-
const UniValue& result = find_value(reply, "result");
497-
const UniValue& error = find_value(reply, "error");
498-
499-
if (!error.isNull()) {
500-
// Error
501-
int code = error["code"].get_int();
502-
if (fWait && code == RPC_IN_WARMUP)
503-
throw CConnectionFailed("server in warmup");
504-
strPrint = "error: " + error.write();
505-
nRet = abs(code);
506-
if (error.isObject())
507-
{
508-
UniValue errCode = find_value(error, "code");
509-
UniValue errMsg = find_value(error, "message");
510-
strPrint = errCode.isNull() ? "" : "error code: "+errCode.getValStr()+"\n";
511-
512-
if (errMsg.isStr())
513-
strPrint += "error message:\n"+errMsg.get_str();
514-
515-
if (errCode.isNum() && errCode.get_int() == RPC_WALLET_NOT_SPECIFIED) {
516-
strPrint += "\nTry adding \"-rpcwallet=<filename>\" option to bitcoin-cli command line.";
517-
}
518-
}
519-
} else {
520-
// Result
521-
if (result.isNull())
522-
strPrint = "";
523-
else if (result.isStr())
524-
strPrint = result.get_str();
525-
else
526-
strPrint = result.write(2);
545+
Optional<std::string> wallet_name{};
546+
if (gArgs.IsArgSet("-rpcwallet")) wallet_name = gArgs.GetArg("-rpcwallet", "");
547+
const UniValue reply = ConnectAndCallRPC(rh.get(), method, args, wallet_name);
548+
549+
// Parse reply
550+
UniValue result = find_value(reply, "result");
551+
const UniValue& error = find_value(reply, "error");
552+
if (!error.isNull()) {
553+
// Error
554+
strPrint = "error: " + error.write();
555+
nRet = abs(error["code"].get_int());
556+
if (error.isObject()) {
557+
const UniValue& errCode = find_value(error, "code");
558+
const UniValue& errMsg = find_value(error, "message");
559+
strPrint = errCode.isNull() ? "" : ("error code: " + errCode.getValStr() + "\n");
560+
561+
if (errMsg.isStr()) {
562+
strPrint += ("error message:\n" + errMsg.get_str());
563+
}
564+
if (errCode.isNum() && errCode.get_int() == RPC_WALLET_NOT_SPECIFIED) {
565+
strPrint += "\nTry adding \"-rpcwallet=<filename>\" option to bitcoin-cli command line.";
527566
}
528-
// Connection succeeded, no need to retry.
529-
break;
530567
}
531-
catch (const CConnectionFailed&) {
532-
if (fWait)
533-
UninterruptibleSleep(std::chrono::milliseconds{1000});
534-
else
535-
throw;
568+
} else {
569+
if (gArgs.IsArgSet("-getinfo") && !gArgs.IsArgSet("-rpcwallet")) {
570+
GetWalletBalances(result); // fetch multiwallet balances and append to result
536571
}
537-
} while (fWait);
538-
}
539-
catch (const std::exception& e) {
572+
// Result
573+
if (result.isNull()) {
574+
strPrint = "";
575+
} else if (result.isStr()) {
576+
strPrint = result.get_str();
577+
} else {
578+
strPrint = result.write(2);
579+
}
580+
}
581+
} catch (const std::exception& e) {
540582
strPrint = std::string("error: ") + e.what();
541583
nRet = EXIT_FAILURE;
542-
}
543-
catch (...) {
584+
} catch (...) {
544585
PrintExceptionContinue(nullptr, "CommandLineRPC()");
545586
throw;
546587
}

src/rpc/request.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,20 +130,20 @@ void DeleteAuthCookie()
130130
}
131131
}
132132

133-
std::vector<UniValue> JSONRPCProcessBatchReply(const UniValue &in, size_t num)
133+
std::vector<UniValue> JSONRPCProcessBatchReply(const UniValue& in)
134134
{
135135
if (!in.isArray()) {
136136
throw std::runtime_error("Batch must be an array");
137137
}
138+
const size_t num {in.size()};
138139
std::vector<UniValue> batch(num);
139-
for (size_t i=0; i<in.size(); ++i) {
140-
const UniValue &rec = in[i];
140+
for (const UniValue& rec : in.getValues()) {
141141
if (!rec.isObject()) {
142-
throw std::runtime_error("Batch member must be object");
142+
throw std::runtime_error("Batch member must be an object");
143143
}
144144
size_t id = rec["id"].get_int();
145145
if (id >= num) {
146-
throw std::runtime_error("Batch member id larger than size");
146+
throw std::runtime_error("Batch member id is larger than batch size");
147147
}
148148
batch[id] = rec;
149149
}

src/rpc/request.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ bool GetAuthCookie(std::string *cookie_out);
2626
/** Delete RPC authentication cookie from disk */
2727
void DeleteAuthCookie();
2828
/** Parse JSON-RPC batch reply into a vector */
29-
std::vector<UniValue> JSONRPCProcessBatchReply(const UniValue &in, size_t num);
29+
std::vector<UniValue> JSONRPCProcessBatchReply(const UniValue& in);
3030

3131
class JSONRPCRequest
3232
{

test/functional/interface_bitcoin_cli.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def run_test(self):
6767
if self.is_wallet_compiled():
6868
self.log.info("Test -getinfo and bitcoin-cli getwalletinfo return expected wallet info")
6969
assert_equal(cli_get_info['balance'], BALANCE)
70+
assert 'balances' not in cli_get_info.keys()
7071
wallet_info = self.nodes[0].getwalletinfo()
7172
assert_equal(cli_get_info['keypoolsize'], wallet_info['keypoolsize'])
7273
assert_equal(cli_get_info['unlocked_until'], wallet_info['unlocked_until'])
@@ -76,42 +77,60 @@ def run_test(self):
7677

7778
# Setup to test -getinfo and -rpcwallet= with multiple wallets.
7879
wallets = ['', 'Encrypted', 'secret']
79-
amounts = [Decimal('59.999928'), Decimal(9), Decimal(31)]
80+
amounts = [BALANCE + Decimal('9.999928'), Decimal(9), Decimal(31)]
8081
self.nodes[0].createwallet(wallet_name=wallets[1])
8182
self.nodes[0].createwallet(wallet_name=wallets[2])
8283
w1 = self.nodes[0].get_wallet_rpc(wallets[0])
8384
w2 = self.nodes[0].get_wallet_rpc(wallets[1])
8485
w3 = self.nodes[0].get_wallet_rpc(wallets[2])
8586
w1.walletpassphrase(password, self.rpc_timeout)
87+
w2.encryptwallet(password)
8688
w1.sendtoaddress(w2.getnewaddress(), amounts[1])
8789
w1.sendtoaddress(w3.getnewaddress(), amounts[2])
8890

8991
# Mine a block to confirm; adds a block reward (50 BTC) to the default wallet.
9092
self.nodes[0].generate(1)
9193

92-
self.log.info("Test -getinfo with multiple wallets loaded returns no balance")
93-
assert_equal(set(self.nodes[0].listwallets()), set(wallets))
94-
assert 'balance' not in self.nodes[0].cli('-getinfo').send_cli().keys()
95-
9694
self.log.info("Test -getinfo with multiple wallets and -rpcwallet returns specified wallet balance")
9795
for i in range(len(wallets)):
98-
cli_get_info = self.nodes[0].cli('-getinfo').send_cli('-rpcwallet={}'.format(wallets[i]))
96+
cli_get_info = self.nodes[0].cli('-getinfo', '-rpcwallet={}'.format(wallets[i])).send_cli()
97+
assert 'balances' not in cli_get_info.keys()
9998
assert_equal(cli_get_info['balance'], amounts[i])
10099

101-
self.log.info("Test -getinfo with multiple wallets and -rpcwallet=non-existing-wallet returns no balance")
102-
assert 'balance' not in self.nodes[0].cli('-getinfo').send_cli('-rpcwallet=does-not-exist').keys()
100+
self.log.info("Test -getinfo with multiple wallets and -rpcwallet=non-existing-wallet returns no balances")
101+
cli_get_info_keys = self.nodes[0].cli('-getinfo', '-rpcwallet=does-not-exist').send_cli().keys()
102+
assert 'balance' not in cli_get_info_keys
103+
assert 'balances' not in cli_get_info_keys
103104

104-
self.log.info("Test -getinfo after unloading all wallets except a non-default one returns its balance")
105+
self.log.info("Test -getinfo with multiple wallets returns all loaded wallet names and balances")
106+
assert_equal(set(self.nodes[0].listwallets()), set(wallets))
107+
cli_get_info = self.nodes[0].cli('-getinfo').send_cli()
108+
assert 'balance' not in cli_get_info.keys()
109+
assert_equal(cli_get_info['balances'], {k: v for k, v in zip(wallets, amounts)})
110+
111+
# Unload the default wallet and re-verify.
105112
self.nodes[0].unloadwallet(wallets[0])
113+
assert wallets[0] not in self.nodes[0].listwallets()
114+
cli_get_info = self.nodes[0].cli('-getinfo').send_cli()
115+
assert 'balance' not in cli_get_info.keys()
116+
assert_equal(cli_get_info['balances'], {k: v for k, v in zip(wallets[1:], amounts[1:])})
117+
118+
self.log.info("Test -getinfo after unloading all wallets except a non-default one returns its balance")
106119
self.nodes[0].unloadwallet(wallets[2])
107120
assert_equal(self.nodes[0].listwallets(), [wallets[1]])
108-
assert_equal(self.nodes[0].cli('-getinfo').send_cli()['balance'], amounts[1])
109-
110-
self.log.info("Test -getinfo -rpcwallet=remaining-non-default-wallet returns its balance")
111-
assert_equal(self.nodes[0].cli('-getinfo').send_cli('-rpcwallet={}'.format(wallets[1]))['balance'], amounts[1])
112-
113-
self.log.info("Test -getinfo with -rpcwallet=unloaded wallet returns no balance")
114-
assert 'balance' not in self.nodes[0].cli('-getinfo').send_cli('-rpcwallet={}'.format(wallets[2])).keys()
121+
cli_get_info = self.nodes[0].cli('-getinfo').send_cli()
122+
assert 'balances' not in cli_get_info.keys()
123+
assert_equal(cli_get_info['balance'], amounts[1])
124+
125+
self.log.info("Test -getinfo with -rpcwallet=remaining-non-default-wallet returns only its balance")
126+
cli_get_info = self.nodes[0].cli('-getinfo', '-rpcwallet={}'.format(wallets[1])).send_cli()
127+
assert 'balances' not in cli_get_info.keys()
128+
assert_equal(cli_get_info['balance'], amounts[1])
129+
130+
self.log.info("Test -getinfo with -rpcwallet=unloaded wallet returns no balances")
131+
cli_get_info = self.nodes[0].cli('-getinfo', '-rpcwallet={}'.format(wallets[2])).send_cli()
132+
assert 'balance' not in cli_get_info_keys
133+
assert 'balances' not in cli_get_info_keys
115134
else:
116135
self.log.info("*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests skipped")
117136
self.nodes[0].generate(1) # maintain block parity with the wallet_compiled conditional branch

0 commit comments

Comments
 (0)