]> Git Repo - VerusCoin.git/commitdiff
Closes #2910. Add z_listunspent RPC call.
authorSimon <[email protected]>
Wed, 28 Mar 2018 17:38:57 +0000 (10:38 -0700)
committerSimon <[email protected]>
Wed, 28 Mar 2018 17:38:57 +0000 (10:38 -0700)
qa/rpc-tests/wallet_protectcoinbase.py
src/rpcclient.cpp
src/rpcserver.cpp
src/rpcserver.h
src/test/rpc_wallet_tests.cpp
src/wallet/rpcwallet.cpp
src/wallet/wallet.cpp
src/wallet/wallet.h

index afe851a1625ddba851ef92cf75bf5cfd63cbbe8e..5854449ab3d9a28da63778e3aee9aaec14ce382c 100755 (executable)
@@ -8,7 +8,7 @@ from test_framework.test_framework import BitcoinTestFramework
 from test_framework.authproxy import JSONRPCException
 from test_framework.mininode import COIN
 from test_framework.util import assert_equal, initialize_chain_clean, \
-    start_nodes, connect_nodes_bi, stop_node, wait_and_assert_operationid_status
+    start_nodes, connect_nodes_bi, wait_and_assert_operationid_status
 
 import sys
 import time
@@ -96,8 +96,6 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
                 break
         assert_equal("failed", status)
         assert_equal("no UTXOs found for taddr from address" in errorString, True)
-        stop_node(self.nodes[3], 3)
-        self.nodes.pop()
 
         # This send will fail because our wallet does not allow any change when protecting a coinbase utxo,
         # as it's currently not possible to specify a change address in z_sendmany.
@@ -129,6 +127,10 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
         assert_equal("failed", status)
         assert_equal("wallet does not allow any change" in errorString, True)
 
+        # Add viewing key for myzaddr to Node 3
+        myviewingkey = self.nodes[0].z_exportviewingkey(myzaddr)
+        self.nodes[3].z_importviewingkey(myviewingkey, "no")
+
         # This send will succeed.  We send two coinbase utxos totalling 20.0 less a fee of 0.00010000, with no change.
         shieldvalue = Decimal('20.0') - Decimal('0.0001')
         recipients = []
@@ -136,9 +138,43 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
         myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
         mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
         self.sync_all()
+
+        # Verify that z_listunspent can return a note that has zero confirmations
+        results = self.nodes[0].z_listunspent()
+        assert(len(results) == 0)
+        results = self.nodes[0].z_listunspent(0) # set minconf to zero
+        assert(len(results) == 1)
+        assert_equal(results[0]["address"], myzaddr)
+        assert_equal(results[0]["amount"], shieldvalue)
+        assert_equal(results[0]["confirmations"], 0)
+
+        # Mine the tx
         self.nodes[1].generate(1)
         self.sync_all()
 
+        # Verify that z_listunspent returns one note which has been confirmed
+        results = self.nodes[0].z_listunspent()
+        assert(len(results) == 1)
+        assert_equal(results[0]["address"], myzaddr)
+        assert_equal(results[0]["amount"], shieldvalue)
+        assert_equal(results[0]["confirmations"], 1)
+        assert_equal(results[0]["spendable"], True)
+
+        # Verify that z_listunspent returns note for watchonly address on node 3.
+        results = self.nodes[3].z_listunspent(1, 999, True)
+        assert(len(results) == 1)
+        assert_equal(results[0]["address"], myzaddr)
+        assert_equal(results[0]["amount"], shieldvalue)
+        assert_equal(results[0]["confirmations"], 1)
+        assert_equal(results[0]["spendable"], False)
+
+        # Verify that z_listunspent returns error when address spending key from node 0 is not available in wallet of node 1.
+        try:
+            results = self.nodes[1].z_listunspent(1, 999, False, [myzaddr])
+        except JSONRPCException as e:
+            errorString = e.error['message']
+        assert_equal("Invalid parameter, spending key for address does not belong to wallet" in errorString, True)
+
         # Verify that debug=zrpcunsafe logs params, and that full txid is associated with opid
         logpath = self.options.tmpdir+"/node0/regtest/debug.log"
         logcounter = 0
@@ -333,13 +369,22 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
         self.nodes[1].generate(1)
         self.sync_all()
 
-        # check balances
+        # check balances and unspent notes
         resp = self.nodes[2].z_gettotalbalance()
         assert_equal(Decimal(resp["private"]), send_amount)
