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
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.
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 = []
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
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()
{ "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},
{ "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 },
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
}
+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)
{
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))
}
}
}
+
+
+/* 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()));
+ }
+ }
+ }
+}
+
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
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. */