diff --git a/qa/rpc-tests/wallet_nullifiers.py b/qa/rpc-tests/wallet_nullifiers.py index b7e94b6a6..743af7c92 100755 --- a/qa/rpc-tests/wallet_nullifiers.py +++ b/qa/rpc-tests/wallet_nullifiers.py @@ -170,5 +170,50 @@ class WalletNullifiersTest (BitcoinTestFramework): assert_equal(self.nodes[1].z_getbalance(myzaddr), zaddrremaining2) assert_equal(self.nodes[2].z_getbalance(myzaddr), zaddrremaining2) + # Test viewing keys + + node3mined = Decimal('250.0') + assert_equal({k: Decimal(v) for k, v in self.nodes[3].z_gettotalbalance().items()}, { + 'transparent': node3mined, + 'private': zsendmany2notevalue, + 'total': node3mined + zsendmany2notevalue, + }) + + # add node 1 address and node 2 viewing key to node 3 + myzvkey = self.nodes[2].z_exportviewingkey(myzaddr) + self.nodes[3].importaddress(mytaddr1) + self.nodes[3].z_importviewingkey(myzvkey) + + # Check the address has been imported + assert_equal(myzaddr in self.nodes[3].z_listaddresses(), False) + assert_equal(myzaddr in self.nodes[3].z_listaddresses(True), True) + + # Node 3 should see the same received notes as node 2 + assert_equal( + self.nodes[2].z_listreceivedbyaddress(myzaddr), + self.nodes[3].z_listreceivedbyaddress(myzaddr)) + + # Node 3's balances should be unchanged without explicitly requesting + # to include watch-only balances + assert_equal({k: Decimal(v) for k, v in self.nodes[3].z_gettotalbalance().items()}, { + 'transparent': node3mined, + 'private': zsendmany2notevalue, + 'total': node3mined + zsendmany2notevalue, + }) + + # Wallet can't cache nullifiers for notes received by addresses it only has a + # viewing key for, and therefore can't detect spends. So it sees a balance + # corresponding to the sum of all notes the address received. + # TODO: Fix this during the Sapling upgrade (via #2277) + assert_equal({k: Decimal(v) for k, v in self.nodes[3].z_gettotalbalance(1, True).items()}, { + 'transparent': node3mined + Decimal('1.0'), + 'private': zsendmany2notevalue + zsendmanynotevalue + zaddrremaining + zaddrremaining2, + 'total': node3mined + Decimal('1.0') + zsendmany2notevalue + zsendmanynotevalue + zaddrremaining + zaddrremaining2, + }) + + # Check individual balances reflect the above + assert_equal(self.nodes[3].z_getbalance(mytaddr1), Decimal('1.0')) + assert_equal(self.nodes[3].z_getbalance(myzaddr), zsendmanynotevalue + zaddrremaining + zaddrremaining2) + if __name__ == '__main__': WalletNullifiersTest().main () diff --git a/src/gtest/test_keystore.cpp b/src/gtest/test_keystore.cpp index 903a48839..76b57cd9f 100644 --- a/src/gtest/test_keystore.cpp +++ b/src/gtest/test_keystore.cpp @@ -65,6 +65,11 @@ TEST(keystore_tests, StoreAndRetrieveViewingKey) { EXPECT_FALSE(keyStore.GetSpendingKey(addr, skOut)); EXPECT_FALSE(keyStore.GetNoteDecryptor(addr, decOut)); + // and we can't find it in our list of addresses + std::set addresses; + keyStore.GetPaymentAddresses(addresses); + EXPECT_FALSE(addresses.count(addr)); + keyStore.AddViewingKey(vk); EXPECT_TRUE(keyStore.HaveViewingKey(addr)); EXPECT_TRUE(keyStore.GetViewingKey(addr, vkOut)); @@ -78,11 +83,19 @@ TEST(keystore_tests, StoreAndRetrieveViewingKey) { EXPECT_TRUE(keyStore.GetNoteDecryptor(addr, decOut)); EXPECT_EQ(ZCNoteDecryption(sk.receiving_key()), decOut); + // ... and we should find it in our list of addresses + addresses.clear(); + keyStore.GetPaymentAddresses(addresses); + EXPECT_TRUE(addresses.count(addr)); + keyStore.RemoveViewingKey(vk); EXPECT_FALSE(keyStore.HaveViewingKey(addr)); EXPECT_FALSE(keyStore.GetViewingKey(addr, vkOut)); EXPECT_FALSE(keyStore.HaveSpendingKey(addr)); EXPECT_FALSE(keyStore.GetSpendingKey(addr, skOut)); + addresses.clear(); + keyStore.GetPaymentAddresses(addresses); + EXPECT_FALSE(addresses.count(addr)); // We still have a decryptor because those are cached in memory // (and also we only remove viewing keys when adding a spending key) diff --git a/src/keystore.h b/src/keystore.h index 0b548920b..b1ad32a42 100644 --- a/src/keystore.h +++ b/src/keystore.h @@ -174,6 +174,12 @@ public: setAddress.insert((*mi).first); mi++; } + ViewingKeyMap::const_iterator mvi = mapViewingKeys.begin(); + while (mvi != mapViewingKeys.end()) + { + setAddress.insert((*mvi).first); + mvi++; + } } } diff --git a/src/rpcclient.cpp b/src/rpcclient.cpp index 7ac5db926..def32500d 100644 --- a/src/rpcclient.cpp +++ b/src/rpcclient.cpp @@ -103,9 +103,12 @@ static const CRPCConvertParam vRPCConvertParams[] = { "zcbenchmark", 1 }, { "zcbenchmark", 2 }, { "getblocksubsidy", 0}, + { "z_listaddresses", 0}, { "z_listreceivedbyaddress", 1}, { "z_getbalance", 1}, { "z_gettotalbalance", 0}, + { "z_gettotalbalance", 1}, + { "z_gettotalbalance", 2}, { "z_sendmany", 1}, { "z_sendmany", 2}, { "z_sendmany", 3}, diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index d2c558c96..4e0a798ac 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2960,9 +2960,10 @@ UniValue z_listaddresses(const UniValue& params, bool fHelp) if (fHelp || params.size() > 1) throw runtime_error( - "z_listaddresses\n" + "z_listaddresses ( includeWatchonly )\n" "\nReturns the list of zaddr belonging to the wallet.\n" "\nArguments:\n" + "1. includeWatchonly (bool, optional, default=false) Also include watchonly addresses (see 'z_importviewingkey')\n" "\nResult:\n" "[ (json array of string)\n" " \"zaddr\" (string) a zaddr belonging to the wallet\n" @@ -2975,16 +2976,23 @@ UniValue z_listaddresses(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); + bool fIncludeWatchonly = false; + if (params.size() > 0) { + fIncludeWatchonly = params[0].get_bool(); + } + UniValue ret(UniValue::VARR); std::set addresses; pwalletMain->GetPaymentAddresses(addresses); for (auto addr : addresses ) { - ret.push_back(CZCPaymentAddress(addr).ToString()); + if (fIncludeWatchonly || pwalletMain->HaveSpendingKey(addr)) { + ret.push_back(CZCPaymentAddress(addr).ToString()); + } } return ret; } -CAmount getBalanceTaddr(std::string transparentAddress, int minDepth=1) { +CAmount getBalanceTaddr(std::string transparentAddress, int minDepth=1, bool ignoreUnspendable=true) { set setAddress; vector vecOutputs; CAmount balance = 0; @@ -3006,6 +3014,10 @@ CAmount getBalanceTaddr(std::string transparentAddress, int minDepth=1) { continue; } + if (ignoreUnspendable && !out.fSpendable) { + continue; + } + if (setAddress.size()) { CTxDestination address; if (!ExtractDestination(out.tx->vout[out.i].scriptPubKey, address)) { @@ -3023,11 +3035,11 @@ CAmount getBalanceTaddr(std::string transparentAddress, int minDepth=1) { return balance; } -CAmount getBalanceZaddr(std::string address, int minDepth = 1) { +CAmount getBalanceZaddr(std::string address, int minDepth = 1, bool ignoreUnspendable=true) { CAmount balance = 0; std::vector entries; LOCK2(cs_main, pwalletMain->cs_wallet); - pwalletMain->GetFilteredNotes(entries, address, minDepth); + pwalletMain->GetFilteredNotes(entries, address, minDepth, true, ignoreUnspendable); for (auto & entry : entries) { balance += CAmount(entry.plaintext.value); } @@ -3076,14 +3088,14 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid zaddr."); } - if (!pwalletMain->HaveSpendingKey(zaddr)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "From address does not belong to this node, zaddr spending key not found."); + if (!(pwalletMain->HaveSpendingKey(zaddr) || pwalletMain->HaveViewingKey(zaddr))) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "From address does not belong to this node, zaddr spending key or viewing key not found."); } UniValue result(UniValue::VARR); std::vector entries; - pwalletMain->GetFilteredNotes(entries, fromaddress, nMinDepth, false); + pwalletMain->GetFilteredNotes(entries, fromaddress, nMinDepth, false, false); for (CNotePlaintextEntry & entry : entries) { UniValue obj(UniValue::VOBJ); obj.push_back(Pair("txid",entry.jsop.hash.ToString())); @@ -3142,16 +3154,16 @@ UniValue z_getbalance(const UniValue& params, bool fHelp) } catch (const std::runtime_error&) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid from address, should be a taddr or zaddr."); } - if (!pwalletMain->HaveSpendingKey(zaddr)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "From address does not belong to this node, zaddr spending key not found."); + if (!(pwalletMain->HaveSpendingKey(zaddr) || pwalletMain->HaveViewingKey(zaddr))) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "From address does not belong to this node, zaddr spending key or viewing key not found."); } } CAmount nBalance = 0; if (fromTaddr) { - nBalance = getBalanceTaddr(fromaddress, nMinDepth); + nBalance = getBalanceTaddr(fromaddress, nMinDepth, false); } else { - nBalance = getBalanceZaddr(fromaddress, nMinDepth); + nBalance = getBalanceZaddr(fromaddress, nMinDepth, false); } return ValueFromAmount(nBalance); @@ -3163,12 +3175,13 @@ UniValue z_gettotalbalance(const UniValue& params, bool fHelp) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() > 1) + if (fHelp || params.size() > 2) throw runtime_error( - "z_gettotalbalance ( minconf )\n" + "z_gettotalbalance ( minconf includeWatchonly )\n" "\nReturn the total value of funds stored in the node’s wallet.\n" "\nArguments:\n" "1. minconf (numeric, optional, default=1) Only include private and transparent transactions confirmed at least this many times.\n" + "2. includeWatchonly (bool, optional, default=false) Also include balance in watchonly addresses (see 'importaddress' and 'z_importviewingkey')\n" "\nResult:\n" "{\n" " \"transparent\": xxxxx, (numeric) the total balance of transparent funds\n" @@ -3187,19 +3200,24 @@ UniValue z_gettotalbalance(const UniValue& params, bool fHelp) LOCK2(cs_main, pwalletMain->cs_wallet); int nMinDepth = 1; - if (params.size() == 1) { + if (params.size() > 0) { nMinDepth = params[0].get_int(); } if (nMinDepth < 0) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0"); } + bool fIncludeWatchonly = false; + if (params.size() > 1) { + fIncludeWatchonly = params[1].get_bool(); + } + // getbalance and "getbalance * 1 true" should return the same number // but they don't because wtx.GetAmounts() does not handle tx where there are no outputs // pwalletMain->GetBalance() does not accept min depth parameter // so we use our own method to get balance of utxos. - CAmount nBalance = getBalanceTaddr("", nMinDepth); - CAmount nPrivateBalance = getBalanceZaddr("", nMinDepth); + CAmount nBalance = getBalanceTaddr("", nMinDepth, !fIncludeWatchonly); + CAmount nPrivateBalance = getBalanceZaddr("", nMinDepth, !fIncludeWatchonly); CAmount nTotalBalance = nBalance + nPrivateBalance; UniValue result(UniValue::VOBJ); result.push_back(Pair("transparent", FormatMoney(nBalance)));