+
+        notes = self.nodes[2].z_listunspent()
+        sum_of_notes = sum([note["amount"] for note in notes])
+        assert_equal(Decimal(resp["private"]), sum_of_notes)
+
         resp = self.nodes[0].z_getbalance(myzaddr)
         assert_equal(Decimal(resp), zbalance - custom_fee - send_amount)
         sproutvalue -= custom_fee
         check_value_pool(self.nodes[0], 'sprout', sproutvalue)
 
+        notes = self.nodes[0].z_listunspent(1, 99999, False, [myzaddr])
+        sum_of_notes = sum([note["amount"] for note in notes])
+        assert_equal(Decimal(resp), sum_of_notes)
+
 if __name__ == '__main__':
     WalletProtectCoinbaseTest().main()
index 45809cdb0ab0cd197cc5b9f2c16a62a675cbef6e..65134b4308f7eaabe41b57e2c04becb0047c9fd1 100644 (file)
@@ -105,6 +105,10 @@ static const CRPCConvertParam vRPCConvertParams[] =
     { "getblocksubsidy", 0},
     { "z_listaddresses", 0},
     { "z_listreceivedbyaddress", 1},
+    { "z_listunspent", 0 },
+    { "z_listunspent", 1 },
+    { "z_listunspent", 2 },
+    { "z_listunspent", 3 },
     { "z_getbalance", 1},
     { "z_gettotalbalance", 0},
     { "z_gettotalbalance", 1},
index 568ba792662212ca01e6c501a99907ad3d56c70f..86e27867e9f70e733141622b804b980d261d6062 100644 (file)
@@ -385,6 +385,7 @@ static const CRPCCommand vRPCCommands[] =
     { "wallet",             "zcrawreceive",           &zc_raw_receive,         true  },
     { "wallet",             "zcsamplejoinsplit",      &zc_sample_joinsplit,    true  },
     { "wallet",             "z_listreceivedbyaddress",&z_listreceivedbyaddress,false },
+    { "wallet",             "z_listunspent",          &z_listunspent,          false },
     { "wallet",             "z_getbalance",           &z_getbalance,           false },
     { "wallet",             "z_gettotalbalance",      &z_gettotalbalance,      false },
     { "wallet",             "z_mergetoaddress",       &z_mergetoaddress,       false },
index 8ce108cb480b0edea4fa809de2da5a16d4512b7a..fe1b2bdeb5d9266b80ef8efd8ca5bd15c97f1128 100644 (file)
@@ -287,6 +287,7 @@ extern UniValue z_listaddresses(const UniValue& params, bool fHelp); // in rpcwa
 extern UniValue z_exportwallet(const UniValue& params, bool fHelp); // in rpcdump.cpp
 extern UniValue z_importwallet(const UniValue& params, bool fHelp); // in rpcdump.cpp
 extern UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp); // in rpcwallet.cpp
+extern UniValue z_listunspent(const UniValue& params, bool fHelp); // in rpcwallet.cpp
 extern UniValue z_getbalance(const UniValue& params, bool fHelp); // in rpcwallet.cpp
 extern UniValue z_gettotalbalance(const UniValue& params, bool fHelp); // in rpcwallet.cpp
 extern UniValue z_mergetoaddress(const UniValue& params, bool fHelp); // in rpcwallet.cpp
index 04fccfe81d877a4a48f9d98a25b6445223386062..ce2da60c1218a18b967f61e9d5f8f10e813d5a13 100644 (file)
@@ -1266,6 +1266,54 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_encrypted_wallet_zkeys)
 }
 
 
