Update z_listreceivedbyaddress to support unified addresses (5467)

This commit is contained in:
Larry Ruane 2022-02-05 00:33:42 -07:00 committed by Jack Grigg
parent d2b900a0c8
commit c48e35bb66
3 changed files with 186 additions and 45 deletions

View File

@ -10,11 +10,12 @@ from test_framework.util import (
assert_true,
assert_false,
assert_raises_message,
connect_nodes_bi,
get_coinbase_address,
DEFAULT_FEE,
DEFAULT_FEE_ZATS
)
from test_framework.util import wait_and_assert_operationid_status
from test_framework.util import wait_and_assert_operationid_status, start_nodes
from decimal import Decimal
my_memo_str = 'c0ffee' # stay awake
@ -24,6 +25,20 @@ my_memo = my_memo + '0'*(1024-len(my_memo))
no_memo = 'f6' + ('0'*1022) # see section 5.5 of the protocol spec
class ListReceivedTest (BitcoinTestFramework):
def __init__(self):
super().__init__()
self.num_nodes = 3
self.setup_clean_chain = True
def setup_network(self):
self.nodes = start_nodes(
self.num_nodes, self.options.tmpdir,
extra_args=[['-experimentalfeatures', '-orchardwallet']] * self.num_nodes)
connect_nodes_bi(self.nodes, 0, 1)
connect_nodes_bi(self.nodes, 1, 2)
self.is_network_split = False
self.sync_all()
def generate_and_sync(self, new_height):
current_height = self.nodes[0].getblockcount()
assert(new_height > current_height)
@ -40,7 +55,7 @@ class ListReceivedTest (BitcoinTestFramework):
result = self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), zaddr1, 0, 1)
txid_shielding1 = wait_and_assert_operationid_status(self.nodes[0], result['opid'])
zaddrExt = self.nodes[3].z_getnewaddress('sprout')
zaddrExt = self.nodes[2].z_getnewaddress('sprout')
result = self.nodes[0].z_shieldcoinbase(get_coinbase_address(self.nodes[0]), zaddrExt, 0, 1)
txid_shieldingExt = wait_and_assert_operationid_status(self.nodes[0], result['opid'])
@ -72,8 +87,8 @@ class ListReceivedTest (BitcoinTestFramework):
self.nodes[1].z_viewtransaction,
txid_shieldingExt)
# Second transaction should be visible on node3
pt = self.nodes[3].z_viewtransaction(txid_shieldingExt)
# Second transaction should be visible on node0
pt = self.nodes[2].z_viewtransaction(txid_shieldingExt)
assert_equal(pt['txid'], txid_shieldingExt)
assert_equal(len(pt['spends']), 0)
assert_equal(len(pt['outputs']), 1)
@ -190,9 +205,9 @@ class ListReceivedTest (BitcoinTestFramework):
self.generate_and_sync(height+1)
taddr = self.nodes[1].getnewaddress()
zaddr1 = self.nodes[1].z_getnewaddress('sapling')
zaddrExt = self.nodes[3].z_getnewaddress('sapling')
zaddrExt = self.nodes[2].z_getnewaddress('sapling')
self.nodes[0].sendtoaddress(taddr, 4.0)
txid_taddr = self.nodes[0].sendtoaddress(taddr, 4.0)
self.generate_and_sync(height+2)
# Send 1 ZEC to zaddr1
@ -219,6 +234,7 @@ class ListReceivedTest (BitcoinTestFramework):
assert_equal(pt['outputs'][0]['type'], 'sapling')
if pt['outputs'][0]['address'] == zaddr1:
assert_false('jsOutput' in pt['outputs'][0])
assert_equal(pt['outputs'][0]['outgoing'], False)
assert_equal(pt['outputs'][0]['memoStr'], my_memo_str)
else:
@ -380,9 +396,96 @@ class ListReceivedTest (BitcoinTestFramework):
c = self.nodes[1].z_getnotescount(0)
assert_equal(3, c['sapling'], "Count of unconfirmed notes should be 3(2 in zaddr1 + 1 in zaddr2)")
# As part of UA support, a transparent address is now accepted
r = self.nodes[1].z_listreceivedbyaddress(taddr, 0)
assert_equal(len(r), 1)
assert_equal(r[0]['pool'], 'transparent')
assert_equal(r[0]['txid'], txid_taddr)
assert_equal(r[0]['amount'], Decimal('4'))
assert_equal(r[0]['amountZat'], 400000000)
assert_equal(r[0]['confirmations'], 3)
assert r[0]['outindex'] < 2
# Test unified address
node = self.nodes[1]
# Create a unified address on one node, try z_listreceivedbyaddress on another node
account = self.nodes[0].z_getnewaccount()['account']
r = self.nodes[0].z_getaddressforaccount(account)
unified_addr = r['unifiedaddress']
# this address isn't in node1's wallet
assert_raises_message(
JSONRPCException,
"From address does not belong to this node",
node.z_listreceivedbyaddress, unified_addr, 0)
# create a UA on node1
r = node.z_getnewaccount()
account = r['account']
r = node.z_getaddressforaccount(account)
unified_addr = r['unifiedaddress']
receivers = node.z_listunifiedreceivers(unified_addr)
assert_equal(len(receivers), 2)
assert 'transparent' in receivers
assert 'sapling' in receivers
# Wallet contains no notes
r = node.z_listreceivedbyaddress(unified_addr, 0)
assert_equal(len(r), 0, "unified_addr should have received zero notes")
# Create a note in this UA on node1
opid = node.z_sendmany(zaddr1, [{'address': unified_addr, 'amount': 0.1}])
txid_sapling = wait_and_assert_operationid_status(node, opid)
self.generate_and_sync(height+5)
# Create a UTXO that unified_address's transparent component references, on node1
outputs = {receivers['transparent']: 0.2}
txid_taddr = node.sendmany("", outputs)
r = node.z_listreceivedbyaddress(unified_addr, 0)
assert_equal(len(r), 2, "unified_addr should have received 2 payments")
print(r)
outputs = [];
outputs.append({
'pool': r[0]['pool'],
'txid': r[0]['txid'],
'amount': r[0]['amount'],
'amountZat': r[0]['amountZat'],
'change': r[0]['change'] if 'change' in r[0] else None,
'memo': r[0]['memo'] if 'memo' in r[0] else None,
'confirmations': r[0]['confirmations']
})
outputs.append({
'pool': r[1]['pool'],
'txid': r[1]['txid'],
'amount': r[1]['amount'],
'amountZat': r[1]['amountZat'],
'change': r[1]['change'] if 'change' in r[1] else None,
'memo': r[1]['memo'] if 'memo' in r[1] else None,
'confirmations': r[1]['confirmations']
})
assert({
'pool': 'sapling',
'txid': txid_sapling,
'amount': Decimal('0.1'),
'amountZat': 10000000,
'change': False,
'memo': no_memo,
'confirmations': 0,
} in outputs)
assert({
'pool': 'transparent',
'txid': txid_taddr,
'amount': Decimal('0.2'),
'amountZat': 20000000,
'change': None,
'memo': None,
'confirmations': 0,
} in outputs)
def run_test(self):
self.test_received_sprout(200)
self.test_received_sapling(214)
if __name__ == '__main__':
ListReceivedTest().main()

