diff --git a/qa/pull-tester/rpc-tests.sh b/qa/pull-tester/rpc-tests.sh index dde4144c4..50244440e 100755 --- a/qa/pull-tester/rpc-tests.sh +++ b/qa/pull-tester/rpc-tests.sh @@ -27,6 +27,7 @@ testScripts=( 'wallet_1941.py' 'wallet_addresses.py' 'wallet_sapling.py' + 'wallet_listnotes.py' 'listtransactions.py' 'mempool_resurrect_test.py' 'txn_doublespend.py' diff --git a/qa/rpc-tests/wallet_listnotes.py b/qa/rpc-tests/wallet_listnotes.py new file mode 100755 index 000000000..5cd89c661 --- /dev/null +++ b/qa/rpc-tests/wallet_listnotes.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python2 +# Copyright (c) 2018 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, start_nodes, wait_and_assert_operationid_status + +from decimal import Decimal + +# Test wallet z_listunspent behaviour across network upgrades +class WalletListNotes(BitcoinTestFramework): + + def setup_nodes(self): + return start_nodes(4, self.options.tmpdir, [[ + '-nuparams=5ba81b19:202', # Overwinter + '-nuparams=76b809bb:204', # Sapling + ]] * 4) + + def run_test(self): + # Current height = 200 -> Sprout + assert_equal(200, self.nodes[0].getblockcount()) + sproutzaddr = self.nodes[0].z_getnewaddress('sprout') + + # test that we can create a sapling zaddr before sapling activates + saplingzaddr = self.nodes[0].z_getnewaddress('sapling') + + # we've got lots of coinbase (taddr) but no shielded funds yet + assert_equal(0, Decimal(self.nodes[0].z_gettotalbalance()['private'])) + + # Set current height to 201 -> Sprout + self.nodes[0].generate(1) + self.sync_all() + assert_equal(201, self.nodes[0].getblockcount()) + + mining_addr = self.nodes[0].listunspent()[0]['address'] + + # Shield coinbase funds (must be a multiple of 10, no change allowed pre-sapling) + receive_amount_10 = Decimal('10.0') - Decimal('0.0001') + recipients = [{"address":sproutzaddr, "amount":receive_amount_10}] + myopid = self.nodes[0].z_sendmany(mining_addr, recipients) + txid_1 = wait_and_assert_operationid_status(self.nodes[0], myopid) + self.sync_all() + + # No funds (with (default) one or more confirmations) in sproutzaddr yet + assert_equal(0, len(self.nodes[0].z_listunspent())) + assert_equal(0, len(self.nodes[0].z_listunspent(1))) + + # no private balance because no confirmations yet + assert_equal(0, Decimal(self.nodes[0].z_gettotalbalance()['private'])) + + # list private unspent, this time allowing 0 confirmations + unspent_cb = self.nodes[0].z_listunspent(0) + assert_equal(1, len(unspent_cb)) + assert_equal(False, unspent_cb[0]['change']) + assert_equal(txid_1, unspent_cb[0]['txid']) + assert_equal(True, unspent_cb[0]['spendable']) + assert_equal(sproutzaddr, unspent_cb[0]['address']) + assert_equal(receive_amount_10, unspent_cb[0]['amount']) + + # list unspent, filtering by address, should produce same result + unspent_cb_filter = self.nodes[0].z_listunspent(0, 9999, False, [sproutzaddr]) + assert_equal(unspent_cb, unspent_cb_filter) + + # Generate a block to confirm shield coinbase tx + self.nodes[0].generate(1) + self.sync_all() + + # Current height = 202 -> Overwinter. Default address type remains Sprout + assert_equal(202, self.nodes[0].getblockcount()) + + # Send 1.0 (actually 0.9999) from sproutzaddr to a new zaddr + sproutzaddr2 = self.nodes[0].z_getnewaddress() + receive_amount_1 = Decimal('1.0') - Decimal('0.0001') + change_amount_9 = receive_amount_10 - Decimal('1.0') + assert_equal('sprout', self.nodes[0].z_validateaddress(sproutzaddr2)['type']) + recipients = [{"address": sproutzaddr2, "amount":receive_amount_1}] + myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients) + txid_2 = wait_and_assert_operationid_status(self.nodes[0], myopid) + self.sync_all() + + # list unspent, allowing 0conf txs + unspent_tx = self.nodes[0].z_listunspent(0) + assert_equal(len(unspent_tx), 2) + # sort low-to-high by amount (order of returned entries is not guaranteed) + unspent_tx = sorted(unspent_tx, key=lambda k: k['amount']) + assert_equal(False, unspent_tx[0]['change']) + assert_equal(txid_2, unspent_tx[0]['txid']) + assert_equal(True, unspent_tx[0]['spendable']) + assert_equal(sproutzaddr2, unspent_tx[0]['address']) + assert_equal(receive_amount_1, unspent_tx[0]['amount']) + + assert_equal(True, unspent_tx[1]['change']) + assert_equal(txid_2, unspent_tx[1]['txid']) + assert_equal(True, unspent_tx[1]['spendable']) + assert_equal(sproutzaddr, unspent_tx[1]['address']) + assert_equal(change_amount_9, unspent_tx[1]['amount']) + + unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, [sproutzaddr2]) + assert_equal(1, len(unspent_tx_filter)) + assert_equal(unspent_tx[0], unspent_tx_filter[0]) + + unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, [sproutzaddr]) + assert_equal(1, len(unspent_tx_filter)) + assert_equal(unspent_tx[1], unspent_tx_filter[0]) + + # Set current height to 204 -> Sapling + self.nodes[0].generate(2) + self.sync_all() + assert_equal(204, self.nodes[0].getblockcount()) + + # No funds in saplingzaddr yet + assert_equal(0, len(self.nodes[0].z_listunspent(0, 9999, False, [saplingzaddr]))) + + # Send 0.9999 to our sapling zaddr + # (sending from a sprout zaddr to a sapling zaddr is disallowed, + # so send from coin base) + receive_amount_2 = Decimal('2.0') - Decimal('0.0001') + recipients = [{"address": saplingzaddr, "amount":receive_amount_2}] + myopid = self.nodes[0].z_sendmany(mining_addr, recipients) + txid_3 = wait_and_assert_operationid_status(self.nodes[0], myopid) + self.sync_all() + unspent_tx = self.nodes[0].z_listunspent(0) + assert_equal(3, len(unspent_tx)) + + # low-to-high in amount + unspent_tx = sorted(unspent_tx, key=lambda k: k['amount']) + + assert_equal(False, unspent_tx[0]['change']) + assert_equal(txid_2, unspent_tx[0]['txid']) + assert_equal(True, unspent_tx[0]['spendable']) + assert_equal(sproutzaddr2, unspent_tx[0]['address']) + assert_equal(receive_amount_1, unspent_tx[0]['amount']) + + assert_equal(False, unspent_tx[1]['change']) + assert_equal(txid_3, unspent_tx[1]['txid']) + assert_equal(True, unspent_tx[1]['spendable']) + assert_equal(saplingzaddr, unspent_tx[1]['address']) + assert_equal(receive_amount_2, unspent_tx[1]['amount']) + + assert_equal(True, unspent_tx[2]['change']) + assert_equal(txid_2, unspent_tx[2]['txid']) + assert_equal(True, unspent_tx[2]['spendable']) + assert_equal(sproutzaddr, unspent_tx[2]['address']) + assert_equal(change_amount_9, unspent_tx[2]['amount']) + + unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, [saplingzaddr]) + assert_equal(1, len(unspent_tx_filter)) + assert_equal(unspent_tx[1], unspent_tx_filter[0]) + + # test that pre- and post-sapling can be filtered in a single call + unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, + [sproutzaddr, saplingzaddr]) + assert_equal(2, len(unspent_tx_filter)) + unspent_tx_filter = sorted(unspent_tx_filter, key=lambda k: k['amount']) + assert_equal(unspent_tx[1], unspent_tx_filter[0]) + assert_equal(unspent_tx[2], unspent_tx_filter[1]) + + # so far, this node has no watchonly addresses, so results are the same + unspent_tx_watchonly = self.nodes[0].z_listunspent(0, 9999, True) + unspent_tx_watchonly = sorted(unspent_tx_watchonly, key=lambda k: k['amount']) + assert_equal(unspent_tx, unspent_tx_watchonly) + + # TODO: use z_exportviewingkey, z_importviewingkey to test includeWatchonly + # but this requires Sapling support for those RPCs + +if __name__ == '__main__': + WalletListNotes().main() diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index d93279986..9d8ae70a6 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -2458,12 +2458,13 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) "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" + "{txid, jsindex, jsoutindex, confirmations, address, amount, memo} (Sprout)\n" + "{txid, outindex, confirmations, address, amount, memo} (Sapling)\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" + "4. \"addresses\" (string) A json array of zaddrs (both Sprout and Sapling) to filter on. Duplicate addresses not allowed.\n" " [\n" " \"address\" (string) zaddr\n" " ,...\n" @@ -2473,7 +2474,8 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) " {\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" + " \"jsoutindex\" (sprout) : n (numeric) the output index of the joinsplit\n" + " \"outindex\" (sapling) : n (numeric) the output index\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" @@ -2533,17 +2535,14 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) } string address = o.get_str(); auto zaddr = DecodePaymentAddress(address); - if (IsValidPaymentAddress(zaddr)) { - // TODO: Add Sapling support. For now, ensure we can freely convert. - assert(boost::get(&zaddr) != nullptr); - libzcash::SproutPaymentAddress addr = boost::get(zaddr); - if (!fIncludeWatchonly && !pwalletMain->HaveSproutSpendingKey(addr)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, spending key for address does not belong to wallet: ") + address); - } - zaddrs.insert(addr); - } else { + if (!IsValidPaymentAddress(zaddr)) { throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, address is not a valid zaddr: ") + address); } + auto hasSpendingKey = boost::apply_visitor(HaveSpendingKeyForPaymentAddress(pwalletMain), zaddr); + if (!fIncludeWatchonly && !hasSpendingKey) { + throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, spending key for address does not belong to wallet: ") + address); + } + zaddrs.insert(zaddr); if (setAddress.count(address)) { throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated address: ") + address); @@ -2553,10 +2552,15 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) } else { // User did not provide zaddrs, so use default i.e. all addresses - // TODO: Add Sapling support std::set sproutzaddrs = {}; pwalletMain->GetSproutPaymentAddresses(sproutzaddrs); + + // Sapling support + std::set saplingzaddrs = {}; + pwalletMain->GetSaplingPaymentAddresses(saplingzaddrs); + zaddrs.insert(sproutzaddrs.begin(), sproutzaddrs.end()); + zaddrs.insert(saplingzaddrs.begin(), saplingzaddrs.end()); } UniValue results(UniValue::VARR); @@ -2566,6 +2570,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) std::vector saplingEntries; pwalletMain->GetUnspentFilteredNotes(sproutEntries, saplingEntries, zaddrs, nMinDepth, nMaxDepth, !fIncludeWatchonly); std::set> nullifierSet = pwalletMain->GetNullifiersForAddresses(zaddrs); + for (CUnspentSproutNotePlaintextEntry & entry : sproutEntries) { UniValue obj(UniValue::VOBJ); obj.push_back(Pair("txid", entry.jsop.hash.ToString())); @@ -2583,7 +2588,26 @@ UniValue z_listunspent(const UniValue& params, bool fHelp) } results.push_back(obj); } - // TODO: Sapling + + for (UnspentSaplingNoteEntry & entry : saplingEntries) { + UniValue obj(UniValue::VOBJ); + obj.push_back(Pair("txid", entry.op.hash.ToString())); + obj.push_back(Pair("outindex", (int)entry.op.n)); + obj.push_back(Pair("confirmations", entry.nHeight)); + libzcash::SaplingIncomingViewingKey ivk; + libzcash::SaplingFullViewingKey fvk; + pwalletMain->GetSaplingIncomingViewingKey(boost::get(entry.address), ivk); + pwalletMain->GetSaplingFullViewingKey(ivk, fvk); + bool hasSaplingSpendingKey = pwalletMain->HaveSaplingSpendingKey(fvk); + obj.push_back(Pair("spendable", hasSaplingSpendingKey)); + obj.push_back(Pair("address", EncodePaymentAddress(entry.address))); + obj.push_back(Pair("amount", ValueFromAmount(CAmount(entry.note.value())))); // note.value() is equivalent to plaintext.value() + obj.push_back(Pair("memo", HexStr(entry.memo))); + if (hasSaplingSpendingKey) { + obj.push_back(Pair("change", pwalletMain->IsNoteSaplingChange(nullifierSet, entry.address, entry.op))); + } + results.push_back(obj); + } } return results;