+BOOST_AUTO_TEST_CASE(rpc_z_listunspent_parameters)
+{
+    SelectParams(CBaseChainParams::TESTNET);
+
+    LOCK(pwalletMain->cs_wallet);
+
+    UniValue retValue;
+
+    // too many args
+    BOOST_CHECK_THROW(CallRPC("z_listunspent 1 2 3 4 5"), runtime_error);
+
+    // minconf must be >= 0
+    BOOST_CHECK_THROW(CallRPC("z_listunspent -1"), runtime_error);
+
+    // maxconf must be > minconf
+    BOOST_CHECK_THROW(CallRPC("z_listunspent 2 1"), runtime_error);
+
+    // maxconf must not be out of range
+    BOOST_CHECK_THROW(CallRPC("z_listunspent 1 9999999999"), runtime_error);
+
+    // must be an array of addresses
+    BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 false ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP"), runtime_error);
+
+    // address must be string
+    BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 false [123456]"), runtime_error);
+
+    // no spending key
+    BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 false [\"ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP\"]"), runtime_error);
+
+    // allow watch only
+    BOOST_CHECK_NO_THROW(CallRPC("z_listunspent 1 999 true [\"ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP\"]"));
+
+    // wrong network, mainnet instead of testnet
+    BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 true [\"zcMuhvq8sEkHALuSU2i4NbNQxshSAYrpCExec45ZjtivYPbuiFPwk6WHy4SvsbeZ4siy1WheuRGjtaJmoD1J8bFqNXhsG6U\"]"), runtime_error);
+
+    // create shielded address so we have the spending key
+    BOOST_CHECK_NO_THROW(retValue = CallRPC("z_getnewaddress"));
+    std::string myzaddr = retValue.get_str();
+
+    // return empty array for this address
+    BOOST_CHECK_NO_THROW(retValue = CallRPC("z_listunspent 1 999 false [\"" + myzaddr + "\"]"));
+    UniValue arr = retValue.get_array();
+    BOOST_CHECK_EQUAL(0, arr.size());
+
+    // duplicate address error
+    BOOST_CHECK_THROW(CallRPC("z_listunspent 1 999 false [\"" + myzaddr + "\", \"" + myzaddr + "\"]"), runtime_error);
+}
+
 
 BOOST_AUTO_TEST_CASE(rpc_z_shieldcoinbase_parameters)
 {
index b242580b1db811338ca692ad5c96d2449b34fa8b..59640b4d6f9ccb2c8cd0be9665792750d832a8b2 100644 (file)
@@ -2428,6 +2428,138 @@ UniValue listunspent(const UniValue& params, bool fHelp)
     return results;
 }
 