View File

@ -2271,8 +2271,7 @@ UniValue z_listunspent(const UniValue& params, bool fHelp)
" \"type\" : \"sprout|sapling|orchard\", (string) The shielded pool\n"
" \"jsindex\" (sprout) : n, (numeric) the joinsplit index\n"
" \"jsoutindex\" (sprout) : n, (numeric) the output index of the joinsplit\n"
" \"outindex\" (sapling) : n, (numeric) the output index\n"
" \"actionindex\" (orchard) : n, (numeric) the output index\n"
" \"outindex\" (transparent, sapling, orchard) : 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 address is watchonly\n"
" \"address\" : \"address\", (string) the shielded address\n"
@ -3432,6 +3431,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
"2. minconf (numeric, optional, default=1) Only include transactions confirmed at least this many times.\n"
"\nResult (output indices for only one pool will be present):\n"
"{\n"
" \"pool\": \"pool\" (string) one of (\"transparent\", \"sprout\", \"sapling\", \"orchard\")\n"
" \"txid\": \"txid\", (string) the transaction id\n"
" \"amount\": xxxxx, (numeric) the amount of value in the note\n"
" \"amountZat\" : xxxx (numeric) The amount in " + MINOR_CURRENCY_UNIT + "\n"
@ -3442,8 +3442,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
" \"blocktime\": xxx, (numeric) The transaction time in seconds since epoch (midnight Jan 1 1970 GMT).\n"
" \"jsindex\" (sprout) : n, (numeric) the joinsplit index\n"
" \"jsoutindex\" (sprout) : n, (numeric) the output index of the joinsplit\n"
" \"outindex\" (sapling) : n, (numeric) the output index\n"
" \"actionindex\" (orchard) : n, (numeric) the output index\n"
" \"outindex\" (transparent, sapling, orchard) : n, (numeric) the output index for transparent and Sapling outputs, or the action index for Orchard\n"
" \"change\": true|false, (boolean) true if the address that received the note is also one of the sending addresses\n"
"}\n"
"\nExamples:\n"
@ -3460,6 +3459,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
if (nMinDepth < 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Minimum number of confirmations cannot be less than 0");
}
UniValue result(UniValue::VARR);
// Check that the from address is valid.
auto fromaddress = params[0].get_str();
@ -3475,19 +3475,66 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "From address does not belong to this node, zaddr spending key or viewing key not found.");
}
UniValue result(UniValue::VARR);
std::vector<SproutNoteEntry> sproutEntries;
std::vector<SaplingNoteEntry> saplingEntries;
auto noteFilter = AddrSet::ForPaymentAddresses(std::vector({decoded.value()}));
pwalletMain->GetFilteredNotes(sproutEntries, saplingEntries, noteFilter, nMinDepth, INT_MAX, false, false);
auto push_transparent_result = [&](const CTxDestination& dest) -> void {
const CScript scriptPubKey{GetScriptForDestination(dest)};
for (const auto& [_txid, wtx] : pwalletMain->mapWallet) {
if (!CheckFinalTx(wtx))
continue;
int nDepth = wtx.GetDepthInMainChain();
if (nDepth < nMinDepth) continue;
for (size_t i = 0; i < wtx.vout.size(); ++i) {
const CTxOut& txout{wtx.vout[i]};
if (txout.scriptPubKey == scriptPubKey) {
UniValue obj(UniValue::VOBJ);
obj.pushKV("pool", "transparent");
obj.pushKV("txid", wtx.GetHash().ToString());
obj.pushKV("amount", ValueFromAmount(txout.nValue));
obj.pushKV("amountZat", txout.nValue);
obj.pushKV("outindex", int(i));
obj.pushKV("confirmations", nDepth);
result.push_back(obj);
}
}
}
};
auto push_sapling_result = [&](const libzcash::SaplingPaymentAddress& addr) -> void {
bool hasSpendingKey = pwalletMain->HaveSaplingSpendingKeyForAddress(addr);
std::set<std::pair<libzcash::SaplingPaymentAddress, uint256>> nullifierSet;
if (hasSpendingKey) {
nullifierSet = pwalletMain->GetSaplingNullifiers({addr});
}
for (const SaplingNoteEntry& entry : saplingEntries) {
UniValue obj(UniValue::VOBJ);
obj.pushKV("pool", "sapling");
obj.pushKV("txid", entry.op.hash.ToString());
obj.pushKV("amount", ValueFromAmount(CAmount(entry.note.value())));
obj.pushKV("amountZat", CAmount(entry.note.value()));
obj.pushKV("memo", HexStr(entry.memo));
obj.pushKV("outindex", (int)entry.op.n);
obj.pushKV("confirmations", entry.confirmations);
txblock BlockData(entry.op.hash);
obj.pushKV("blockheight", BlockData.height);
obj.pushKV("blockindex", BlockData.index);
obj.pushKV("blocktime", BlockData.time);
if (hasSpendingKey) {
obj.pushKV("change", pwalletMain->IsNoteSaplingChange(nullifierSet, entry.address, entry.op));
}
result.push_back(obj);
}
};
std::visit(match {
[&](const CKeyID& addr) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transparent addresses are not supported by this endpoint.");
},
[&](const CScriptID& addr) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transparent addresses are not supported by this endpoint.");
},
[&](const CKeyID& addr) { push_transparent_result(addr); },
[&](const CScriptID& addr) { push_transparent_result(addr); },
[&](const libzcash::SproutPaymentAddress& addr) {
bool hasSpendingKey = pwalletMain->HaveSproutSpendingKey(addr);
std::set<std::pair<libzcash::SproutPaymentAddress, uint256>> nullifierSet;
@ -3496,6 +3543,7 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
}
for (const SproutNoteEntry& entry : sproutEntries) {
UniValue obj(UniValue::VOBJ);
obj.pushKV("pool", "sprout");
obj.pushKV("txid", entry.jsop.hash.ToString());
obj.pushKV("amount", ValueFromAmount(CAmount(entry.note.value())));
obj.pushKV("amountZat", CAmount(entry.note.value()));
@ -3517,36 +3565,28 @@ UniValue z_listreceivedbyaddress(const UniValue& params, bool fHelp)
}
},
[&](const libzcash::SaplingPaymentAddress& addr) {
bool hasSpendingKey = pwalletMain->HaveSaplingSpendingKeyForAddress(addr);
std::set<std::pair<libzcash::SaplingPaymentAddress, uint256>> nullifierSet;
if (hasSpendingKey) {
nullifierSet = pwalletMain->GetSaplingNullifiers({addr});
}
for (const SaplingNoteEntry& entry : saplingEntries) {
UniValue obj(UniValue::VOBJ);
obj.pushKV("txid", entry.op.hash.ToString());
obj.pushKV("amount", ValueFromAmount(CAmount(entry.note.value())));
obj.pushKV("amountZat", CAmount(entry.note.value()));
obj.pushKV("memo", HexStr(entry.memo));
obj.pushKV("outindex", (int)entry.op.n);
obj.pushKV("confirmations", entry.confirmations);
txblock BlockData(entry.op.hash);
obj.pushKV("blockheight", BlockData.height);
obj.pushKV("blockindex", BlockData.index);
obj.pushKV("blocktime", BlockData.time);
if (hasSpendingKey) {
obj.pushKV("change", pwalletMain->IsNoteSaplingChange(nullifierSet, entry.address, entry.op));
}
result.push_back(obj);
}
push_sapling_result(addr);
},
[&](const libzcash::UnifiedAddress& addr) {
// TODO UNIFIED
for (const auto& receiver : addr) {
std::visit(match {
[&](const libzcash::SaplingPaymentAddress& addr) {
push_sapling_result(addr);
},
[&](const CScriptID& addr) {
CTxDestination dest = addr;
push_transparent_result(dest);
},
[&](const CKeyID& addr) {
CTxDestination dest = addr;
push_transparent_result(dest);
},
[&](const auto& other) { } // TODO orchard
}, receiver);
}
}
}, decoded.value());
return result;
}

View File

@ -398,8 +398,6 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_getbalance)
BOOST_CHECK_THROW(CallRPC("z_listreceivedbyaddress too many args"), runtime_error);
// negative minconf not allowed
BOOST_CHECK_THROW(CallRPC("z_listreceivedbyaddress tmC6YZnCUhm19dEXxh3Jb7srdBJxDawaCab -1"), runtime_error);
// invalid zaddr, taddr not allowed
BOOST_CHECK_THROW(CallRPC("z_listreceivedbyaddress tmC6YZnCUhm19dEXxh3Jb7srdBJxDawaCab 0"), runtime_error);
// don't have the spending key
BOOST_CHECK_THROW(CallRPC("z_listreceivedbyaddress tnRZ8bPq2pff3xBWhTJhNkVUkm2uhzksDeW5PvEa7aFKGT9Qi3YgTALZfjaY4jU3HLVKBtHdSXxoPoLA3naMPcHBcY88FcF 1"), runtime_error);
}