Use transaction builder for asyncrpcoperation_sendmany.
This replaces the old implementation of asyncrpcoperation_sendmany with one where all transaction construction is delegated to the transaction builder. The capabilities of z_sendmany are somewhat modified in the process: * z_sendmany now permits sending funds from a Sprout address to both transparent and Sapling addresses. PRIVACY NOTE: When user sends a Sprout->Sapling transaction, the amount of the transaction is publicly revealed. * z_sendmany no longer supports transactions sending funds into the Sprout pool, with the exception of change amounts when sending from a Sprout address. * When sending transparent coinbase funds to a set of shielded addresses, the amount sent to recipients must fully consume the input value and no change is permitted. This is a slightly weaker constraint than was previously implemented; in the past, only a single shielded recipient was allowed.
This commit is contained in:
parent
005a8624bf
commit
f9477a4499
|
@ -62,7 +62,6 @@ BASE_SCRIPTS= [
|
|||
'reorg_limit.py',
|
||||
'mempool_limit.py',
|
||||
'p2p-fullblocktest.py',
|
||||
'paymentdisclosure.py',
|
||||
# vv Tests less than 30s vv
|
||||
'wallet_1941.py',
|
||||
'wallet_addresses.py',
|
||||
|
|
|
@ -131,10 +131,8 @@ class FinalSaplingRootTest(BitcoinTestFramework):
|
|||
|
||||
# Mine a block with a Sprout shielded tx and verify the final Sapling root does not change
|
||||
zaddr1 = self.nodes[1].z_getnewaddress('sprout')
|
||||
recipients = []
|
||||
recipients.append({"address": zaddr1, "amount": Decimal('10')})
|
||||
myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0)
|
||||
wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr1, 0, 1)
|
||||
wait_and_assert_operationid_status(self.nodes[0], result['opid'])
|
||||
|
||||
self.sync_all()
|
||||
self.nodes[0].generate(1)
|
||||
|
|
|
@ -29,8 +29,8 @@ class MergeToAddressMixedNotes(BitcoinTestFramework):
|
|||
saplingAddr = self.nodes[0].z_getnewaddress('sapling')
|
||||
t_addr = self.nodes[1].getnewaddress()
|
||||
|
||||
opid = self.nodes[0].z_sendmany(coinbase_addr, [{"address": sproutAddr, "amount": Decimal('10')}], 1, 0)
|
||||
wait_and_assert_operationid_status(self.nodes[0], opid)
|
||||
result = self.nodes[0].z_shieldcoinbase(coinbase_addr, sproutAddr, 0, 1)
|
||||
wait_and_assert_operationid_status(self.nodes[0], result['opid'])
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
assert_equal(self.nodes[0].z_getbalance(sproutAddr), Decimal('10'))
|
||||
|
|
|
@ -1,218 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2017 The Zcash developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
|
||||
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.authproxy import JSONRPCException
|
||||
from test_framework.util import assert_equal, \
|
||||
start_node, connect_nodes_bi, wait_and_assert_operationid_status, \
|
||||
get_coinbase_address, DEFAULT_FEE
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
class PaymentDisclosureTest (BitcoinTestFramework):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.num_nodes = 3
|
||||
self.setup_clean_chain = True
|
||||
|
||||
def setup_network(self, split=False):
|
||||
args = ['-debug=zrpcunsafe,paymentdisclosure', '-experimentalfeatures', '-paymentdisclosure', '-txindex=1']
|
||||
self.nodes = []
|
||||
self.nodes.append(start_node(0, self.options.tmpdir, args))
|
||||
self.nodes.append(start_node(1, self.options.tmpdir, args))
|
||||
# node 2 does not enable payment disclosure
|
||||
args2 = ['-debug=zrpcunsafe', '-experimentalfeatures', '-txindex=1']
|
||||
self.nodes.append(start_node(2, self.options.tmpdir, args2))
|
||||
connect_nodes_bi(self.nodes,0,1)
|
||||
connect_nodes_bi(self.nodes,1,2)
|
||||
connect_nodes_bi(self.nodes,0,2)
|
||||
self.is_network_split=False
|
||||
self.sync_all()
|
||||
|
||||
def run_test (self):
|
||||
print("Mining blocks...")
|
||||
|
||||
self.nodes[0].generate(4)
|
||||
self.sync_all()
|
||||
walletinfo = self.nodes[0].getwalletinfo()
|
||||
assert_equal(walletinfo['immature_balance'], 40)
|
||||
assert_equal(walletinfo['balance'], 0)
|
||||
self.sync_all()
|
||||
self.nodes[2].generate(3)
|
||||
self.sync_all()
|
||||
self.nodes[1].generate(101)
|
||||
self.sync_all()
|
||||
assert_equal(self.nodes[0].getbalance(), 40)
|
||||
assert_equal(self.nodes[1].getbalance(), 10)
|
||||
assert_equal(self.nodes[2].getbalance(), 30)
|
||||
|
||||
mytaddr = get_coinbase_address(self.nodes[0])
|
||||
myzaddr = self.nodes[0].z_getnewaddress('sprout')
|
||||
|
||||
# Check that Node 2 has payment disclosure disabled.
|
||||
try:
|
||||
self.nodes[2].z_getpaymentdisclosure("invalidtxid", 0, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("payment disclosure is disabled" in errorString)
|
||||
|
||||
# Check that Node 0 returns an error for an unknown txid
|
||||
try:
|
||||
self.nodes[0].z_getpaymentdisclosure("invalidtxid", 0, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("No information available about transaction" in errorString)
|
||||
|
||||
# Shield coinbase utxos from node 0 of value 40, default fee
|
||||
recipients = [{"address": myzaddr, "amount": Decimal('40.0') - DEFAULT_FEE}]
|
||||
myopid = self.nodes[0].z_sendmany(mytaddr, recipients)
|
||||
txid = wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
|
||||
# Check the tx has joinsplits
|
||||
assert( len(self.nodes[0].getrawtransaction("" + txid, 1)["vjoinsplit"]) > 0 )
|
||||
|
||||
# Sync mempools
|
||||
self.sync_all()
|
||||
|
||||
# Confirm that you can't create a payment disclosure for an unconfirmed tx
|
||||
try:
|
||||
self.nodes[0].z_getpaymentdisclosure(txid, 0, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Transaction has not been confirmed yet" in errorString)
|
||||
|
||||
try:
|
||||
self.nodes[1].z_getpaymentdisclosure(txid, 0, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Transaction has not been confirmed yet" in errorString)
|
||||
|
||||
# Mine tx
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# Confirm that Node 1 cannot create a payment disclosure for a transaction which does not impact its wallet
|
||||
try:
|
||||
self.nodes[1].z_getpaymentdisclosure(txid, 0, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Transaction does not belong to the wallet" in errorString)
|
||||
|
||||
# Check that an invalid joinsplit index is rejected
|
||||
try:
|
||||
self.nodes[0].z_getpaymentdisclosure(txid, 1, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Invalid js_index" in errorString)
|
||||
|
||||
try:
|
||||
self.nodes[0].z_getpaymentdisclosure(txid, -1, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Invalid js_index" in errorString)
|
||||
|
||||
# Check that an invalid output index is rejected
|
||||
try:
|
||||
self.nodes[0].z_getpaymentdisclosure(txid, 0, 2)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Invalid output_index" in errorString)
|
||||
|
||||
try:
|
||||
self.nodes[0].z_getpaymentdisclosure(txid, 0, -1)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Invalid output_index" in errorString)
|
||||
|
||||
# Ask Node 0 to create and validate a payment disclosure for output 0
|
||||
message = "Here is proof of my payment!"
|
||||
pd = self.nodes[0].z_getpaymentdisclosure(txid, 0, 0, message)
|
||||
result = self.nodes[0].z_validatepaymentdisclosure(pd)
|
||||
assert(result["valid"])
|
||||
output_value_sum = Decimal(result["value"])
|
||||
|
||||
# Ask Node 1 to confirm the payment disclosure is valid
|
||||
result = self.nodes[1].z_validatepaymentdisclosure(pd)
|
||||
assert(result["valid"])
|
||||
assert_equal(result["message"], message)
|
||||
assert_equal(result["value"], output_value_sum)
|
||||
|
||||
# Confirm that payment disclosure begins with prefix zpd:
|
||||
assert(pd.startswith("zpd:"))
|
||||
|
||||
# Confirm that payment disclosure without prefix zpd: fails validation
|
||||
try:
|
||||
self.nodes[1].z_validatepaymentdisclosure(pd[4:])
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("payment disclosure prefix not found" in errorString)
|
||||
|
||||
# Check that total value of output index 0 and index 1 should equal shielding amount of 40 less standard fee.
|
||||
pd = self.nodes[0].z_getpaymentdisclosure(txid, 0, 1)
|
||||
result = self.nodes[0].z_validatepaymentdisclosure(pd)
|
||||
output_value_sum += Decimal(result["value"])
|
||||
assert_equal(output_value_sum, Decimal('40.0') - DEFAULT_FEE)
|
||||
|
||||
# Create a z->z transaction, sending shielded funds from node 0 to node 1
|
||||
node1zaddr = self.nodes[1].z_getnewaddress('sprout')
|
||||
recipients = [{"address":node1zaddr, "amount":Decimal('1')}]
|
||||
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
|
||||
txid = wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
self.sync_all()
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# Confirm that Node 0 can create a valid payment disclosure
|
||||
pd = self.nodes[0].z_getpaymentdisclosure(txid, 0, 0, "a message of your choice")
|
||||
result = self.nodes[0].z_validatepaymentdisclosure(pd)
|
||||
assert(result["valid"])
|
||||
|
||||
# Confirm that Node 1, even as recipient of shielded funds, cannot create a payment disclosure
|
||||
# as the transaction was created by Node 0 and Node 1's payment disclosure database does not
|
||||
# contain the necessary data to do so, where the data would only have been available on Node 0
|
||||
# when executing z_shieldcoinbase.
|
||||
try:
|
||||
self.nodes[1].z_getpaymentdisclosure(txid, 0, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Could not find payment disclosure info for the given joinsplit output" in errorString)
|
||||
|
||||
# Payment disclosures cannot be created for transparent transactions.
|
||||
txid = self.nodes[2].sendtoaddress(mytaddr, 1.0)
|
||||
self.sync_all()
|
||||
|
||||
# No matter the type of transaction, if it has not been confirmed, it is ignored.
|
||||
try:
|
||||
self.nodes[0].z_getpaymentdisclosure(txid, 0, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Transaction has not been confirmed yet" in errorString)
|
||||
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# Confirm that a payment disclosure can only be generated for a shielded transaction.
|
||||
try:
|
||||
self.nodes[0].z_getpaymentdisclosure(txid, 0, 0)
|
||||
assert(False)
|
||||
except JSONRPCException as e:
|
||||
errorString = e.error['message']
|
||||
assert("Transaction is not a shielded transaction" in errorString)
|
||||
|
||||
if __name__ == '__main__':
|
||||
PaymentDisclosureTest().main()
|
|
@ -59,14 +59,6 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
|
|||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# Create taddr -> Sprout transaction and mine on node 0 before it is Canopy-aware. Should pass
|
||||
sendmany_tx_0 = self.nodes[0].z_sendmany(taddr_0, [{"address": self.nodes[1].z_getnewaddress('sprout'), "amount": 1}])
|
||||
wait_and_assert_operationid_status(self.nodes[0], sendmany_tx_0)
|
||||
print("taddr -> Sprout z_sendmany tx accepted before Canopy on node 0")
|
||||
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# Create mergetoaddress taddr -> Sprout transaction and mine on node 0 before it is Canopy-aware. Should pass
|
||||
merge_tx_0 = self.nodes[0].z_mergetoaddress(["ANY_TADDR"], self.nodes[1].z_getnewaddress('sprout'))
|
||||
wait_and_assert_operationid_status(self.nodes[0], merge_tx_0['opid'])
|
||||
|
@ -75,7 +67,7 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
|
|||
# Mine to one block before Canopy activation on node 0; adding value
|
||||
# to the Sprout pool will fail now since the transaction must be
|
||||
# included in the next (or later) block, after Canopy has activated.
|
||||
self.nodes[0].generate(4)
|
||||
self.nodes[0].generate(5)
|
||||
self.sync_all()
|
||||
|
||||
# Shield coinbase to Sprout on node 0. Should fail
|
||||
|
@ -91,7 +83,7 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
|
|||
sprout_addr = self.nodes[1].z_getnewaddress('sprout')
|
||||
assert_raises_message(
|
||||
JSONRPCException,
|
||||
"Sprout shielding is not supported after Canopy",
|
||||
"Sending funds into the Sprout pool is not supported by z_sendmany",
|
||||
self.nodes[0].z_sendmany,
|
||||
taddr_0, [{"address": sprout_addr, "amount": 1}])
|
||||
print("taddr -> Sprout z_sendmany tx rejected at Canopy activation on node 0")
|
||||
|
|
|
@ -158,8 +158,8 @@ class SproutSaplingMigration(BitcoinTestFramework):
|
|||
|
||||
def send_to_sprout_zaddr(self, tAddr, sproutAddr):
|
||||
# Send some ZEC to a Sprout address
|
||||
opid = self.nodes[0].z_sendmany(tAddr, [{"address": sproutAddr, "amount": Decimal('10')}], 1, 0)
|
||||
wait_and_assert_operationid_status(self.nodes[0], opid)
|
||||
result = self.nodes[0].z_shieldcoinbase(tAddr, sproutAddr, 0, 1)
|
||||
wait_and_assert_operationid_status(self.nodes[0], result['opid'])
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
fail,
|
||||
get_coinbase_address,
|
||||
start_node, start_nodes,
|
||||
sync_blocks, sync_mempools,
|
||||
|
@ -88,9 +89,14 @@ class TurnstileTest (BitcoinTestFramework):
|
|||
# Node 0 shields some funds
|
||||
dest_addr = self.nodes[0].z_getnewaddress(POOL_NAME.lower())
|
||||
taddr0 = get_coinbase_address(self.nodes[0])
|
||||
recipients = []
|
||||
recipients.append({"address": dest_addr, "amount": Decimal('10')})
|
||||
myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0)
|
||||
if (POOL_NAME == "SPROUT"):
|
||||
myopid = self.nodes[0].z_shieldcoinbase(taddr0, dest_addr, 0, 1)['opid']
|
||||
elif (POOL_NAME == "SAPLING"):
|
||||
recipients = []
|
||||
recipients.append({"address": dest_addr, "amount": Decimal('10')})
|
||||
myopid = self.nodes[0].z_sendmany(taddr0, recipients, 1, 0)
|
||||
else:
|
||||
fail("Unrecognized pool name: " + POOL_NAME)
|
||||
wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
self.sync_all()
|
||||
self.nodes[0].generate(1)
|
||||
|
|
|
@ -81,15 +81,11 @@ class WalletChangeAddressesTest(BitcoinTestFramework):
|
|||
|
||||
taddr = self.nodes[0].getnewaddress()
|
||||
saplingAddr = self.nodes[0].z_getnewaddress('sapling')
|
||||
sproutAddr = self.nodes[0].z_getnewaddress('sprout')
|
||||
|
||||
print()
|
||||
print('Checking z_sendmany(taddr->Sapling)')
|
||||
check_change_taddr_reuse(saplingAddr)
|
||||
print()
|
||||
print('Checking z_sendmany(taddr->Sprout)')
|
||||
check_change_taddr_reuse(sproutAddr)
|
||||
print()
|
||||
print('Checking z_sendmany(taddr->taddr)')
|
||||
check_change_taddr_reuse(taddr)
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
get_coinbase_address,
|
||||
wait_and_assert_operationid_status,
|
||||
DEFAULT_FEE
|
||||
)
|
||||
|
@ -20,7 +19,6 @@ class WalletListNotes(BitcoinTestFramework):
|
|||
# Current height = 200
|
||||
assert_equal(200, self.nodes[0].getblockcount())
|
||||
sproutzaddr = self.nodes[0].z_getnewaddress('sprout')
|
||||
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']))
|
||||
|
@ -30,11 +28,10 @@ class WalletListNotes(BitcoinTestFramework):
|
|||
self.sync_all()
|
||||
assert_equal(201, self.nodes[0].getblockcount())
|
||||
|
||||
# Shield coinbase funds (must be a multiple of 10, no change allowed)
|
||||
receive_amount_10 = Decimal('10.0') - DEFAULT_FEE
|
||||
recipients = [{"address":sproutzaddr, "amount":receive_amount_10}]
|
||||
myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients)
|
||||
txid_1 = wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
# Shield one coinbase output
|
||||
receive_amount_1 = Decimal('10.0') - DEFAULT_FEE
|
||||
result = self.nodes[0].z_shieldcoinbase('*', sproutzaddr, DEFAULT_FEE, 1)
|
||||
txid_1 = wait_and_assert_operationid_status(self.nodes[0], result['opid'])
|
||||
self.sync_all()
|
||||
|
||||
# No funds (with (default) one or more confirmations) in sproutzaddr yet
|
||||
|
@ -51,7 +48,7 @@ class WalletListNotes(BitcoinTestFramework):
|
|||
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'])
|
||||
assert_equal(receive_amount_1, 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])
|
||||
|
@ -64,12 +61,12 @@ class WalletListNotes(BitcoinTestFramework):
|
|||
# Current height = 202
|
||||
assert_equal(202, self.nodes[0].getblockcount())
|
||||
|
||||
# Send 1.0 minus default fee from sproutzaddr to a new zaddr
|
||||
sproutzaddr2 = self.nodes[0].z_getnewaddress('sprout')
|
||||
receive_amount_1 = Decimal('1.0') - DEFAULT_FEE
|
||||
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}]
|
||||
# Send 1.0 minus default fee from sproutzaddr to a new Sapling zaddr
|
||||
saplingzaddr = self.nodes[0].z_getnewaddress('sapling')
|
||||
receive_amount_2 = Decimal('1.0')
|
||||
change_amount_2 = receive_amount_1 - receive_amount_2 - DEFAULT_FEE
|
||||
assert_equal('sapling', self.nodes[0].z_validateaddress(saplingzaddr)['type'])
|
||||
recipients = [{"address": saplingzaddr, "amount":receive_amount_2}]
|
||||
myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients)
|
||||
txid_2 = wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
self.sync_all()
|
||||
|
@ -82,16 +79,16 @@ class WalletListNotes(BitcoinTestFramework):
|
|||
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(saplingzaddr, unspent_tx[0]['address'])
|
||||
assert_equal(receive_amount_2, 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'])
|
||||
assert_equal(change_amount_2, unspent_tx[1]['amount'])
|
||||
|
||||
unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, [sproutzaddr2])
|
||||
unspent_tx_filter = self.nodes[0].z_listunspent(0, 9999, False, [saplingzaddr])
|
||||
assert_equal(1, len(unspent_tx_filter))
|
||||
assert_equal(unspent_tx[0], unspent_tx_filter[0])
|
||||
|
||||
|
@ -99,15 +96,15 @@ class WalletListNotes(BitcoinTestFramework):
|
|||
assert_equal(1, len(unspent_tx_filter))
|
||||
assert_equal(unspent_tx[1], unspent_tx_filter[0])
|
||||
|
||||
# No funds in saplingzaddr yet
|
||||
assert_equal(0, len(self.nodes[0].z_listunspent(0, 9999, False, [saplingzaddr])))
|
||||
self.nodes[0].generate(1)
|
||||
self.sync_all()
|
||||
|
||||
# Send 2.0 minus default fee 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') - DEFAULT_FEE
|
||||
recipients = [{"address": saplingzaddr, "amount":receive_amount_2}]
|
||||
myopid = self.nodes[0].z_sendmany(get_coinbase_address(self.nodes[0]), recipients)
|
||||
# Send 2.0 minus default fee to a new sapling zaddr
|
||||
saplingzaddr2 = self.nodes[0].z_getnewaddress('sapling')
|
||||
receive_amount_3 = Decimal('2.0')
|
||||
change_amount_3 = change_amount_2 - receive_amount_3 - DEFAULT_FEE
|
||||
recipients = [{"address": saplingzaddr2, "amount":receive_amount_3}]
|
||||
myopid = self.nodes[0].z_sendmany(sproutzaddr, recipients)
|
||||
txid_3 = wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
self.sync_all()
|
||||
unspent_tx = self.nodes[0].z_listunspent(0)
|
||||
|
@ -119,31 +116,31 @@ class WalletListNotes(BitcoinTestFramework):
|
|||
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(saplingzaddr, unspent_tx[0]['address'])
|
||||
assert_equal(receive_amount_2, 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(saplingzaddr2, unspent_tx[1]['address'])
|
||||
assert_equal(receive_amount_3, unspent_tx[1]['amount'])
|
||||
|
||||
assert_equal(True, unspent_tx[2]['change'])
|
||||
assert_equal(txid_2, unspent_tx[2]['txid'])
|
||||
assert_equal(txid_3, 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'])
|
||||
assert_equal(change_amount_3, 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])
|
||||
assert_equal(unspent_tx[0], 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[0], 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
|
||||
|
|
|
@ -226,7 +226,7 @@ class ListReceivedTest (BitcoinTestFramework):
|
|||
assert_equal(3, c[release], "Count of unconfirmed notes should be 3(2 in zaddr1 + 1 in zaddr2)")
|
||||
|
||||
def run_test(self):
|
||||
self.run_test_release('sprout', 200)
|
||||
#self.run_test_release('sprout', 200)
|
||||
self.run_test_release('sapling', 214)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -72,10 +72,8 @@ class WalletOverwinterTxTest (BitcoinTestFramework):
|
|||
|
||||
# Node 0 shields to Node 2, a coinbase utxo of value 10.0 less default fee
|
||||
zsendamount = Decimal('10.0') - DEFAULT_FEE
|
||||
recipients = []
|
||||
recipients.append({"address":zaddr2, "amount": zsendamount})
|
||||
myopid = self.nodes[0].z_sendmany(taddr0, recipients)
|
||||
txid_shielded = wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr2, DEFAULT_FEE, 1)
|
||||
txid_shielded = wait_and_assert_operationid_status(self.nodes[0], result['opid'])
|
||||
|
||||
# Skip over the three blocks prior to activation; no transactions can be mined
|
||||
# in them due to the nearly-expiring restrictions.
|
||||
|
@ -143,10 +141,8 @@ class WalletOverwinterTxTest (BitcoinTestFramework):
|
|||
|
||||
# Node 0 shields to Node 3, a coinbase utxo of value 10.0 less default fee
|
||||
zsendamount = Decimal('10.0') - DEFAULT_FEE
|
||||
recipients = []
|
||||
recipients.append({"address":zaddr3, "amount": zsendamount})
|
||||
myopid = self.nodes[0].z_sendmany(taddr0, recipients)
|
||||
txid_shielded = wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||
result = self.nodes[0].z_shieldcoinbase(taddr0, zaddr3, DEFAULT_FEE, 1)
|
||||
txid_shielded = wait_and_assert_operationid_status(self.nodes[0], result['opid'])
|
||||
|
||||
# Mine the first Blossom block
|
||||
self.sync_all()
|
||||
|
|
|
@ -169,7 +169,7 @@ class WalletSaplingTest(BitcoinTestFramework):
|
|||
)
|
||||
raise AssertionError("Should have thrown an exception")
|
||||
except JSONRPCException as e:
|
||||
assert_equal("Cannot send to both Sprout and Sapling addresses using z_sendmany", e.error['message'])
|
||||
assert_equal("Sending funds into the Sprout pool is not supported by z_sendmany", e.error['message'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletSaplingTest().main()
|
||||
|
|
|
@ -26,7 +26,7 @@ class WalletSendManyAnyTaddr(BitcoinTestFramework):
|
|||
self.nodes[3],
|
||||
self.nodes[3].z_sendmany('ANY_TADDR', [{'address': recipient, 'amount': 100}]),
|
||||
'failed',
|
||||
'Could not find any non-coinbase UTXOs to spend. Coinbase UTXOs can only be sent to a single zaddr recipient from a single taddr.',
|
||||
'Insufficient funds: have 0.00, need 100.00001; if you are attempting to shield transparent coinbase funds, ensure that you have specified only a single recipient address.',
|
||||
)
|
||||
|
||||
# Prepare some non-coinbase UTXOs
|
||||
|
|
|
@ -126,12 +126,12 @@ TransactionBuilderResult::TransactionBuilderResult(const CTransaction& tx) : may
|
|||
|
||||
TransactionBuilderResult::TransactionBuilderResult(const std::string& error) : maybeError(error) {}
|
||||
|
||||
bool TransactionBuilderResult::IsTx() { return maybeTx != std::nullopt; }
|
||||
bool TransactionBuilderResult::IsTx() { return maybeTx.has_value(); }
|
||||
|
||||
bool TransactionBuilderResult::IsError() { return maybeError != std::nullopt; }
|
||||
bool TransactionBuilderResult::IsError() { return maybeError.has_value(); }
|
||||
|
||||
CTransaction TransactionBuilderResult::GetTxOrThrow() {
|
||||
if (maybeTx) {
|
||||
if (maybeTx.has_value()) {
|
||||
return maybeTx.value();
|
||||
} else {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Failed to build transaction: " + GetError());
|
||||
|
@ -139,7 +139,7 @@ CTransaction TransactionBuilderResult::GetTxOrThrow() {
|
|||
}
|
||||
|
||||
std::string TransactionBuilderResult::GetError() {
|
||||
if (maybeError) {
|
||||
if (maybeError.has_value()) {
|
||||
return maybeError.value();
|
||||
} else {
|
||||
// This can only happen if isTx() is true in which case we should not call getError()
|
||||
|
@ -216,7 +216,7 @@ void TransactionBuilder::AddSaplingOutput(
|
|||
|
||||
libzcash::Zip212Enabled zip_212_enabled = libzcash::Zip212Enabled::BeforeZip212;
|
||||
// We use nHeight = chainActive.Height() + 1 since the output will be included in the next block
|
||||
if (Params().GetConsensus().NetworkUpgradeActive(nHeight, Consensus::UPGRADE_CANOPY)) {
|
||||
if (consensusParams.NetworkUpgradeActive(nHeight, Consensus::UPGRADE_CANOPY)) {
|
||||
zip_212_enabled = libzcash::Zip212Enabled::AfterZip212;
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -24,52 +24,72 @@
|
|||
#include <rust/ed25519/types.h>
|
||||
|
||||
using namespace libzcash;
|
||||
class TxValues;
|
||||
|
||||
class FromAnyTaddr {
|
||||
public:
|
||||
friend bool operator==(const FromAnyTaddr &a, const FromAnyTaddr &b) { return true; }
|
||||
};
|
||||
|
||||
typedef std::variant<FromAnyTaddr, PaymentAddress> PaymentSource;
|
||||
|
||||
class SendManyRecipient {
|
||||
public:
|
||||
std::string address;
|
||||
PaymentAddress address;
|
||||
CAmount amount;
|
||||
std::string memo;
|
||||
std::optional<std::string> memo;
|
||||
|
||||
SendManyRecipient(std::string address_, CAmount amount_, std::string memo_) :
|
||||
SendManyRecipient(PaymentAddress address_, CAmount amount_, std::optional<std::string> memo_) :
|
||||
address(address_), amount(amount_), memo(memo_) {}
|
||||
};
|
||||
|
||||
class SendManyInputJSOP {
|
||||
class SpendableInputs {
|
||||
public:
|
||||
JSOutPoint point;
|
||||
SproutNote note;
|
||||
CAmount amount;
|
||||
std::vector<COutput> utxos;
|
||||
std::vector<SproutNoteEntry> sproutNoteEntries;
|
||||
std::vector<SaplingNoteEntry> saplingNoteEntries;
|
||||
|
||||
SendManyInputJSOP(JSOutPoint point_, SproutNote note_, CAmount amount_) :
|
||||
point(point_), note(note_), amount(amount_) {}
|
||||
};
|
||||
/**
|
||||
* Selectively discard notes that are not required to obtain the desired
|
||||
* amount. Returns `false` if the available inputs do not add up to the
|
||||
* desired amount.
|
||||
*/
|
||||
bool LimitToAmount(CAmount amount);
|
||||
|
||||
// Package of info which is passed to perform_joinsplit methods.
|
||||
struct AsyncJoinSplitInfo
|
||||
{
|
||||
std::vector<JSInput> vjsin;
|
||||
std::vector<JSOutput> vjsout;
|
||||
std::vector<SproutNote> notes;
|
||||
CAmount vpub_old = 0;
|
||||
CAmount vpub_new = 0;
|
||||
};
|
||||
/**
|
||||
* Compute the total ZEC amount of spendable inputs.
|
||||
*/
|
||||
CAmount Total() const {
|
||||
CAmount result = 0;
|
||||
for (const auto& t : utxos) {
|
||||
result += t.Value();
|
||||
}
|
||||
for (const auto& t : sproutNoteEntries) {
|
||||
result += t.note.value();
|
||||
}
|
||||
for (const auto& t : saplingNoteEntries) {
|
||||
result += t.note.value();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// A struct to help us track the witness and anchor for a given JSOutPoint
|
||||
struct WitnessAnchorData {
|
||||
std::optional<SproutWitness> witness;
|
||||
uint256 anchor;
|
||||
/**
|
||||
* Return whether or not the set of selected UTXOs contains
|
||||
* coinbase outputs.
|
||||
*/
|
||||
bool HasTransparentCoinbase() const;
|
||||
|
||||
/**
|
||||
* List spendable inputs in zrpcunsafe log entries.
|
||||
*/
|
||||
void LogInputs(const AsyncRPCOperationId& id) const;
|
||||
};
|
||||
|
||||
class AsyncRPCOperation_sendmany : public AsyncRPCOperation {
|
||||
public:
|
||||
AsyncRPCOperation_sendmany(
|
||||
std::optional<TransactionBuilder> builder,
|
||||
CMutableTransaction contextualTx,
|
||||
std::string fromAddress,
|
||||
std::vector<SendManyRecipient> tOutputs,
|
||||
std::vector<SendManyRecipient> zOutputs,
|
||||
TransactionBuilder builder,
|
||||
PaymentSource paymentSource,
|
||||
std::vector<SendManyRecipient> recipients,
|
||||
int minDepth,
|
||||
CAmount fee = DEFAULT_FEE,
|
||||
UniValue contextInfo = NullUniValue);
|
||||
|
@ -85,64 +105,26 @@ public:
|
|||
|
||||
virtual UniValue getStatus() const;
|
||||
|
||||
bool testmode = false; // Set to true to disable sending txs and generating proofs
|
||||
|
||||
bool paymentDisclosureMode = false; // Set to true to save esk for encrypted notes in payment disclosure database.
|
||||
bool testmode{false}; // Set to true to disable sending txs and generating proofs
|
||||
|
||||
private:
|
||||
friend class TEST_FRIEND_AsyncRPCOperation_sendmany; // class for unit testing
|
||||
|
||||
TransactionBuilder builder_;
|
||||
PaymentSource paymentSource_;
|
||||
std::vector<SendManyRecipient> recipients_;
|
||||
int mindepth_{1};
|
||||
CAmount fee_;
|
||||
UniValue contextinfo_; // optional data to include in return value from getStatus()
|
||||
|
||||
bool isUsingBuilder_{false}; // Indicates that no Sprout addresses are involved
|
||||
uint32_t consensusBranchId_;
|
||||
CAmount fee_;
|
||||
int mindepth_{1};
|
||||
std::string fromaddress_;
|
||||
bool useanyutxo_{false};
|
||||
bool isfromtaddr_{false};
|
||||
bool isfromzaddr_{false};
|
||||
CTxDestination fromtaddr_;
|
||||
PaymentAddress frompaymentaddress_;
|
||||
|
||||
Ed25519VerificationKey joinSplitPubKey_;
|
||||
Ed25519SigningKey joinSplitPrivKey_;
|
||||
SpendableInputs FindSpendableInputs(bool fAcceptCoinbase);
|
||||
|
||||
// The key is the result string from calling JSOutPoint::ToString()
|
||||
std::unordered_map<std::string, WitnessAnchorData> jsopWitnessAnchorMap;
|
||||
|
||||
std::vector<SendManyRecipient> t_outputs_;
|
||||
std::vector<SendManyRecipient> z_outputs_;
|
||||
std::vector<COutput> t_inputs_;
|
||||
std::vector<SendManyInputJSOP> z_sprout_inputs_;
|
||||
std::vector<SaplingNoteEntry> z_sapling_inputs_;
|
||||
|
||||
TransactionBuilder builder_;
|
||||
CTransaction tx_;
|
||||
|
||||
void add_taddr_change_output_to_tx(CReserveKey& keyChange, CAmount amount);
|
||||
void add_taddr_outputs_to_tx();
|
||||
bool find_unspent_notes();
|
||||
bool find_utxos(bool fAcceptCoinbase, TxValues& txValues);
|
||||
// Load transparent inputs into the transaction or the transactionBuilder (in case of have it)
|
||||
bool load_inputs(TxValues& txValues);
|
||||
std::array<unsigned char, ZC_MEMO_SIZE> get_memo_from_hex_string(std::string s);
|
||||
bool main_impl();
|
||||
|
||||
// JoinSplit without any input notes to spend
|
||||
UniValue perform_joinsplit(AsyncJoinSplitInfo &);
|
||||
|
||||
// JoinSplit with input notes to spend (JSOutPoints))
|
||||
UniValue perform_joinsplit(AsyncJoinSplitInfo &, std::vector<JSOutPoint> & );
|
||||
|
||||
// JoinSplit where you have the witnesses and anchor
|
||||
UniValue perform_joinsplit(
|
||||
AsyncJoinSplitInfo & info,
|
||||
std::vector<std::optional < SproutWitness>> witnesses,
|
||||
uint256 anchor);
|
||||
|
||||
// payment disclosure!
|
||||
std::vector<PaymentDisclosureKeyInfo> paymentDisclosureData_;
|
||||
uint256 main_impl();
|
||||
};
|
||||
|
||||
|
||||
|
@ -153,52 +135,14 @@ public:
|
|||
|
||||
TEST_FRIEND_AsyncRPCOperation_sendmany(std::shared_ptr<AsyncRPCOperation_sendmany> ptr) : delegate(ptr) {}
|
||||
|
||||
CTransaction getTx() {
|
||||
return delegate->tx_;
|
||||
}
|
||||
|
||||
void setTx(CTransaction tx) {
|
||||
delegate->tx_ = tx;
|
||||
}
|
||||
|
||||
// Delegated methods
|
||||
|
||||
void add_taddr_change_output_to_tx(CReserveKey& keyChange, CAmount amount) {
|
||||
delegate->add_taddr_change_output_to_tx(keyChange, amount);
|
||||
}
|
||||
|
||||
void add_taddr_outputs_to_tx() {
|
||||
delegate->add_taddr_outputs_to_tx();
|
||||
}
|
||||
|
||||
bool find_unspent_notes() {
|
||||
return delegate->find_unspent_notes();
|
||||
}
|
||||
|
||||
std::array<unsigned char, ZC_MEMO_SIZE> get_memo_from_hex_string(std::string s) {
|
||||
return delegate->get_memo_from_hex_string(s);
|
||||
}
|
||||
|
||||
bool main_impl() {
|
||||
uint256 main_impl() {
|
||||
return delegate->main_impl();
|
||||
}
|
||||
|
||||
UniValue perform_joinsplit(AsyncJoinSplitInfo &info) {
|
||||
return delegate->perform_joinsplit(info);
|
||||
}
|
||||
|
||||
UniValue perform_joinsplit(AsyncJoinSplitInfo &info, std::vector<JSOutPoint> &v ) {
|
||||
return delegate->perform_joinsplit(info, v);
|
||||
}
|
||||
|
||||
UniValue perform_joinsplit(
|
||||
AsyncJoinSplitInfo & info,
|
||||
std::vector<std::optional < SproutWitness>> witnesses,
|
||||
uint256 anchor)
|
||||
{
|
||||
return delegate->perform_joinsplit(info, witnesses, anchor);
|
||||
}
|
||||
|
||||
void set_state(OperationStatus state) {
|
||||
delegate->state_.store(state);
|
||||
}
|
||||
|
|
|
@ -3667,7 +3667,6 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO
|
|||
return ret;
|
||||
}
|
||||
|
||||
|
||||
// JSDescription size depends on the transaction version
|
||||
#define V3_JS_DESCRIPTION_SIZE (GetSerializeSize(JSDescription(), SER_NETWORK, (OVERWINTER_TX_VERSION | (1 << 31))))
|
||||
// Here we define the maximum number of zaddr outputs that can be included in a transaction.
|
||||
|
@ -3680,6 +3679,82 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO
|
|||
#define CTXIN_SPEND_DUST_SIZE 148
|
||||
#define CTXOUT_REGULAR_SIZE 34
|
||||
|
||||
size_t EstimateTxSize(
|
||||
const PaymentSource& paymentSource,
|
||||
const std::vector<SendManyRecipient>& recipients) {
|
||||
int nextBlockHeight = chainActive.Height() + 1;
|
||||
CMutableTransaction mtx;
|
||||
mtx.fOverwintered = true;
|
||||
mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID;
|
||||
mtx.nVersion = SAPLING_TX_VERSION;
|
||||
|
||||
bool fromTaddr = std::visit(match {
|
||||
[&](const FromAnyTaddr& any) {
|
||||
return true;
|
||||
},
|
||||
[&](const PaymentAddress& addr) {
|
||||
return std::visit(match {
|
||||
[&](const CKeyID& keyId) {
|
||||
return true;
|
||||
},
|
||||
[&](const CScriptID& scriptId) {
|
||||
return true;
|
||||
},
|
||||
[&](const libzcash::SproutPaymentAddress& addr) {
|
||||
return false;
|
||||
},
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
return false;
|
||||
},
|
||||
[&](const libzcash::UnifiedAddress& addr) {
|
||||
// TODO UA
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Unified addresses not yet supported.");
|
||||
return false; // compiler is dumb
|
||||
}
|
||||
}, addr);
|
||||
}
|
||||
}, paymentSource);
|
||||
|
||||
// As a sanity check, estimate and verify that the size of the transaction will be valid.
|
||||
// Depending on the input notes, the actual tx size may turn out to be larger and perhaps invalid.
|
||||
size_t txsize = 0;
|
||||
size_t taddrRecipientCount = 0;
|
||||
for (const SendManyRecipient& recipient : recipients) {
|
||||
std::visit(match {
|
||||
[&](const CKeyID&) {
|
||||
taddrRecipientCount += 1;
|
||||
},
|
||||
[&](const CScriptID&) {
|
||||
taddrRecipientCount += 1;
|
||||
},
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
mtx.vShieldedOutput.push_back(OutputDescription());
|
||||
},
|
||||
[&](const libzcash::SproutPaymentAddress& addr) {
|
||||
JSDescription jsdesc;
|
||||
if (mtx.fOverwintered && (mtx.nVersion >= SAPLING_TX_VERSION)) {
|
||||
jsdesc.proof = GrothProof();
|
||||
}
|
||||
mtx.vJoinSplit.push_back(jsdesc);
|
||||
},
|
||||
[&](const libzcash::UnifiedAddress& ua) {
|
||||
// FIXME
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Unified addresses not yet supported.");
|
||||
}
|
||||
}, recipient.address);
|
||||
}
|
||||
|
||||
CTransaction tx(mtx);
|
||||
txsize += GetSerializeSize(tx, SER_NETWORK, tx.nVersion);
|
||||
if (fromTaddr) {
|
||||
txsize += CTXIN_SPEND_DUST_SIZE;
|
||||
txsize += CTXOUT_REGULAR_SIZE; // There will probably be taddr change
|
||||
}
|
||||
txsize += CTXOUT_REGULAR_SIZE * taddrRecipientCount;
|
||||
|
||||
return txsize;
|
||||
}
|
||||
|
||||
UniValue z_sendmany(const UniValue& params, bool fHelp)
|
||||
{
|
||||
if (!EnsureWalletIsAvailable(fHelp))
|
||||
|
@ -3720,15 +3795,16 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
|
|||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
ThrowIfInitialBlockDownload();
|
||||
KeyIO keyIO(Params());
|
||||
|
||||
// Check that the from address is valid.
|
||||
auto fromaddress = params[0].get_str();
|
||||
PaymentSource paymentSource;
|
||||
bool fromTaddr = false;
|
||||
bool fromSapling = false;
|
||||
bool fromSprout = false;
|
||||
KeyIO keyIO(Params());
|
||||
bool fromSapling = false;
|
||||
if (fromaddress == "ANY_TADDR") {
|
||||
fromTaddr = true;
|
||||
paymentSource = FromAnyTaddr();
|
||||
} else {
|
||||
auto addr = keyIO.DecodePaymentAddress(fromaddress);
|
||||
if (!addr.has_value()) {
|
||||
|
@ -3737,13 +3813,6 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
|
|||
"Invalid from address: should be a taddr, a zaddr, or the string 'ANY_TADDR'.");
|
||||
}
|
||||
|
||||
// This is a sanity check; the actual checks will come later when the spend is attempted.
|
||||
if (!std::visit(HaveSpendingKeyForPaymentAddress(pwalletMain), addr.value())) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_ADDRESS_OR_KEY,
|
||||
"Invalid from address: does not belong to this node, spending key not found.");
|
||||
}
|
||||
|
||||
// Remember what sort of address this is
|
||||
std::visit(match {
|
||||
[&](const CKeyID&) {
|
||||
|
@ -3764,190 +3833,80 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
|
|||
"Invalid from address: unified addresses are not yet supported.");
|
||||
}
|
||||
}, addr.value());
|
||||
|
||||
paymentSource = addr.value();
|
||||
}
|
||||
|
||||
UniValue outputs = params[1].get_array();
|
||||
if (outputs.size()==0)
|
||||
if (outputs.size() == 0) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amounts array is empty.");
|
||||
}
|
||||
|
||||
// Keep track of addresses to spot duplicates
|
||||
set<std::string> setAddress;
|
||||
|
||||
// Track whether we see any Sprout addresses
|
||||
bool noSproutAddrs = !fromSprout;
|
||||
|
||||
// Recipients
|
||||
std::vector<SendManyRecipient> taddrRecipients;
|
||||
std::vector<SendManyRecipient> zaddrRecipients;
|
||||
std::vector<SendManyRecipient> recipients;
|
||||
CAmount nTotalOut = 0;
|
||||
|
||||
bool containsSproutOutput = false;
|
||||
bool containsSaplingOutput = false;
|
||||
|
||||
for (const UniValue& o : outputs.getValues()) {
|
||||
if (!o.isObject())
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected object");
|
||||
|
||||
// sanity check, report error if unknown key-value pairs
|
||||
for (const string& name_ : o.getKeys()) {
|
||||
std::string s = name_;
|
||||
if (s != "address" && s != "amount" && s!="memo")
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown key: ")+s);
|
||||
for (const std::string& s : o.getKeys()) {
|
||||
if (s != "address" && s != "amount" && s != "memo")
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown key: ") + s);
|
||||
}
|
||||
|
||||
string address = find_value(o, "address").get_str();
|
||||
|
||||
bool isZaddr = false;
|
||||
auto toAddr = keyIO.DecodePaymentAddress(address);
|
||||
if (toAddr.has_value()) {
|
||||
bool toSprout = false;
|
||||
bool toSapling = false;
|
||||
std::visit(match {
|
||||
[&](const CKeyID&) { },
|
||||
[&](const CScriptID&) { },
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
isZaddr = true;
|
||||
toSapling = true;
|
||||
containsSaplingOutput = true;
|
||||
},
|
||||
[&](const libzcash::SproutPaymentAddress& addr) {
|
||||
isZaddr = true;
|
||||
toSprout = true;
|
||||
containsSproutOutput = true;
|
||||
noSproutAddrs = false;
|
||||
},
|
||||
[&](const libzcash::UnifiedAddress& ua) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_ADDRESS_OR_KEY,
|
||||
"Invalid recipient address: unified addresses are not yet supported.");
|
||||
}
|
||||
}, toAddr.value());
|
||||
|
||||
// Sending to both Sprout and Sapling is currently unsupported using z_sendmany
|
||||
if (containsSproutOutput && containsSaplingOutput) {
|
||||
std::string addrStr = find_value(o, "address").get_str();
|
||||
auto addr = keyIO.DecodePaymentAddress(addrStr);
|
||||
if (addr.has_value()) {
|
||||
// TODO: If we want to continue to support sending to Sprout, we'll simply relax the
|
||||
// restriction here to allow sprout->sprout; these transfers will not be forbidden
|
||||
// by later code.
|
||||
bool toSprout = std::holds_alternative<libzcash::SproutPaymentAddress>(addr.value());
|
||||
if (toSprout) { // && !fromSprout) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
"Cannot send to both Sprout and Sapling addresses using z_sendmany");
|
||||
}
|
||||
|
||||
// If sending between shielded addresses, they must be the same type
|
||||
if ((fromSprout && toSapling) || (fromSapling && toSprout)) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
"Cannot send between Sprout and Sapling addresses using z_sendmany");
|
||||
}
|
||||
|
||||
int nextBlockHeight = chainActive.Height() + 1;
|
||||
|
||||
if (fromTaddr && toSprout) {
|
||||
const bool canopyActive = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_CANOPY);
|
||||
if (canopyActive) {
|
||||
throw JSONRPCError(RPC_VERIFY_REJECTED, "Sprout shielding is not supported after Canopy");
|
||||
}
|
||||
"Sending funds into the Sprout pool is not supported by z_sendmany");
|
||||
}
|
||||
} else {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown address format: ")+address );
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
std::string("Invalid parameter, unknown address format: ") + addrStr);
|
||||
}
|
||||
|
||||
if (setAddress.count(address))
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated address: ")+address);
|
||||
setAddress.insert(address);
|
||||
|
||||
UniValue memoValue = find_value(o, "memo");
|
||||
string memo;
|
||||
std::optional<std::string> memo;
|
||||
if (!memoValue.isNull()) {
|
||||
memo = memoValue.get_str();
|
||||
if (!isZaddr) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Memo cannot be used with a taddr. It can only be used with a zaddr.");
|
||||
} else if (!IsHex(memo)) {
|
||||
if (!std::visit(libzcash::HasShieldedRecipient(), addr.value())) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, memos cannot be sent to transparent addresses.");
|
||||
} else if (!IsHex(memo.value())) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected memo data in hexadecimal format.");
|
||||
}
|
||||
if (memo.length() > ZC_MEMO_SIZE*2) {
|
||||
|
||||
if (memo.value().length() > ZC_MEMO_SIZE*2) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, size of memo is larger than maximum allowed %d", ZC_MEMO_SIZE ));
|
||||
}
|
||||
}
|
||||
|
||||
UniValue av = find_value(o, "amount");
|
||||
CAmount nAmount = AmountFromValue( av );
|
||||
if (nAmount < 0)
|
||||
if (nAmount < 0) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive");
|
||||
|
||||
if (isZaddr) {
|
||||
zaddrRecipients.push_back( SendManyRecipient(address, nAmount, memo) );
|
||||
} else {
|
||||
taddrRecipients.push_back( SendManyRecipient(address, nAmount, memo) );
|
||||
}
|
||||
|
||||
recipients.push_back(SendManyRecipient(addr.value(), nAmount, memo) );
|
||||
nTotalOut += nAmount;
|
||||
}
|
||||
|
||||
int nextBlockHeight = chainActive.Height() + 1;
|
||||
CMutableTransaction mtx;
|
||||
mtx.fOverwintered = true;
|
||||
mtx.nVersionGroupId = SAPLING_VERSION_GROUP_ID;
|
||||
mtx.nVersion = SAPLING_TX_VERSION;
|
||||
unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING;
|
||||
if (!Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING)) {
|
||||
if (Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_OVERWINTER)) {
|
||||
mtx.nVersionGroupId = OVERWINTER_VERSION_GROUP_ID;
|
||||
mtx.nVersion = OVERWINTER_TX_VERSION;
|
||||
} else {
|
||||
mtx.fOverwintered = false;
|
||||
mtx.nVersion = 2;
|
||||
}
|
||||
|
||||
max_tx_size = MAX_TX_SIZE_BEFORE_SAPLING;
|
||||
|
||||
// Check the number of zaddr outputs does not exceed the limit.
|
||||
if (zaddrRecipients.size() > Z_SENDMANY_MAX_ZADDR_OUTPUTS_BEFORE_SAPLING) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, too many zaddr outputs");
|
||||
}
|
||||
// If Sapling is not active, do not allow sending from or sending to Sapling addresses.
|
||||
if (fromSapling || containsSaplingOutput) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, Sapling has not activated");
|
||||
}
|
||||
if (recipients.empty()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "No recipients");
|
||||
}
|
||||
|
||||
// As a sanity check, estimate and verify that the size of the transaction will be valid.
|
||||
// Depending on the input notes, the actual tx size may turn out to be larger and perhaps invalid.
|
||||
size_t txsize = 0;
|
||||
for (int i = 0; i < zaddrRecipients.size(); i++) {
|
||||
auto address = zaddrRecipients[i].address;
|
||||
auto decoded = keyIO.DecodePaymentAddress(address);
|
||||
|
||||
if (decoded.has_value()) {
|
||||
std::visit(match {
|
||||
[&](const CKeyID&) {
|
||||
// Handled elsewhere
|
||||
},
|
||||
[&](const CScriptID&) {
|
||||
// Handled elsewhere
|
||||
},
|
||||
[&](const libzcash::SaplingPaymentAddress& addr) {
|
||||
mtx.vShieldedOutput.push_back(OutputDescription());
|
||||
},
|
||||
[&](const libzcash::SproutPaymentAddress& addr) {
|
||||
JSDescription jsdesc;
|
||||
if (mtx.fOverwintered && (mtx.nVersion >= SAPLING_TX_VERSION)) {
|
||||
jsdesc.proof = GrothProof();
|
||||
}
|
||||
mtx.vJoinSplit.push_back(jsdesc);
|
||||
},
|
||||
[&](const libzcash::UnifiedAddress& ua) {
|
||||
// TODO UNIFIED
|
||||
}
|
||||
}, decoded.value());
|
||||
}
|
||||
}
|
||||
CTransaction tx(mtx);
|
||||
txsize += GetSerializeSize(tx, SER_NETWORK, tx.nVersion);
|
||||
if (fromTaddr) {
|
||||
txsize += CTXIN_SPEND_DUST_SIZE;
|
||||
txsize += CTXOUT_REGULAR_SIZE; // There will probably be taddr change
|
||||
}
|
||||
txsize += CTXOUT_REGULAR_SIZE * taddrRecipients.size();
|
||||
if (txsize > max_tx_size) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Too many outputs, size of raw transaction would be larger than limit of %d bytes", max_tx_size ));
|
||||
// Sanity check for transaction size
|
||||
// TODO: move this to the builder?
|
||||
auto txsize = EstimateTxSize(paymentSource, recipients);
|
||||
if (txsize > MAX_TX_SIZE_AFTER_SAPLING) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
strprintf("Too many outputs, size of raw transaction would be larger than limit of %d bytes", MAX_TX_SIZE_AFTER_SAPLING));
|
||||
}
|
||||
|
||||
// Minimum confirmations
|
||||
|
@ -3960,9 +3919,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
|
|||
}
|
||||
|
||||
// Fee in Zatoshis, not currency format)
|
||||
CAmount nFee = DEFAULT_FEE;
|
||||
CAmount nDefaultFee = nFee;
|
||||
|
||||
CAmount nFee = DEFAULT_FEE;
|
||||
if (params.size() > 3) {
|
||||
if (params[3].get_real() == 0.0) {
|
||||
nFee = 0;
|
||||
|
@ -3971,16 +3928,20 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
|
|||
}
|
||||
|
||||
// Check that the user specified fee is not absurd.
|
||||
// This allows amount=0 (and all amount < nDefaultFee) transactions to use the default network fee
|
||||
// or anything less than nDefaultFee instead of being forced to use a custom fee and leak metadata
|
||||
if (nTotalOut < nDefaultFee) {
|
||||
if (nFee > nDefaultFee) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Small transaction amount %s has fee %s that is greater than the default fee %s", FormatMoney(nTotalOut), FormatMoney(nFee), FormatMoney(nDefaultFee)));
|
||||
// This allows amount=0 (and all amount < DEFAULT_FEE) transactions to use the default network fee
|
||||
// or anything less than DEFAULT_FEE instead of being forced to use a custom fee and leak metadata
|
||||
if (nTotalOut < DEFAULT_FEE) {
|
||||
if (nFee > DEFAULT_FEE) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
strprintf("Small transaction amount %s has fee %s that is greater than the default fee %s", FormatMoney(nTotalOut), FormatMoney(nFee), FormatMoney(DEFAULT_FEE)));
|
||||
}
|
||||
} else {
|
||||
// Check that the user specified fee is not absurd.
|
||||
if (nFee > nTotalOut) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Fee %s is greater than the sum of outputs %s and also greater than the default fee", FormatMoney(nFee), FormatMoney(nTotalOut)));
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER,
|
||||
strprintf("Fee %s is greater than the sum of outputs %s and also greater than the default fee", FormatMoney(nFee), FormatMoney(nTotalOut)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3993,33 +3954,14 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
|
|||
o.pushKV("fee", std::stod(FormatMoney(nFee)));
|
||||
UniValue contextInfo = o;
|
||||
|
||||
if (!fromTaddr || !zaddrRecipients.empty()) {
|
||||
// We have shielded inputs or outputs, and therefore cannot create
|
||||
// transactions before Sapling activates.
|
||||
if (!Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_SAPLING)) {
|
||||
throw JSONRPCError(
|
||||
RPC_INVALID_PARAMETER, "Cannot create shielded transactions before Sapling has activated");
|
||||
}
|
||||
}
|
||||
|
||||
// Builder (used if Sapling addresses are involved)
|
||||
std::optional<TransactionBuilder> builder;
|
||||
if (noSproutAddrs) {
|
||||
builder = TransactionBuilder(Params().GetConsensus(), nextBlockHeight, pwalletMain);
|
||||
}
|
||||
|
||||
// Contextual transaction we will build on
|
||||
// (used if no Sapling addresses are involved)
|
||||
CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction(
|
||||
Params().GetConsensus(), nextBlockHeight, !noSproutAddrs);
|
||||
bool isShielded = !fromTaddr || zaddrRecipients.size() > 0;
|
||||
if (contextualTx.nVersion == 1 && isShielded) {
|
||||
contextualTx.nVersion = 2; // Tx format should support vJoinSplits
|
||||
}
|
||||
int nextBlockHeight = chainActive.Height() + 1;
|
||||
TransactionBuilder builder(Params().GetConsensus(), nextBlockHeight, pwalletMain);
|
||||
|
||||
// Create operation and add to global queue
|
||||
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(builder, contextualTx, fromaddress, taddrRecipients, zaddrRecipients, nMinDepth, nFee, contextInfo) );
|
||||
std::shared_ptr<AsyncRPCOperation> operation(
|
||||
new AsyncRPCOperation_sendmany(builder, paymentSource, recipients, nMinDepth, nFee, contextInfo)
|
||||
);
|
||||
q->addOperation(operation);
|
||||
AsyncRPCOperationId operationId = operation->getId();
|
||||
return operationId;
|
||||
|
|
|
@ -1177,41 +1177,12 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters)
|
|||
// Mutable tx containing contextual information we need to build tx
|
||||
UniValue retValue = CallRPC("getblockcount");
|
||||
int nHeight = retValue.get_int();
|
||||
CMutableTransaction mtx = CreateNewContextualCMutableTransaction(Params().GetConsensus(), nHeight + 1);
|
||||
if (mtx.nVersion == 1) {
|
||||
mtx.nVersion = 2;
|
||||
}
|
||||
|
||||
// Test constructor of AsyncRPCOperation_sendmany
|
||||
try {
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::nullopt, mtx, "",{}, {}, -1));
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK( find_error(objError, "Minconf cannot be negative"));
|
||||
}
|
||||
TransactionBuilder builder(Params().GetConsensus(), nHeight + 1, pwalletMain);
|
||||
|
||||
try {
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::nullopt, mtx, "",{}, {}, 1));
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK( find_error(objError, "From address parameter missing"));
|
||||
}
|
||||
|
||||
try {
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "tmRr6yJonqGK23UVhrKuyvTpF8qxQQjKigJ", {}, {}, 1) );
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK( find_error(objError, "No recipients"));
|
||||
}
|
||||
|
||||
try {
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient("dummy", 1*COIN, "") };
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "INVALID", recipients, {}, 1) );
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK( find_error(objError, "Invalid from address"));
|
||||
}
|
||||
|
||||
// Testnet payment addresses begin with 'zt'. This test detects an incorrect prefix.
|
||||
try {
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient("dummy", 1*COIN, "") };
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "zcMuhvq8sEkHALuSU2i4NbNQxshSAYrpCExec45ZjtivYPbuiFPwk6WHy4SvsbeZ4siy1WheuRGjtaJmoD1J8bFqNXhsG6U", recipients, {}, 1) );
|
||||
libzcash::UnifiedAddress ua; //dummy
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient(ua, 1*COIN, std::nullopt) };
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, ua, recipients, 1));
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK( find_error(objError, "Invalid from address"));
|
||||
}
|
||||
|
@ -1219,8 +1190,11 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_parameters)
|
|||
// Note: The following will crash as a google test because AsyncRPCOperation_sendmany
|
||||
// invokes a method on pwalletMain, which is undefined in the google test environment.
|
||||
try {
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient("dummy", 1*COIN, "") };
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, "ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP", recipients, {}, 1) );
|
||||
KeyIO keyIO(Params());
|
||||
auto sender = keyIO.DecodePaymentAddress("ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP").value();
|
||||
libzcash::UnifiedAddress ua; //dummy
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient(ua, 1*COIN, std::nullopt) };
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, sender, recipients, 1));
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK( find_error(objError, "no spending key found for address"));
|
||||
}
|
||||
|
@ -1243,6 +1217,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
|
|||
{
|
||||
SelectParams(CBaseChainParams::TESTNET);
|
||||
const Consensus::Params& consensusParams = Params().GetConsensus();
|
||||
KeyIO keyIO(Params());
|
||||
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
|
@ -1252,54 +1227,55 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
|
|||
// We removed the ability to create pre-Sapling Sprout proofs, so we can
|
||||
// only create Sapling-onwards transactions.
|
||||
int nHeight = consensusParams.vUpgrades[Consensus::UPGRADE_SAPLING].nActivationHeight;
|
||||
CMutableTransaction mtx = CreateNewContextualCMutableTransaction(consensusParams, nHeight + 1);
|
||||
if (mtx.nVersion == 1) {
|
||||
mtx.nVersion = 2;
|
||||
}
|
||||
|
||||
// add keys manually
|
||||
BOOST_CHECK_NO_THROW(retValue = CallRPC("getnewaddress"));
|
||||
std::string taddr1 = retValue.get_str();
|
||||
auto pa = pwalletMain->GenerateNewSproutZKey();
|
||||
KeyIO keyIO(Params());
|
||||
std::string zaddr1 = keyIO.EncodePaymentAddress(pa);
|
||||
auto taddr1 = keyIO.DecodePaymentAddress(retValue.get_str()).value();
|
||||
|
||||
if (!pwalletMain->HaveHDSeed()) {
|
||||
pwalletMain->GenerateNewSeed();
|
||||
}
|
||||
auto zaddr1 = pwalletMain->GenerateNewSaplingZKey();
|
||||
|
||||
// there are no utxos to spend
|
||||
{
|
||||
TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain);
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") };
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, taddr1, {}, recipients, 1) );
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, taddr1, recipients, 1));
|
||||
operation->main();
|
||||
BOOST_CHECK(operation->isFailed());
|
||||
std::string msg = operation->getErrorMessage();
|
||||
BOOST_CHECK( msg.find("Insufficient transparent funds") != string::npos);
|
||||
BOOST_CHECK( msg.find("Insufficient funds") != string::npos);
|
||||
}
|
||||
|
||||
// minconf cannot be zero when sending from zaddr
|
||||
{
|
||||
TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain);
|
||||
try {
|
||||
std::vector<SendManyRecipient> recipients = {SendManyRecipient(taddr1, 100*COIN, "DEADBEEF")};
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 0));
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, zaddr1, recipients, 0));
|
||||
BOOST_CHECK(false); // Fail test if an exception is not thrown
|
||||
} catch (const UniValue& objError) {
|
||||
BOOST_CHECK(find_error(objError, "Minconf cannot be zero when sending from zaddr"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// there are no unspent notes to spend
|
||||
{
|
||||
TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain);
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient(taddr1, 100*COIN, "DEADBEEF") };
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) );
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, zaddr1, recipients, 1));
|
||||
operation->main();
|
||||
BOOST_CHECK(operation->isFailed());
|
||||
std::string msg = operation->getErrorMessage();
|
||||
BOOST_CHECK( msg.find("Insufficient funds, no unspent notes") != string::npos);
|
||||
BOOST_CHECK(msg.find("Insufficient funds, have 0.00") != string::npos);
|
||||
}
|
||||
|
||||
// get_memo_from_hex_string())
|
||||
{
|
||||
TransactionBuilder builder(consensusParams, nHeight + 1, pwalletMain);
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") };
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) );
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, zaddr1, recipients, 1));
|
||||
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
|
||||
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
|
||||
|
||||
|
@ -1345,106 +1321,6 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
|
|||
BOOST_CHECK( find_error(objError, "hexadecimal format"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// add_taddr_change_output_to_tx() will append a vout to a raw transaction
|
||||
{
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 100*COIN, "DEADBEEF") };
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) );
|
||||
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
|
||||
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
|
||||
|
||||
CTransaction tx = proxy.getTx();
|
||||
BOOST_CHECK(tx.vout.size() == 0);
|
||||
|
||||
CReserveKey keyChange(pwalletMain);
|
||||
CAmount amount = 12345600000;
|
||||
proxy.add_taddr_change_output_to_tx(keyChange, amount);
|
||||
tx = proxy.getTx();
|
||||
BOOST_CHECK(tx.vout.size() == 1);
|
||||
CTxOut out = tx.vout[0];
|
||||
BOOST_CHECK_EQUAL(out.nValue, amount);
|
||||
|
||||
amount = 111100000;
|
||||
proxy.add_taddr_change_output_to_tx(keyChange, amount);
|
||||
tx = proxy.getTx();
|
||||
BOOST_CHECK(tx.vout.size() == 2);
|
||||
out = tx.vout[1];
|
||||
BOOST_CHECK_EQUAL(out.nValue, amount);
|
||||
}
|
||||
|
||||
// add_taddr_outputs_to_tx() will append many vouts to a raw transaction
|
||||
{
|
||||
std::vector<SendManyRecipient> recipients = {
|
||||
SendManyRecipient("tmTGScYwiLMzHe4uGZtBYmuqoW4iEoYNMXt", 123000000, ""),
|
||||
SendManyRecipient("tmUSbHz3vxnwLvRyNDXbwkZxjVyDodMJEhh", 456000000, ""),
|
||||
SendManyRecipient("tmYZAXYPCP56Xa5JQWWPZuK7o7bfUQW6kkd", 789000000, ""),
|
||||
};
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, recipients, {}, 1) );
|
||||
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
|
||||
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
|
||||
|
||||
proxy.add_taddr_outputs_to_tx();
|
||||
|
||||
CTransaction tx = proxy.getTx();
|
||||
BOOST_CHECK(tx.vout.size() == 3);
|
||||
BOOST_CHECK_EQUAL(tx.vout[0].nValue, 123000000);
|
||||
BOOST_CHECK_EQUAL(tx.vout[1].nValue, 456000000);
|
||||
BOOST_CHECK_EQUAL(tx.vout[2].nValue, 789000000);
|
||||
}
|
||||
|
||||
// Test the perform_joinsplit methods.
|
||||
{
|
||||
// Dummy input so the operation object can be instantiated.
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 50000, "ABCD") };
|
||||
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(std::nullopt, mtx, zaddr1, {}, recipients, 1) );
|
||||
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
|
||||
TEST_FRIEND_AsyncRPCOperation_sendmany proxy(ptr);
|
||||
|
||||
// Enable test mode so tx is not sent and proofs are not generated
|
||||
static_cast<AsyncRPCOperation_sendmany *>(operation.get())->testmode = true;
|
||||
|
||||
AsyncJoinSplitInfo info;
|
||||
std::vector<std::optional < SproutWitness>> witnesses;
|
||||
uint256 anchor;
|
||||
try {
|
||||
proxy.perform_joinsplit(info, witnesses, anchor);
|
||||
} catch (const std::runtime_error & e) {
|
||||
BOOST_CHECK( string(e.what()).find("anchor is null")!= string::npos);
|
||||
}
|
||||
|
||||
try {
|
||||
std::vector<JSOutPoint> v;
|
||||
proxy.perform_joinsplit(info, v);
|
||||
} catch (const std::runtime_error & e) {
|
||||
BOOST_CHECK( string(e.what()).find("anchor is null")!= string::npos);
|
||||
}
|
||||
|
||||
info.notes.push_back(SproutNote());
|
||||
try {
|
||||
proxy.perform_joinsplit(info);
|
||||
} catch (const std::runtime_error & e) {
|
||||
BOOST_CHECK( string(e.what()).find("number of notes")!= string::npos);
|
||||
}
|
||||
|
||||
info.notes.clear();
|
||||
info.vjsin.push_back(JSInput());
|
||||
info.vjsin.push_back(JSInput());
|
||||
info.vjsin.push_back(JSInput());
|
||||
try {
|
||||
proxy.perform_joinsplit(info);
|
||||
} catch (const std::runtime_error & e) {
|
||||
BOOST_CHECK( string(e.what()).find("unsupported joinsplit input")!= string::npos);
|
||||
}
|
||||
|
||||
info.vjsin.clear();
|
||||
try {
|
||||
proxy.perform_joinsplit(info);
|
||||
} catch (const std::runtime_error & e) {
|
||||
BOOST_CHECK( string(e.what()).find("error verifying joinsplit")!= string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1463,9 +1339,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling)
|
|||
KeyIO keyIO(Params());
|
||||
// add keys manually
|
||||
auto taddr = pwalletMain->GenerateNewKey().GetID();
|
||||
std::string taddr1 = keyIO.EncodeDestination(taddr);
|
||||
auto pa = pwalletMain->GenerateNewSaplingZKey();
|
||||
std::string zaddr1 = keyIO.EncodePaymentAddress(pa);
|
||||
|
||||
const Consensus::Params& consensusParams = Params().GetConsensus();
|
||||
retValue = CallRPC("getblockcount");
|
||||
|
@ -1498,8 +1372,8 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_taddr_to_sapling)
|
|||
auto builder = TransactionBuilder(consensusParams, nextBlockHeight, pwalletMain);
|
||||
mtx = CreateNewContextualCMutableTransaction(consensusParams, nextBlockHeight);
|
||||
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient(zaddr1, 1*COIN, "ABCD") };
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(builder, mtx, taddr1, {}, recipients, 0) );
|
||||
std::vector<SendManyRecipient> recipients = { SendManyRecipient(pa, 1*COIN, "ABCD") };
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(builder, taddr, recipients, 1));
|
||||
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);
|
||||
|
||||
// Enable test mode so tx is not sent
|
||||
|
@ -1613,8 +1487,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet_encrypted_wallet_sapzkeys)
|
|||
UniValue retValue;
|
||||
int n = 100;
|
||||
|
||||
if(!pwalletMain->HaveHDSeed())
|
||||
{
|
||||
if (!pwalletMain->HaveHDSeed()) {
|
||||
pwalletMain->GenerateNewSeed();
|
||||
}
|
||||
|
||||
|
|
|
@ -5084,6 +5084,12 @@ bool CWallet::HasSpendingKeys(const AddrSet& addrSet) const {
|
|||
* Find notes in the wallet filtered by payment addresses, min depth, max depth,
|
||||
* if the note is spent, if a spending key is required, and if the notes are locked.
|
||||
* These notes are decrypted and added to the output parameter vector, outEntries.
|
||||
*
|
||||
* For the `noteFilter` argument, `std::nullopt` will return every address; if a
|
||||
* value is provided, all returned notes will correspond to the addresses in
|
||||
* that address set. If the empty address set is provided, this function will
|
||||
* return early and the return arguments `sproutEntries` and `saplingEntries`
|
||||
* will be unmodified.
|
||||
*/
|
||||
void CWallet::GetFilteredNotes(
|
||||
std::vector<SproutNoteEntry>& sproutEntries,
|
||||
|
@ -5095,6 +5101,10 @@ void CWallet::GetFilteredNotes(
|
|||
bool requireSpendingKey,
|
||||
bool ignoreLocked)
|
||||
{
|
||||
// Don't bother to do anything if the note filter would reject all notes
|
||||
if (noteFilter.has_value() && noteFilter.value().IsEmpty())
|
||||
return;
|
||||
|
||||
LOCK2(cs_main, cs_wallet);
|
||||
|
||||
KeyIO keyIO(Params());
|
||||
|
|
|
@ -630,6 +630,7 @@ private:
|
|||
|
||||
AddrSet() {}
|
||||
public:
|
||||
static AddrSet Empty() { return AddrSet(); }
|
||||
static AddrSet ForPaymentAddresses(const std::vector<libzcash::PaymentAddress>& addrs);
|
||||
|
||||
const std::set<libzcash::SproutPaymentAddress>& GetSproutAddresses() const {
|
||||
|
@ -640,6 +641,10 @@ public:
|
|||
return saplingAddresses;
|
||||
}
|
||||
|
||||
bool IsEmpty() const {
|
||||
return sproutAddresses.empty() && saplingAddresses.empty();
|
||||
}
|
||||
|
||||
bool HasSproutAddress(libzcash::SproutPaymentAddress addr) const {
|
||||
return sproutAddresses.count(addr) > 0;
|
||||
}
|
||||
|
|
|
@ -133,6 +133,17 @@ typedef std::variant<
|
|||
SproutSpendingKey,
|
||||
SaplingExtendedSpendingKey> SpendingKey;
|
||||
|
||||
class HasShieldedRecipient {
|
||||
public:
|
||||
bool operator()(const CKeyID& p2pkh) { return false; }
|
||||
bool operator()(const CScriptID& p2sh) { return false; }
|
||||
bool operator()(const SproutPaymentAddress& addr) { return true; }
|
||||
bool operator()(const SaplingPaymentAddress& addr) { return true; }
|
||||
// unified addresses must contain a shielded receiver, so we
|
||||
// consider this to be safe by construction
|
||||
bool operator()(const UnifiedAddress& addr) { return true; }
|
||||
};
|
||||
|
||||
class AddressInfoFromSpendingKey {
|
||||
public:
|
||||
std::pair<std::string, PaymentAddress> operator()(const SproutSpendingKey&) const;
|
||||
|
|
Loading…
Reference in New Issue