+
+UniValue z_listunspent(const UniValue& params, bool fHelp)
+{
+    if (!EnsureWalletIsAvailable(fHelp))
+        return NullUniValue;
+
+    if (fHelp || params.size() > 4)
+        throw runtime_error(
+            "z_listunspent ( minconf maxconf includeWatchonly [\"zaddr\",...] )\n"
+            "\nReturns array of unspent shielded notes with between minconf and maxconf (inclusive) confirmations.\n"
+            "Optionally filter to only include notes sent to specified addresses.\n"
+            "When minconf is 0, unspent notes with zero confirmations are returned, even though they are not immediately spendable.\n"
+            "Results are an array of Objects, each of which has:\n"
+            "{txid, jsindex, jsoutindex, confirmations, address, amount, memo}\n"
+            "\nArguments:\n"
+            "1. minconf          (numeric, optional, default=1) The minimum confirmations to filter\n"
+            "2. maxconf          (numeric, optional, default=9999999) The maximum confirmations to filter\n"
+            "3. includeWatchonly (bool, optional, default=false) Also include watchonly addresses (see 'z_importviewingkey')\n"
+            "4. \"addresses\"      (string) A json array of zaddrs to filter on.  Duplicate addresses not allowed.\n"
+            "    [\n"
+            "      \"address\"     (string) zaddr\n"
+            "      ,...\n"
+            "    ]\n"
+            "\nResult\n"
+            "[                             (array of json object)\n"
+            "  {\n"
+            "    \"txid\" : \"txid\",          (string) the transaction id \n"
+            "    \"jsindex\" : n             (numeric) the joinsplit index\n"
+            "    \"jsoutindex\" : n          (numeric) the output index of the joinsplit\n"
+            "    \"confirmations\" : n       (numeric) the number of confirmations\n"
+            "    \"spendable\" : true|false  (boolean) true if note can be spent by wallet, false if note has zero confirmations, false if address is watchonly\n"
+            "    \"address\" : \"address\",    (string) the shielded address\n"
+            "    \"amount\": xxxxx,          (numeric) the amount of value in the note\n"
+            "    \"memo\": xxxxx,            (string) hexademical string representation of memo field\n"
+            "  }\n"
+            "  ,...\n"
+            "]\n"
+
+            "\nExamples\n"
+            + HelpExampleCli("z_listunspent", "")
+            + HelpExampleCli("z_listunspent", "6 9999999 false \"[\\\"ztbx5DLDxa5ZLFTchHhoPNkKs57QzSyib6UqXpEdy76T1aUdFxJt1w9318Z8DJ73XzbnWHKEZP9Yjg712N5kMmP4QzS9iC9\\\",\\\"ztfaW34Gj9FrnGUEf833ywDVL62NWXBM81u6EQnM6VR45eYnXhwztecW1SjxA7JrmAXKJhxhj3vDNEpVCQoSvVoSpmbhtjf\\\"]\"")
+            + HelpExampleRpc("z_listunspent", "6 9999999 false \"[\\\"ztbx5DLDxa5ZLFTchHhoPNkKs57QzSyib6UqXpEdy76T1aUdFxJt1w9318Z8DJ73XzbnWHKEZP9Yjg712N5kMmP4QzS9iC9\\\",\\\"ztfaW34Gj9FrnGUEf833ywDVL62NWXBM81u6EQnM6VR45eYnXhwztecW1SjxA7JrmAXKJhxhj3vDNEpVCQoSvVoSpmbhtjf\\\"]\"")
+        );
+
+    RPCTypeCheck(params, boost::assign::list_of(UniValue::VNUM)(UniValue::VNUM)(UniValue::VBOOL)(UniValue::VARR));
+
+    int nMinDepth = 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");
+    }
+
+    int nMaxDepth = 9999999;
+    if (params.size() > 1) {
+        nMaxDepth = params[1].get_int();
+    }
+    if (nMaxDepth < nMinDepth) {
+        throw JSONRPCError(RPC_INVALID_PARAMETER, "Maximum number of confirmations must be greater or equal to the minimum number of confirmations");
+    }
+
+    std::set<libzcash::PaymentAddress> zaddrs = {};
+
+    bool fIncludeWatchonly = false;
+    if (params.size() > 2) {
+        fIncludeWatchonly = params[2].get_bool();
+    }
+
+    LOCK2(cs_main, pwalletMain->cs_wallet);
+
+    // User has supplied zaddrs to filter on
+    if (params.size() > 3) {
+        UniValue addresses = params[3].get_array();
+        if (addresses.size()==0)
+            throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, addresses array is empty.");
+
+        // Keep track of addresses to spot duplicates
+        set<std::string> setAddress;
+
+        // Sources
+        for (const UniValue& o : addresses.getValues()) {
+            if (!o.isStr()) {
+                throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected string");
+            }
+            string address = o.get_str();
+            try {
+                CZCPaymentAddress zaddr(address);
+                libzcash::PaymentAddress addr = zaddr.Get();
+                if (!fIncludeWatchonly && !pwalletMain->HaveSpendingKey(addr)) {
+                    throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, spending key for address does not belong to wallet: ") + address);
+                }
+                zaddrs.insert(addr);
+            } catch (const std::runtime_error&) {
+                throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, address is not a valid zaddr: ") + address);
+            }
+
+            if (setAddress.count(address)) {
+                throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated address: ") + address);
+            }
+            setAddress.insert(address);
+        }
+    }
+    else {
+        // User did not provide zaddrs, so use default i.e. all addresses
+        pwalletMain->GetPaymentAddresses(zaddrs);
+    }
+
+    UniValue results(UniValue::VARR);
+
+    if (zaddrs.size() > 0) {
+        std::vector<CUnspentNotePlaintextEntry> entries;
+        pwalletMain->GetUnspentFilteredNotes(entries, zaddrs, nMinDepth, nMaxDepth, !fIncludeWatchonly);
+        for (CUnspentNotePlaintextEntry & entry : entries) {
+            UniValue obj(UniValue::VOBJ);
+            obj.push_back(Pair("txid",entry.jsop.hash.ToString()));
+            obj.push_back(Pair("jsindex", (int)entry.jsop.js ));
+            obj.push_back(Pair("jsoutindex", (int)entry.jsop.n));
+            obj.push_back(Pair("confirmations", entry.nHeight));
+            obj.push_back(Pair("spendable", pwalletMain->HaveSpendingKey(entry.address)));
+            obj.push_back(Pair("address", CZCPaymentAddress(entry.address).ToString()));
+            obj.push_back(Pair("amount", ValueFromAmount(CAmount(entry.plaintext.value))));
+            std::string data(entry.plaintext.memo.begin(), entry.plaintext.memo.end());
+            obj.push_back(Pair("memo", HexStr(data)));
+            results.push_back(obj);
+        }
+    }
+
+    return results;
+}
+
+
 UniValue fundrawtransaction(const UniValue& params, bool fHelp)
 {
     if (!EnsureWalletIsAvailable(fHelp))
index e2cd016378886c4c797ef2029bd042340cb125d4..8412325976597ad294cb89a6d9d3d167970791c8 100644 (file)
@@ -3758,3 +3758,80 @@ void CWallet::GetFilteredNotes(
         }
     }
 }
