Browse Source

Closes #2910. Add z_listunspent RPC call.

pull/4/head
Simon 6 years ago
parent
commit
d72c19a662
  1. 53
      qa/rpc-tests/wallet_protectcoinbase.py
  2. 4
      src/rpcclient.cpp
  3. 1
      src/rpcserver.cpp
  4. 1
      src/rpcserver.h
  5. 48
      src/test/rpc_wallet_tests.cpp
  6. 132
      src/wallet/rpcwallet.cpp
  7. 77
      src/wallet/wallet.cpp
  8. 14
      src/wallet/wallet.h

53
qa/rpc-tests/wallet_protectcoinbase.py

@ -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()

4
src/rpcclient.cpp

@ -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},

1
src/rpcserver.cpp

@ -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 },

1
src/rpcserver.h

@ -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

48
src/test/rpc_wallet_tests.cpp

@ -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)
{

132
src/wallet/rpcwallet.cpp

@ -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))

77
src/wallet/wallet.cpp

@ -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()));
}
}
}
}

14
src/wallet/wallet.h

@ -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. */

Loading…
Cancel
Save