Browse Source

Merge pull request #121 from MyHush/z_listunspent

Port z_listunspent
pull/123/head
Duke Leto 6 years ago
committed by GitHub
parent
commit
70bb24f368
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 58
      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. 137
      src/wallet/rpcwallet.cpp
  7. 75
      src/wallet/wallet.cpp
  8. 18
      src/wallet/wallet.h

58
qa/rpc-tests/wallet_protectcoinbase.py

@ -122,8 +122,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.
@ -155,6 +153,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 = []
@ -162,9 +164,45 @@ 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 which 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)
assert_equal(results[0]["spendable"], True) # the note is unspent and can only be spent after 1 confirmation.
# 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
@ -359,13 +397,27 @@ 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 = 0
for note in notes:
sum_of_notes += note["amount"]
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 = 0
for note in notes:
sum_of_notes += note["amount"]
assert_equal(Decimal(resp), sum_of_notes)
if __name__ == '__main__':
WalletProtectCoinbaseTest().main()

4
src/rpcclient.cpp

@ -115,6 +115,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

@ -394,6 +394,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_sendmany", &z_sendmany, false },

1
src/rpcserver.h

@ -298,6 +298,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_sendmany(const UniValue& params, bool fHelp); // in rpcwallet.cpp

48
src/test/rpc_wallet_tests.cpp

@ -1312,6 +1312,54 @@ BOOST_AUTO_TEST_CASE(rpc_z_shieldcoinbase_parameters)
}
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_internals)

137
src/wallet/rpcwallet.cpp

@ -2427,6 +2427,143 @@ 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"
"Results are an array of Objects, each of which has:\n"
"{txid, js_index, output_index, 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\n"
" [\n"
" \"address\" (string) zaddr\n"
" ,...\n"
" ]\n"
"\nResult\n"
"[ (array of json object)\n"
" {\n"
" \"txid\" : \"txid\", (string) the transaction id \n"
" \"js_index\" : n (numeric) the joinsplit index\n"
" \"output_index\" : n (numeric) the output index of the joinsplit\n"
" \"confirmations\" : n (numeric) the number of confirmations\n"
" \"spendable\" : true|false (boolean) true if note is spendable, false if 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\\\"]\"")
);
LOCK2(cs_main, pwalletMain->cs_wallet);
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");
}
set<libzcash::PaymentAddress> zaddrs = {};
bool fIncludeWatchonly = false;
if (params.size() > 2) {
fIncludeWatchonly = params[2].get_bool();
}
// 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
std::set<libzcash::PaymentAddress> addresses;
pwalletMain->GetPaymentAddresses(addresses);
for (auto addr : addresses ) {
if (fIncludeWatchonly || pwalletMain->HaveSpendingKey(addr)) {
zaddrs.insert(addr);
}
}
}
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("js_index", (int)entry.jsop.js ));
obj.push_back(Pair("output_index", (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))

75
src/wallet/wallet.cpp

@ -3723,3 +3723,78 @@ void CWallet::GetFilteredNotes(std::vector<CNotePlaintextEntry> & outEntries, st
}
}
}
/* 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 ignoreUnspendable)
{
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 which cannot be spent
if (ignoreUnspendable && !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()));
}
}
}
}

18
src/wallet/wallet.h

@ -270,7 +270,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
@ -1119,14 +1125,20 @@ public:
bool GetBroadcastTransactions() const { return fBroadcastTransactions; }
/** Set whether this wallet broadcasts transactions. */
void SetBroadcastTransactions(bool broadcast) { fBroadcastTransactions = broadcast; }
/* Find notes filtered by payment address, min depth, ability to spend */
void GetFilteredNotes(std::vector<CNotePlaintextEntry> & outEntries,
std::string address,
int minDepth=1,
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 ignoreUnspendable=true);
};
/** A key allocated from the key pool. */

Loading…
Cancel
Save