+
+
+/* Find unspent notes filtered by payment address, min depth and max depth */
+void CWallet::GetUnspentFilteredNotes(
+    std::vector<CUnspentNotePlaintextEntry>& outEntries,
+    std::set<PaymentAddress>& filterAddresses,
+    int minDepth,
+    int maxDepth,
+    bool requireSpendingKey)
+{
+    LOCK2(cs_main, cs_wallet);
+
+    for (auto & p : mapWallet) {
+        CWalletTx wtx = p.second;
+
+        // Filter the transactions before checking for notes
+        if (!CheckFinalTx(wtx) || wtx.GetBlocksToMaturity() > 0 || wtx.GetDepthInMainChain() < minDepth || wtx.GetDepthInMainChain() > maxDepth) {
+            continue;
+        }
+
+        if (wtx.mapNoteData.size() == 0) {
+            continue;
+        }
+
+        for (auto & pair : wtx.mapNoteData) {
+            JSOutPoint jsop = pair.first;
+            CNoteData nd = pair.second;
+            PaymentAddress pa = nd.address;
+
+            // skip notes which belong to a different payment address in the wallet
+            if (!(filterAddresses.empty() || filterAddresses.count(pa))) {
+                continue;
+            }
+
+            // skip note which has been spent
+            if (nd.nullifier && IsSpent(*nd.nullifier)) {
+                continue;
+            }
+
+            // skip notes where the spending key is not available
+            if (requireSpendingKey && !HaveSpendingKey(pa)) {
+                continue;
+            }
+
+            int i = jsop.js; // Index into CTransaction.vjoinsplit
+            int j = jsop.n; // Index into JSDescription.ciphertexts
+
+            // Get cached decryptor
+            ZCNoteDecryption decryptor;
+            if (!GetNoteDecryptor(pa, decryptor)) {
+                // Note decryptors are created when the wallet is loaded, so it should always exist
+                throw std::runtime_error(strprintf("Could not find note decryptor for payment address %s", CZCPaymentAddress(pa).ToString()));
+            }
+
+            // determine amount of funds in the note
+            auto hSig = wtx.vjoinsplit[i].h_sig(*pzcashParams, wtx.joinSplitPubKey);
+            try {
+                NotePlaintext plaintext = NotePlaintext::decrypt(
+                        decryptor,
+                        wtx.vjoinsplit[i].ciphertexts[j],
+                        wtx.vjoinsplit[i].ephemeralKey,
+                        hSig,
+                        (unsigned char) j);
+
+                outEntries.push_back(CUnspentNotePlaintextEntry{jsop, pa, plaintext, wtx.GetDepthInMainChain()});
+
+            } catch (const note_decryption_failed &err) {
+                // Couldn't decrypt with this spending key
+                throw std::runtime_error(strprintf("Could not decrypt note for payment address %s", CZCPaymentAddress(pa).ToString()));
+            } catch (const std::exception &exc) {
+                // Unexpected failure
+                throw std::runtime_error(strprintf("Error while decrypting note for payment address %s: %s", CZCPaymentAddress(pa).ToString(), exc.what()));
+            }
+        }
+    }
+}
+
index 82fb1dca0e6682c88ae73d351815e3eefdceda85..b43099b4b141ae6deeea134ae676bb17808d3dee 100644 (file)
@@ -271,7 +271,13 @@ struct CNotePlaintextEntry
     libzcash::NotePlaintext plaintext;
 };
 
-
+/** Decrypted note, location in a transaction, and confirmation height. */
+struct CUnspentNotePlaintextEntry {
+    JSOutPoint jsop;
+    libzcash::PaymentAddress address;
+    libzcash::NotePlaintext plaintext;
+    int nHeight;
+};
 
 /** A transaction with a merkle branch linking it to the block chain. */
 class CMerkleTx : public CTransaction
@@ -1135,6 +1141,12 @@ public:
                           bool ignoreSpent=true,
                           bool ignoreUnspendable=true);
     
+    /* Find unspent notes filtered by payment address, min depth and max depth */
+    void GetUnspentFilteredNotes(std::vector<CUnspentNotePlaintextEntry>& outEntries,
+                                 std::set<libzcash::PaymentAddress>& filterAddresses,
+                                 int minDepth=1,
+                                 int maxDepth=INT_MAX,
+                                 bool requireSpendingKey=true);
 };
 
 /** A key allocated from the key pool. */
This page took 0.042845 seconds and 4 git commands to generate.