Merge pull request #6377 from sellout/wallet_tx_builder/z_sendmany

Extract common transaction logic from z_sendmany
This commit is contained in:
Greg Pfeil 2023-03-22 11:24:33 -06:00 committed by GitHub
commit 666a8d1b37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1436 additions and 890 deletions

View File

@ -21,6 +21,9 @@ RPC Changes
also available for testnet and regtest nodes, in which case it does not also available for testnet and regtest nodes, in which case it does not
return end-of-service halt information (as testnet and regtest nodes do not return end-of-service halt information (as testnet and regtest nodes do not
have an end-of-service halt feature.) have an end-of-service halt feature.)
- Several `z_sendmany` failures have moved from synchronous to asynchronous, so
while you should already be checking the async operation status, there are now
more cases that may trigger failure at that stage.
[Deprecations](https://zcash.github.io/zcash/user/deprecation.html) [Deprecations](https://zcash.github.io/zcash/user/deprecation.html)
-------------- --------------

View File

@ -34,6 +34,7 @@ class MempoolUpgradeActivationTest(BitcoinTestFramework):
"-debug=mempool", "-debug=mempool",
"-blockmaxsize=4000", "-blockmaxsize=4000",
'-allowdeprecated=getnewaddress', '-allowdeprecated=getnewaddress',
'-allowdeprecated=legacy_privacy',
'-allowdeprecated=z_getnewaddress', '-allowdeprecated=z_getnewaddress',
'-allowdeprecated=z_getbalance', '-allowdeprecated=z_getbalance',
nuparams(BLOSSOM_BRANCH_ID, 200), nuparams(BLOSSOM_BRANCH_ID, 200),

View File

@ -21,7 +21,7 @@ from test_framework.util import (
import logging import logging
HAS_CANOPY = [ HAS_CANOPY = [
'-nurejectoldversions=false', '-nurejectoldversions=false',
'-anchorconfirmations=1', '-anchorconfirmations=1',
nuparams(BLOSSOM_BRANCH_ID, 205), nuparams(BLOSSOM_BRANCH_ID, 205),
nuparams(HEARTWOOD_BRANCH_ID, 210), nuparams(HEARTWOOD_BRANCH_ID, 210),
@ -88,11 +88,9 @@ class RemoveSproutShieldingTest (BitcoinTestFramework):
# Create taddr -> Sprout z_sendmany transaction on node 0. Should fail # Create taddr -> Sprout z_sendmany transaction on node 0. Should fail
sprout_addr = self.nodes[1].z_getnewaddress('sprout') sprout_addr = self.nodes[1].z_getnewaddress('sprout')
assert_raises_message( recipients = [{"address": sprout_addr, "amount": Decimal('1')}]
JSONRPCException, myopid = self.nodes[0].z_sendmany(taddr_0, recipients, 1, 0, 'AllowRevealedSenders')
"Sending funds into the Sprout value pool is not supported by z_sendmany", wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Sending funds into the Sprout pool is no longer supported.")
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") print("taddr -> Sprout z_sendmany tx rejected at Canopy activation on node 0")
# Create z_mergetoaddress [taddr, Sprout] -> Sprout transaction on node 0. Should fail # Create z_mergetoaddress [taddr, Sprout] -> Sprout transaction on node 0. Should fail

View File

@ -4,7 +4,6 @@
# file COPYING or https://www.opensource.org/licenses/mit-license.php . # file COPYING or https://www.opensource.org/licenses/mit-license.php .
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.authproxy import JSONRPCException
from test_framework.util import ( from test_framework.util import (
assert_equal, assert_equal,
get_coinbase_address, get_coinbase_address,
@ -170,16 +169,13 @@ class WalletSaplingTest(BitcoinTestFramework):
# Make sure we get a useful error when trying to send to both sprout and sapling # Make sure we get a useful error when trying to send to both sprout and sapling
node4_sproutaddr = self.nodes[3].z_getnewaddress('sprout') node4_sproutaddr = self.nodes[3].z_getnewaddress('sprout')
node4_saplingaddr = self.nodes[3].z_getnewaddress('sapling') node4_saplingaddr = self.nodes[3].z_getnewaddress('sapling')
try: myopid = self.nodes[1].z_sendmany(
self.nodes[1].z_sendmany( taddr1,
taddr1, [{'address': node4_sproutaddr, 'amount': Decimal('2.5')},
[{'address': node4_sproutaddr, 'amount': Decimal('2.5')}, {'address': node4_saplingaddr, 'amount': Decimal('2.5') - DEFAULT_FEE}],
{'address': node4_saplingaddr, 'amount': Decimal('2.5') - DEFAULT_FEE}], 1, DEFAULT_FEE, 'AllowRevealedSenders'
1, DEFAULT_FEE, 'AllowRevealedSenders' )
) wait_and_assert_operationid_status(self.nodes[1], myopid, "failed", "Sending funds into the Sprout pool is no longer supported.")
raise AssertionError("Should have thrown an exception")
except JSONRPCException as e:
assert_equal("Sending funds into the Sprout value pool is not supported by z_sendmany", e.error['message'])
if __name__ == '__main__': if __name__ == '__main__':
WalletSaplingTest().main() WalletSaplingTest().main()

View File

@ -112,7 +112,7 @@ class WalletSendManyAnyTaddr(BitcoinTestFramework):
'ANY_TADDR', 'ANY_TADDR',
[{'address': recipient, 'amount': 20}], [{'address': recipient, 'amount': 20}],
1, DEFAULT_FEE, 'AllowRevealedSenders') 1, DEFAULT_FEE, 'AllowRevealedSenders')
wait_and_assert_operationid_status(self.nodes[3], myopid, "failed", "Insufficient funds: have 14.99998, need 20.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR or if any transparent recipients are included.") wait_and_assert_operationid_status(self.nodes[3], myopid, "failed", "Insufficient funds: have 14.99998, need 20.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
# Create an expired transaction on node 3. # Create an expired transaction on node 3.
self.split_network() self.split_network()
@ -144,7 +144,7 @@ class WalletSendManyAnyTaddr(BitcoinTestFramework):
'ANY_TADDR', 'ANY_TADDR',
[{'address': recipient, 'amount': 13}], [{'address': recipient, 'amount': 13}],
1, DEFAULT_FEE, 'AllowRevealedSenders'), 1, DEFAULT_FEE, 'AllowRevealedSenders'),
"failed", "Insufficient funds: have 0.00, need 13.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR or if any transparent recipients are included.") "failed", "Insufficient funds: have 0.00, need 13.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
if __name__ == '__main__': if __name__ == '__main__':
WalletSendManyAnyTaddr().main() WalletSendManyAnyTaddr().main()

View File

@ -38,6 +38,7 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
'-regtestshieldcoinbase', '-regtestshieldcoinbase',
'-debug=zrpcunsafe', '-debug=zrpcunsafe',
'-allowdeprecated=getnewaddress', '-allowdeprecated=getnewaddress',
'-allowdeprecated=legacy_privacy',
'-allowdeprecated=z_getnewaddress', '-allowdeprecated=z_getnewaddress',
'-allowdeprecated=z_getbalance', '-allowdeprecated=z_getbalance',
'-allowdeprecated=z_gettotalbalance', '-allowdeprecated=z_gettotalbalance',
@ -232,7 +233,7 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
amount = Decimal('10.0') - DEFAULT_FEE - Decimal('0.00000001') # this leaves change at 1 zatoshi less than dust threshold amount = Decimal('10.0') - DEFAULT_FEE - Decimal('0.00000001') # this leaves change at 1 zatoshi less than dust threshold
recipients.append({"address":self.nodes[0].getnewaddress(), "amount":amount }) recipients.append({"address":self.nodes[0].getnewaddress(), "amount":amount })
myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1) myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 10.00, need 0.00000053 more to avoid creating invalid change output 0.00000001 (dust threshold is 0.00000054); note that coinbase outputs will not be selected if you specify ANY_TADDR or if any transparent recipients are included.") wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 10.00, need 0.00000053 more to avoid creating invalid change output 0.00000001 (dust threshold is 0.00000054); note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
# Send will fail because send amount is too big, even when including coinbase utxos # Send will fail because send amount is too big, even when including coinbase utxos
errorString = "" errorString = ""
@ -246,9 +247,9 @@ class WalletShieldingCoinbaseTest (BitcoinTestFramework):
recipients = [] recipients = []
recipients.append({"address":self.nodes[1].getnewaddress(), "amount":Decimal('10000.0')}) recipients.append({"address":self.nodes[1].getnewaddress(), "amount":Decimal('10000.0')})
myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1) myopid = self.nodes[0].z_sendmany(mytaddr, recipients, 1)
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 10.00, need 10000.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR or if any transparent recipients are included.") wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 10.00, need 10000.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, DEFAULT_FEE, 'AllowRevealedRecipients') myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, DEFAULT_FEE, 'AllowRevealedRecipients')
wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 9.99998, need 10000.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR or if any transparent recipients are included.") wait_and_assert_operationid_status(self.nodes[0], myopid, "failed", "Insufficient funds: have 9.99998, need 10000.00001; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.")
# Send will fail because of insufficient funds unless sender uses coinbase utxos # Send will fail because of insufficient funds unless sender uses coinbase utxos
try: try:

View File

@ -183,9 +183,10 @@ class WalletZSendmanyTest(BitcoinTestFramework):
# If we attempt to spend with the default privacy policy, z_sendmany # If we attempt to spend with the default privacy policy, z_sendmany
# fails because it needs to spend transparent coins in a transaction # fails because it needs to spend transparent coins in a transaction
# involving a Unified Address. # involving a Unified Address.
revealed_senders_msg = 'This transaction requires selecting transparent coins, which is not enabled by default because it will publicly reveal transaction senders and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedSenders` or weaker if you wish to allow this transaction to proceed anyway.' unified_address_msg = 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'
revealed_senders_msg = 'Insufficient funds: have 0.00, need 10.00; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker.'
opid = self.nodes[2].z_sendmany(source, recipients, 1, 0) opid = self.nodes[2].z_sendmany(source, recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', revealed_senders_msg) wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', unified_address_msg)
# We can't create a transaction with an unknown privacy policy. # We can't create a transaction with an unknown privacy policy.
assert_raises_message( assert_raises_message(
@ -196,13 +197,13 @@ class WalletZSendmanyTest(BitcoinTestFramework):
# If we set any policy that does not include AllowRevealedSenders, # If we set any policy that does not include AllowRevealedSenders,
# z_sendmany also fails. # z_sendmany also fails.
for policy in [ for (policy, msg) in [
'FullPrivacy', ('FullPrivacy', unified_address_msg),
'AllowRevealedAmounts', ('AllowRevealedAmounts', revealed_senders_msg),
'AllowRevealedRecipients', ('AllowRevealedRecipients', revealed_senders_msg),
]: ]:
opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, policy) opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, policy)
wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', revealed_senders_msg) wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', msg)
# By setting the correct policy, we can create the transaction. # By setting the correct policy, we can create the transaction.
opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, 'AllowRevealedSenders') opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, 'AllowRevealedSenders')
@ -321,20 +322,26 @@ class WalletZSendmanyTest(BitcoinTestFramework):
# If we try to send 3 ZEC from n1ua0, it will fail with too-few funds. # If we try to send 3 ZEC from n1ua0, it will fail with too-few funds.
recipients = [{"address":n0ua0, "amount":3}] recipients = [{"address":n0ua0, "amount":3}]
linked_addrs_msg = 'Insufficient funds: have 2.00, need 3.00. (This transaction may require selecting transparent coins that were sent to multiple Unified Addresses, which is not enabled by default because it would create a public link between the transparent receivers of these addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to allow this transaction to proceed anyway.)' linked_addrs_with_coinbase_note_msg = 'Insufficient funds: have 2.00, need 3.00; note that coinbase outputs will not be selected if you specify ANY_TADDR, any transparent recipients are included, or if the `privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker. (This transaction may require selecting transparent coins that were sent to multiple Unified Addresses, which is not enabled by default because it would create a public link between the transparent receivers of these addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to allow this transaction to proceed anyway.)'
linked_addrs_without_coinbase_note_msg = 'Insufficient funds: have 2.00, need 3.00. (This transaction may require selecting transparent coins that were sent to multiple Unified Addresses, which is not enabled by default because it would create a public link between the transparent receivers of these addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to allow this transaction to proceed anyway.)'
revealed_amounts_msg = 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0) opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', linked_addrs_msg) wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', revealed_amounts_msg)
# If we try it again with any policy that is too strong, it also fails. # If we try it again with any policy that is too strong, it also fails.
for policy in [ for (policy, msg) in [
'FullPrivacy', ('FullPrivacy', revealed_amounts_msg),
'AllowRevealedAmounts', ('AllowRevealedAmounts', linked_addrs_with_coinbase_note_msg),
'AllowRevealedRecipients', ('AllowRevealedRecipients', linked_addrs_with_coinbase_note_msg),
'AllowRevealedSenders', ('AllowRevealedSenders', linked_addrs_without_coinbase_note_msg),
'AllowFullyTransparent', ('AllowFullyTransparent', linked_addrs_without_coinbase_note_msg),
]: ]:
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, policy) opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, policy)
wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', linked_addrs_msg) wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', msg)
# If we try to send just a bit less than we have, it will fail, complaining about dust
opid = self.nodes[1].z_sendmany(n1ua0, [{"address":n0ua0, "amount":3.9999999}], 1, 0, 'AllowLinkingAccountAddresses')
wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', 'Insufficient funds: have 4.00, need 0.00000044 more to avoid creating invalid change output 0.0000001 (dust threshold is 0.00000054).')
# Once we provide a sufficiently-weak policy, the transaction succeeds. # Once we provide a sufficiently-weak policy, the transaction succeeds.
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, 'AllowLinkingAccountAddresses') opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, 'AllowLinkingAccountAddresses')
@ -356,6 +363,20 @@ class WalletZSendmanyTest(BitcoinTestFramework):
assert_equal(self.nodes[1].z_getbalance(n1ua0), 1) assert_equal(self.nodes[1].z_getbalance(n1ua0), 1)
assert_equal(self.nodes[1].z_getbalance(n1ua1), 1) assert_equal(self.nodes[1].z_getbalance(n1ua1), 1)
#
# Test Orchard-only UA before NU5
#
n0orchard_only = self.nodes[0].z_getaddressforaccount(n0account0, ["orchard"])['address']
recipients = [{"address":n0orchard_only, "amount":1}]
for (policy, msg) in [
('FullPrivacy', 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'),
('AllowRevealedAmounts', 'This transaction would send to a transparent receiver of a unified address, which is not enabled by default because it will publicly reveal transaction recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedRecipients` or weaker if you wish to allow this transaction to proceed anyway.'),
('AllowRevealedRecipients', 'Could not send to an Orchard-only receiver, despite a lax privacy policy. Either there are insufficient non-Sprout funds (there is no transaction version that supports both Sprout and Orchard), or NU5 has not been activated yet.'),
]:
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, policy)
wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', msg)
# #
# Test NoPrivacy policy # Test NoPrivacy policy
# #
@ -363,8 +384,8 @@ class WalletZSendmanyTest(BitcoinTestFramework):
# Send some legacy transparent funds to n1ua0, creating Sapling outputs. # Send some legacy transparent funds to n1ua0, creating Sapling outputs.
source = get_coinbase_address(self.nodes[2]) source = get_coinbase_address(self.nodes[2])
recipients = [{"address":n1ua0, "amount":10}] recipients = [{"address":n1ua0, "amount":10}]
# This requires the AllowRevealedSenders policy... # This requires the AllowRevealedSenders policy, but we specify only AllowRevealedAmounts...
opid = self.nodes[2].z_sendmany(source, recipients, 1, 0) opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, 'AllowRevealedAmounts')
wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', revealed_senders_msg) wait_and_assert_operationid_status(self.nodes[2], opid, 'failed', revealed_senders_msg)
# ... which we can always override with the NoPrivacy policy. # ... which we can always override with the NoPrivacy policy.
opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, 'NoPrivacy') opid = self.nodes[2].z_sendmany(source, recipients, 1, 0, 'NoPrivacy')
@ -389,6 +410,19 @@ class WalletZSendmanyTest(BitcoinTestFramework):
self.nodes[1].generate(10) self.nodes[1].generate(10)
self.sync_all() self.sync_all()
#
# Test Orchard-only UA with insufficient non-Sprout funds
#
recipients = [{"address":n0orchard_only, "amount":100}]
for (policy, msg) in [
('FullPrivacy', 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'),
('AllowRevealedAmounts', 'This transaction would send to a transparent receiver of a unified address, which is not enabled by default because it will publicly reveal transaction recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedRecipients` or weaker if you wish to allow this transaction to proceed anyway.'),
('AllowRevealedRecipients', 'Could not send to an Orchard-only receiver, despite a lax privacy policy. Either there are insufficient non-Sprout funds (there is no transaction version that supports both Sprout and Orchard), or NU5 has not been activated yet.'),
]:
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0, policy)
wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', msg)
# #
# Test AllowRevealedAmounts policy # Test AllowRevealedAmounts policy
# #
@ -402,7 +436,7 @@ class WalletZSendmanyTest(BitcoinTestFramework):
recipients = [{"address":n0ua1, "amount": 6}] recipients = [{"address":n0ua1, "amount": 6}]
# Should fail under default and 'FullPrivacy' policies ... # Should fail under default and 'FullPrivacy' policies ...
revealed_amounts_msg = 'Sending from the Sapling shielded pool to the Orchard shielded pool is not enabled by default because it will publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.' revealed_amounts_msg = 'Could not send to a shielded receiver of a unified address without spending funds from a different pool, which would reveal transaction amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` or weaker if you wish to allow this transaction to proceed anyway.'
opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0) opid = self.nodes[1].z_sendmany(n1ua0, recipients, 1, 0)
wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', revealed_amounts_msg) wait_and_assert_operationid_status(self.nodes[1], opid, 'failed', revealed_amounts_msg)

View File

@ -327,6 +327,7 @@ BITCOIN_CORE_H = \
wallet/asyncrpcoperation_saplingmigration.h \ wallet/asyncrpcoperation_saplingmigration.h \
wallet/asyncrpcoperation_sendmany.h \ wallet/asyncrpcoperation_sendmany.h \
wallet/asyncrpcoperation_shieldcoinbase.h \ wallet/asyncrpcoperation_shieldcoinbase.h \
wallet/wallet_tx_builder.h \
wallet/crypter.h \ wallet/crypter.h \
wallet/db.h \ wallet/db.h \
wallet/memo.h \ wallet/memo.h \
@ -418,6 +419,7 @@ libbitcoin_wallet_a_SOURCES = \
wallet/asyncrpcoperation_saplingmigration.cpp \ wallet/asyncrpcoperation_saplingmigration.cpp \
wallet/asyncrpcoperation_sendmany.cpp \ wallet/asyncrpcoperation_sendmany.cpp \
wallet/asyncrpcoperation_shieldcoinbase.cpp \ wallet/asyncrpcoperation_shieldcoinbase.cpp \
wallet/wallet_tx_builder.cpp \
wallet/crypter.cpp \ wallet/crypter.cpp \
wallet/db.cpp \ wallet/db.cpp \
wallet/orchard.cpp \ wallet/orchard.cpp \

View File

@ -1889,7 +1889,11 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
if (!zaddr.has_value()) { if (!zaddr.has_value()) {
return InitError(_("-mineraddress is not a valid " PACKAGE_NAME " address.")); return InitError(_("-mineraddress is not a valid " PACKAGE_NAME " address."));
} }
auto ztxoSelector = pwalletMain->ZTXOSelectorForAddress(zaddr.value(), true, false); auto ztxoSelector = pwalletMain->ZTXOSelectorForAddress(
zaddr.value(),
true,
TransparentCoinbasePolicy::Allow,
false);
minerAddressInLocalWallet = ztxoSelector.has_value(); minerAddressInLocalWallet = ztxoSelector.has_value();
} }
if (GetBoolArg("-minetolocalwallet", true) && !minerAddressInLocalWallet) { if (GetBoolArg("-minetolocalwallet", true) && !minerAddressInLocalWallet) {

View File

@ -3,6 +3,7 @@
#include "core_io.h" #include "core_io.h"
#include "init.h" #include "init.h"
#include "rpc/protocol.h" #include "rpc/protocol.h"
#include "util/moneystr.h"
extern UniValue signrawtransaction(const UniValue& params, bool fHelp); extern UniValue signrawtransaction(const UniValue& params, bool fHelp);
@ -40,3 +41,125 @@ std::pair<CTransaction, UniValue> SignSendRawTransaction(UniValue obj, std::opti
return std::make_pair(tx, sendResult); return std::make_pair(tx, sendResult);
} }
void ThrowInputSelectionError(
const InputSelectionError& err,
const ZTXOSelector& selector,
const TransactionStrategy& strategy)
{
std::visit(match {
[](const AddressResolutionError& err) {
switch (err) {
case AddressResolutionError::SproutRecipientsNotSupported:
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Sending funds into the Sprout pool is no longer supported.");
case AddressResolutionError::TransparentRecipientNotAllowed:
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"This transaction would have transparent recipients, which is not "
"enabled by default because it will publicly reveal transaction "
"recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit "
"with the `privacyPolicy` parameter set to `AllowRevealedRecipients` "
"or weaker if you wish to allow this transaction to proceed anyway.");
case AddressResolutionError::RevealingSaplingAmountNotAllowed:
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Could not send to the Sapling shielded pool without spending non-Sapling "
"funds, which would reveal transaction amounts. THIS MAY AFFECT YOUR "
"PRIVACY. Resubmit with the `privacyPolicy` parameter set to "
"`AllowRevealedAmounts` or weaker if you wish to allow this transaction to "
"proceed anyway.");
case AddressResolutionError::CouldNotResolveReceiver:
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Could not send to an Orchard-only receiver, despite a lax privacy policy. "
"Either there are insufficient non-Sprout funds (there is no transaction "
"version that supports both Sprout and Orchard), or NU5 has not been "
"activated yet.");
case AddressResolutionError::TransparentReceiverNotAllowed:
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"This transaction would send to a transparent receiver of a unified "
"address, which is not enabled by default because it will publicly reveal "
"transaction recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. "
"Resubmit with the `privacyPolicy` parameter set to "
"`AllowRevealedRecipients` or weaker if you wish to allow this transaction "
"to proceed anyway.");
case AddressResolutionError::RevealingReceiverAmountsNotAllowed:
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Could not send to a shielded receiver of a unified address without "
"spending funds from a different pool, which would reveal transaction "
"amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` "
"parameter set to `AllowRevealedAmounts` or weaker if you wish to allow "
"this transaction to proceed anyway.");
default:
assert(false);
}
},
[&](const InvalidFundsError& err) {
bool isFromUa = std::holds_alternative<libzcash::UnifiedAddress>(selector.GetPattern());
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf(
"Insufficient funds: have %s, %s",
FormatMoney(err.available),
std::visit(match {
[](const InsufficientFundsError& ife) {
return strprintf("need %s", FormatMoney(ife.required));
},
[](const DustThresholdError& dte) {
return strprintf(
"need %s more to avoid creating invalid change output %s (dust threshold is %s)",
FormatMoney(dte.dustThreshold - dte.changeAmount),
FormatMoney(dte.changeAmount),
FormatMoney(dte.dustThreshold));
}
},
err.reason))
+ (selector.TransparentCoinbasePolicy() != TransparentCoinbasePolicy::Disallow
? "" :
"; note that coinbase outputs will not be selected if you specify "
"ANY_TADDR, any transparent recipients are included, or if the "
"`privacyPolicy` parameter is not set to `AllowRevealedSenders` or weaker")
+ (!isFromUa || strategy.AllowLinkingAccountAddresses() ? "." :
". (This transaction may require selecting transparent coins that were sent "
"to multiple Unified Addresses, which is not enabled by default because "
"it would create a public link between the transparent receivers of these "
"addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` "
"parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to "
"allow this transaction to proceed anyway.)"));
},
[](const ChangeNotAllowedError& err) {
throw JSONRPCError(
RPC_WALLET_ERROR,
strprintf(
"When shielding coinbase funds, the wallet does not allow any change. "
"The proposed transaction would result in %s in change.",
FormatMoney(err.available - err.required)));
},
[](const ExcessOrchardActionsError& err) {
std::string side;
switch (err.side) {
case ActionSide::Input:
side = "inputs";
case ActionSide::Output:
side = "outputs";
case ActionSide::Both:
side = "actions";
};
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf(
"Including %u Orchard %s would exceed the current limit "
"of %u notes, which exists to prevent memory exhaustion. Restart with "
"`-orchardactionlimit=N` where N >= %u to allow the wallet to attempt "
"to construct this transaction.",
err.orchardNotes,
side,
err.maxNotes,
err.orchardNotes));
}
}, err);
}

View File

@ -11,6 +11,7 @@
#include "rpc/protocol.h" #include "rpc/protocol.h"
#include "univalue.h" #include "univalue.h"
#include "wallet.h" #include "wallet.h"
#include "wallet/wallet_tx_builder.h"
#include <optional> #include <optional>
@ -63,4 +64,9 @@ UniValue SendTransaction(
*/ */
std::pair<CTransaction, UniValue> SignSendRawTransaction(UniValue obj, std::optional<std::reference_wrapper<CReserveKey>> reservekey, bool testmode); std::pair<CTransaction, UniValue> SignSendRawTransaction(UniValue obj, std::optional<std::reference_wrapper<CReserveKey>> reservekey, bool testmode);
void ThrowInputSelectionError(
const InputSelectionError& err,
const ZTXOSelector& selector,
const TransactionStrategy& strategy);
#endif // ZCASH_WALLET_ASYNCRPCOPERATION_COMMON_H #endif // ZCASH_WALLET_ASYNCRPCOPERATION_COMMON_H

View File

@ -18,7 +18,6 @@
#include "proof_verifier.h" #include "proof_verifier.h"
#include "rpc/protocol.h" #include "rpc/protocol.h"
#include "rpc/server.h" #include "rpc/server.h"
#include "transaction_builder.h"
#include "timedata.h" #include "timedata.h"
#include "util/system.h" #include "util/system.h"
#include "util/match.h" #include "util/match.h"
@ -29,7 +28,6 @@
#include "util/time.h" #include "util/time.h"
#include "zcash/IncrementalMerkleTree.hpp" #include "zcash/IncrementalMerkleTree.hpp"
#include "miner.h" #include "miner.h"
#include "wallet/paymentdisclosuredb.h"
#include "wallet/wallet_tx_builder.h" #include "wallet/wallet_tx_builder.h"
#include <array> #include <array>
@ -45,9 +43,9 @@
using namespace libzcash; using namespace libzcash;
AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany( AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
TransactionBuilder builder, WalletTxBuilder builder,
ZTXOSelector ztxoSelector, ZTXOSelector ztxoSelector,
std::vector<ResolvedPayment> recipients, std::vector<Payment> recipients,
int minDepth, int minDepth,
unsigned int anchorDepth, unsigned int anchorDepth,
TransactionStrategy strategy, TransactionStrategy strategy,
@ -62,32 +60,6 @@ AsyncRPCOperation_sendmany::AsyncRPCOperation_sendmany(
assert(!recipients_.empty()); assert(!recipients_.empty());
assert(ztxoSelector.RequireSpendingKeys()); assert(ztxoSelector.RequireSpendingKeys());
sendFromAccount_ = pwalletMain->FindAccountForSelector(ztxoSelector_).value_or(ZCASH_LEGACY_ACCOUNT);
// Determine the target totals and recipient pools
for (const ResolvedPayment& recipient : recipients_) {
std::visit(match {
[&](const CKeyID& addr) {
txOutputAmounts_.t_outputs_total += recipient.amount;
recipientPools_.insert(OutputPool::Transparent);
},
[&](const CScriptID& addr) {
txOutputAmounts_.t_outputs_total += recipient.amount;
recipientPools_.insert(OutputPool::Transparent);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
txOutputAmounts_.sapling_outputs_total += recipient.amount;
recipientPools_.insert(OutputPool::Sapling);
},
[&](const libzcash::OrchardRawAddress& addr) {
txOutputAmounts_.orchard_outputs_total += recipient.amount;
recipientPools_.insert(OutputPool::Orchard);
// No transaction allows sends from Sprout to Orchard.
assert(!ztxoSelector_.SelectsSprout());
}
}, recipient.address);
}
// Log the context info i.e. the call parameters to z_sendmany // Log the context info i.e. the call parameters to z_sendmany
if (LogAcceptCategory("zrpcunsafe")) { if (LogAcceptCategory("zrpcunsafe")) {
LogPrint("zrpcunsafe", "%s: z_sendmany initialized (params=%s)\n", getId(), contextInfo.write()); LogPrint("zrpcunsafe", "%s: z_sendmany initialized (params=%s)\n", getId(), contextInfo.write());
@ -106,8 +78,6 @@ void AsyncRPCOperation_sendmany::main() {
set_state(OperationStatus::EXECUTING); set_state(OperationStatus::EXECUTING);
start_execution_clock(); start_execution_clock();
bool success = false;
#ifdef ENABLE_MINING #ifdef ENABLE_MINING
GenerateBitcoins(false, 0, Params()); GenerateBitcoins(false, 0, Params());
#endif #endif
@ -164,606 +134,62 @@ void AsyncRPCOperation_sendmany::main() {
// 2. #1360 Note selection is not optimal. // 2. #1360 Note selection is not optimal.
// 3. #1277 Spendable notes are not locked, so an operation running in parallel // 3. #1277 Spendable notes are not locked, so an operation running in parallel
// could also try to use them. // could also try to use them.
// 4. #1614 Anchors are chosen at the most recent block; this is unreliable and leaks // 4. #3615 There is no padding of inputs or outputs, which may leak information.
// information in case of rollback.
// 5. #3615 There is no padding of inputs or outputs, which may leak information.
// //
// At least 4. and 5. differ from the Rust transaction builder. // At least #4 differs from the Rust transaction builder.
uint256 AsyncRPCOperation_sendmany::main_impl() { uint256 AsyncRPCOperation_sendmany::main_impl() {
CAmount sendAmount = ( auto spendable = builder_.FindAllSpendableInputs(ztxoSelector_, mindepth_);
txOutputAmounts_.orchard_outputs_total +
txOutputAmounts_.sapling_outputs_total +
txOutputAmounts_.t_outputs_total);
CAmount targetAmount = sendAmount + fee_;
builder_.SetFee(fee_); auto preparedTx = builder_.PrepareTransaction(
ztxoSelector_,
// Allow transparent coinbase inputs if there are no transparent spendable,
// recipients. recipients_,
bool allowTransparentCoinbase = !recipientPools_.count(OutputPool::Transparent); chainActive,
strategy_,
// Set the dust threshold so that we can select enough inputs to avoid fee_,
// creating dust change amounts. anchordepth_);
CAmount dustThreshold{DefaultDustThreshold()};
// Find spendable inputs, and select a minimal set of them that
// can supply the required target amount.
SpendableInputs spendable;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
spendable = pwalletMain->FindSpendableInputs(ztxoSelector_, allowTransparentCoinbase, mindepth_, std::nullopt);
}
if (!spendable.LimitToAmount(targetAmount, dustThreshold, recipientPools_)) {
CAmount changeAmount{spendable.Total() - targetAmount};
std::string insufficientFundsMessage =
strprintf("Insufficient funds: have %s", FormatMoney(spendable.Total()));
if (changeAmount > 0 && changeAmount < dustThreshold) {
// TODO: we should provide the option for the caller to explicitly
// forego change (definitionally an amount below the dust amount)
// and send the extra to the recipient or the miner fee to avoid
// creating dust change, rather than prohibit them from sending
// entirely in this circumstance.
// (Daira disagrees, as this could leak information to the recipient
// or to an external viewing key holder.)
insufficientFundsMessage +=
strprintf(
", need %s more to avoid creating invalid change output %s (dust threshold is %s)",
FormatMoney(dustThreshold - changeAmount),
FormatMoney(changeAmount),
FormatMoney(dustThreshold));
} else {
insufficientFundsMessage += strprintf(", need %s", FormatMoney(targetAmount));
}
bool isFromUa = std::holds_alternative<libzcash::UnifiedAddress>(ztxoSelector_.GetPattern());
throw JSONRPCError(
RPC_WALLET_INSUFFICIENT_FUNDS,
insufficientFundsMessage
+ (allowTransparentCoinbase && ztxoSelector_.SelectsTransparentCoinbase() ? "." :
"; note that coinbase outputs will not be selected if you specify "
"ANY_TADDR or if any transparent recipients are included.")
+ ((!isFromUa || strategy_.AllowLinkingAccountAddresses()) ? "" :
" (This transaction may require selecting transparent coins that were sent "
"to multiple Unified Addresses, which is not enabled by default because "
"it would create a public link between the transparent receivers of these "
"addresses. THIS MAY AFFECT YOUR PRIVACY. Resubmit with the `privacyPolicy` "
"parameter set to `AllowLinkingAccountAddresses` or weaker if you wish to "
"allow this transaction to proceed anyway.)"));
}
if (!(spendable.utxos.empty() || strategy_.AllowRevealedSenders())) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"This transaction requires selecting transparent coins, which is "
"not enabled by default because it will publicly reveal transaction "
"senders and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit "
"with the `privacyPolicy` parameter set to `AllowRevealedSenders` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
if (recipientPools_.count(OutputPool::Transparent) && !strategy_.AllowRevealedRecipients()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"This transaction would have transparent recipients, which is not "
"enabled by default because it will publicly reveal transaction "
"recipients and amounts. THIS MAY AFFECT YOUR PRIVACY. Resubmit "
"with the `privacyPolicy` parameter set to `AllowRevealedRecipients` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
if (!spendable.sproutNoteEntries.empty()) {
if (recipientPools_.count(OutputPool::Sapling) && !strategy_.AllowRevealedAmounts()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Sending from the Sprout shielded pool to the Sapling "
"shielded pool is not enabled by default because it will "
"publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. "
"Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
}
if (!spendable.saplingNoteEntries.empty()) {
if (recipientPools_.count(OutputPool::Orchard) && !strategy_.AllowRevealedAmounts()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Sending from the Sapling shielded pool to the Orchard "
"shielded pool is not enabled by default because it will "
"publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. "
"Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
// Sending from Sapling to transparent will be caught above in the
// AllowRevealedRecipients check; sending to Sprout is disallowed
// entirely.
}
if (!spendable.orchardNoteMetadata.empty()) {
if (recipientPools_.count(OutputPool::Sapling) && !strategy_.AllowRevealedAmounts()) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Sending from the Orchard shielded pool to the Sapling "
"shielded pool is not enabled by default because it will "
"publicly reveal the transaction amount. THIS MAY AFFECT YOUR PRIVACY. "
"Resubmit with the `privacyPolicy` parameter set to `AllowRevealedAmounts` "
"or weaker if you wish to allow this transaction to proceed anyway.");
}
// Sending from Orchard to transparent will be caught above in the
// AllowRevealedRecipients check; sending to Sprout is disallowed
// entirely.
if (spendable.orchardNoteMetadata.size() > nOrchardActionLimit) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf(
"Attempting to spend %u Orchard notes would exceed the current limit "
"of %u notes, which exists to prevent memory exhaustion. Restart with "
"`-orchardactionlimit=N` where N >= %u to allow the wallet to attempt "
"to construct this transaction.",
spendable.orchardNoteMetadata.size(),
nOrchardActionLimit,
spendable.orchardNoteMetadata.size()));
}
}
spendable.LogInputs(getId());
CAmount t_inputs_total{0};
CAmount z_inputs_total{0};
for (const auto& t : spendable.utxos) {
t_inputs_total += t.Value();
}
for (const auto& t : spendable.sproutNoteEntries) {
z_inputs_total += t.note.value();
}
for (const auto& t : spendable.saplingNoteEntries) {
z_inputs_total += t.note.value();
}
for (const auto& t : spendable.orchardNoteMetadata) {
z_inputs_total += t.GetNoteValue();
}
if (z_inputs_total > 0 && mindepth_ == 0) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Minconf cannot be zero when sending from a shielded address");
}
// When spending transparent coinbase outputs, all inputs must be fully
// consumed, and they may only be sent to shielded recipients.
if (spendable.HasTransparentCoinbase()) {
if (t_inputs_total + z_inputs_total != targetAmount) {
throw JSONRPCError(
RPC_WALLET_ERROR,
strprintf(
"When shielding coinbase funds, the wallet does not allow any change. "
"The proposed transaction would result in %s in change.",
FormatMoney(t_inputs_total - targetAmount)
));
}
if (txOutputAmounts_.t_outputs_total != 0) {
throw JSONRPCError(
RPC_WALLET_ERROR,
"Coinbase funds may only be sent to shielded recipients.");
}
}
LogPrint("zrpcunsafe", "%s: spending %s to send %s with fee %s\n",
getId(), FormatMoney(targetAmount), FormatMoney(sendAmount), FormatMoney(fee_));
LogPrint("zrpc", "%s: total transparent input: %s (to choose from)\n", getId(), FormatMoney(t_inputs_total));
LogPrint("zrpcunsafe", "%s: total shielded input: %s (to choose from)\n", getId(), FormatMoney(z_inputs_total));
LogPrint("zrpc", "%s: total transparent output: %s\n", getId(), FormatMoney(txOutputAmounts_.t_outputs_total));
LogPrint("zrpcunsafe", "%s: total shielded Sapling output: %s\n", getId(), FormatMoney(txOutputAmounts_.sapling_outputs_total));
LogPrint("zrpcunsafe", "%s: total shielded Orchard output: %s\n", getId(), FormatMoney(txOutputAmounts_.orchard_outputs_total));
LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(fee_));
// Allow change to go to any pool for which we have recipients.
std::set<OutputPool> allowedChangeTypes = recipientPools_;
// We always allow shielded change when not sending from the legacy account.
if (sendFromAccount_ != ZCASH_LEGACY_ACCOUNT) {
allowedChangeTypes.insert(OutputPool::Sapling);
}
auto ovks = this->SelectOVKs(spendable);
auto allowChangeTypes = [&](const std::set<ReceiverType>& receiverTypes) {
for (ReceiverType rtype : receiverTypes) {
switch (rtype) {
case ReceiverType::P2PKH:
case ReceiverType::P2SH:
if (!spendable.utxos.empty() || strategy_.AllowRevealedRecipients()) {
allowedChangeTypes.insert(OutputPool::Transparent);
}
break;
case ReceiverType::Sapling:
if (!spendable.saplingNoteEntries.empty() || strategy_.AllowRevealedAmounts()) {
allowedChangeTypes.insert(OutputPool::Sapling);
}
break;
case ReceiverType::Orchard:
if (builder_.SupportsOrchard() &&
(!spendable.orchardNoteMetadata.empty() || strategy_.AllowRevealedAmounts())) {
allowedChangeTypes.insert(OutputPool::Orchard);
}
break;
}
}
};
uint256 txid;
std::visit(match { std::visit(match {
[&](const CKeyID& keyId) { [&](const InputSelectionError& err) {
allowedChangeTypes.insert(OutputPool::Transparent); ThrowInputSelectionError(err, ztxoSelector_, strategy_);
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
}, },
[&](const CScriptID& scriptId) { [&](const TransactionEffects& effects) {
allowedChangeTypes.insert(OutputPool::Transparent); const auto& spendable = effects.GetSpendable();
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount( const auto& payments = effects.GetPayments();
sendFromAccount_, allowedChangeTypes); spendable.LogInputs(getId());
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
},
[&](const libzcash::SproutPaymentAddress& addr) {
// for Sprout, we return change to the originating address.
builder_.SendChangeToSprout(addr);
},
[&](const libzcash::SproutViewingKey& vk) {
// for Sprout, we return change to the originating address.
builder_.SendChangeToSprout(vk.address());
},
[&](const libzcash::SaplingPaymentAddress& addr) {
// for Sapling, if using a legacy address, return change to the
// originating address; otherwise return it to the Sapling internal
// address corresponding to the UFVK.
if (sendFromAccount_ == ZCASH_LEGACY_ACCOUNT) {
builder_.SendChangeTo(addr, ovks.first);
} else {
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
}
},
[&](const libzcash::SaplingExtendedFullViewingKey& fvk) {
// for Sapling, if using a legacy address, return change to the
// originating address; otherwise return it to the Sapling internal
// address corresponding to the UFVK.
if (sendFromAccount_ == ZCASH_LEGACY_ACCOUNT) {
builder_.SendChangeTo(fvk.DefaultAddress(), ovks.first);
} else {
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount_, allowedChangeTypes);
assert(changeAddr.has_value());
builder_.SendChangeTo(changeAddr.value(), ovks.first);
}
},
[&](const libzcash::UnifiedAddress& ua) {
allowChangeTypes(ua.GetKnownReceiverTypes());
auto zufvk = pwalletMain->GetUFVKForAddress(ua); LogPrint("zrpcunsafe", "%s: spending %s to send %s with fee %s\n", getId(),
if (!zufvk.has_value()) { FormatMoney(payments.Total()),
throw JSONRPCError( FormatMoney(spendable.Total()),
RPC_WALLET_ERROR, FormatMoney(effects.GetFee()));
"Could not determine full viewing key for unified address."); LogPrint("zrpc", "%s: total transparent input: %s (to choose from)\n", getId(),
} FormatMoney(spendable.GetTransparentTotal()));
LogPrint("zrpcunsafe", "%s: total shielded input: %s (to choose from)\n", getId(),
FormatMoney(spendable.GetSaplingTotal() + spendable.GetOrchardTotal()));
LogPrint("zrpc", "%s: total transparent output: %s\n", getId(),
FormatMoney(payments.GetTransparentTotal()));
LogPrint("zrpcunsafe", "%s: total shielded Sapling output: %s\n", getId(),
FormatMoney(payments.GetSaplingTotal()));
LogPrint("zrpcunsafe", "%s: total shielded Orchard output: %s\n", getId(),
FormatMoney(payments.GetOrchardTotal()));
LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(effects.GetFee()));
auto changeAddr = zufvk.value().GetChangeAddress(allowedChangeTypes); auto buildResult = effects.ApproveAndBuild(
if (!changeAddr.has_value()) { Params().GetConsensus(),
throw JSONRPCError( *pwalletMain,
RPC_WALLET_ERROR, chainActive,
"Could not generate a change address from the inferred full viewing key."); strategy_);
} auto tx = buildResult.GetTxOrThrow();
builder_.SendChangeTo(changeAddr.value(), ovks.first);
},
[&](const libzcash::UnifiedFullViewingKey& fvk) {
allowChangeTypes(fvk.GetKnownReceiverTypes());
auto zufvk = ZcashdUnifiedFullViewingKey::FromUnifiedFullViewingKey(Params(), fvk);
auto changeAddr = zufvk.GetChangeAddress(allowedChangeTypes);
if (!changeAddr.has_value()) {
throw JSONRPCError(
RPC_WALLET_ERROR,
"Could not generate a change address from the specified full viewing key.");
}
builder_.SendChangeTo(changeAddr.value(), ovks.first);
},
[&](const AccountZTXOPattern& acct) {
allowChangeTypes(acct.GetReceiverTypes());
auto changeAddr = pwalletMain->GenerateChangeAddressForAccount(
acct.GetAccountId(),
allowedChangeTypes);
assert(changeAddr.has_value()); UniValue sendResult = SendTransaction(tx, payments.GetResolvedPayments(), std::nullopt, testmode);
builder_.SendChangeTo(changeAddr.value(), ovks.first); set_result(sendResult);
txid = tx.GetHash();
} }
}, ztxoSelector_.GetPattern()); }, preparedTx);
// Track the total of notes that we've added to the builder. This return txid;
// shouldn't strictly be necessary, given `spendable.LimitToAmount`
CAmount sum = 0;
// Create Sapling outpoints
std::vector<SaplingOutPoint> saplingOutPoints;
std::vector<SaplingNote> saplingNotes;
std::vector<SaplingExtendedSpendingKey> saplingKeys;
for (const auto& t : spendable.saplingNoteEntries) {
saplingOutPoints.push_back(t.op);
saplingNotes.push_back(t.note);
libzcash::SaplingExtendedSpendingKey saplingKey;
assert(pwalletMain->GetSaplingExtendedSpendingKey(t.address, saplingKey));
saplingKeys.push_back(saplingKey);
sum += t.note.value();
if (sum >= targetAmount) {
break;
}
}
// Fetch Sapling anchor and witnesses, and Orchard Merkle paths.
uint256 anchor;
std::vector<std::optional<SaplingWitness>> witnesses;
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> orchardSpendInfo;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
if (!pwalletMain->GetSaplingNoteWitnesses(saplingOutPoints, anchordepth_, witnesses, anchor)) {
// This error should not appear once we're nAnchorConfirmations blocks past
// Sapling activation.
throw JSONRPCError(RPC_WALLET_ERROR, "Insufficient Sapling witnesses.");
}
if (builder_.GetOrchardAnchor().has_value()) {
orchardSpendInfo = pwalletMain->GetOrchardSpendInfo(spendable.orchardNoteMetadata, builder_.GetOrchardAnchor().value());
}
}
// Add Orchard spends
for (size_t i = 0; i < orchardSpendInfo.size(); i++) {
auto spendInfo = std::move(orchardSpendInfo[i]);
if (!builder_.AddOrchardSpend(
std::move(spendInfo.first),
std::move(spendInfo.second)))
{
throw JSONRPCError(
RPC_WALLET_ERROR,
strprintf("Failed to add Orchard note to transaction (check %s for details)", GetDebugLogPath())
);
}
}
// Add Sapling spends
for (size_t i = 0; i < saplingNotes.size(); i++) {
if (!witnesses[i]) {
throw JSONRPCError(
RPC_WALLET_ERROR,
strprintf(
"Missing witness for Sapling note at outpoint %s",
spendable.saplingNoteEntries[i].op.ToString())
);
}
builder_.AddSaplingSpend(saplingKeys[i].expsk, saplingNotes[i], anchor, witnesses[i].value());
}
// Add outputs
for (const auto& r : recipients_) {
std::visit(match {
[&](const CKeyID& keyId) {
builder_.AddTransparentOutput(keyId, r.amount);
},
[&](const CScriptID& scriptId) {
builder_.AddTransparentOutput(scriptId, r.amount);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
builder_.AddSaplingOutput(
ovks.second, addr, r.amount,
r.memo.has_value() ? r.memo.value().ToBytes() : Memo::NoMemo().ToBytes());
},
[&](const libzcash::OrchardRawAddress& addr) {
builder_.AddOrchardOutput(
ovks.second, addr, r.amount,
r.memo.has_value() ? std::optional(r.memo.value().ToBytes()) : std::nullopt);
}
}, r.address);
}
// Add transparent utxos
for (const auto& out : spendable.utxos) {
const CTxOut& txOut = out.tx->vout[out.i];
builder_.AddTransparentInput(COutPoint(out.tx->GetHash(), out.i), txOut.scriptPubKey, txOut.nValue);
sum += txOut.nValue;
if (sum >= targetAmount) {
break;
}
}
// Find Sprout witnesses
// When spending notes, take a snapshot of note witnesses and anchors as the treestate will
// change upon arrival of new blocks which contain joinsplit transactions. This is likely
// to happen as creating a chained joinsplit transaction can take longer than the block interval.
// So, we need to take locks on cs_main and pwalletMain->cs_wallet so that the witnesses aren't
// updated.
//
// TODO: these locks would ideally be shared for selection of Sapling anchors and witnesses
// as well.
std::vector<std::optional<SproutWitness>> vSproutWitnesses;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
std::vector<JSOutPoint> vOutPoints;
for (const auto& t : spendable.sproutNoteEntries) {
vOutPoints.push_back(t.jsop);
}
// inputAnchor is not needed by builder_.AddSproutInput as it is for Sapling.
uint256 inputAnchor;
if (!pwalletMain->GetSproutNoteWitnesses(vOutPoints, anchordepth_, vSproutWitnesses, inputAnchor)) {
// This error should not appear once we're nAnchorConfirmations blocks past
// Sprout activation.
throw JSONRPCError(RPC_WALLET_ERROR, "Insufficient Sprout witnesses.");
}
}
// Add Sprout spends
for (int i = 0; i < spendable.sproutNoteEntries.size(); i++) {
const auto& t = spendable.sproutNoteEntries[i];
libzcash::SproutSpendingKey sk;
assert(pwalletMain->GetSproutSpendingKey(t.address, sk));
builder_.AddSproutInput(sk, t.note, vSproutWitnesses[i].value());
sum += t.note.value();
if (sum >= targetAmount) {
break;
}
}
// Build the transaction
auto buildResult = builder_.Build();
auto tx = buildResult.GetTxOrThrow();
UniValue sendResult = SendTransaction(tx, recipients_, std::nullopt, testmode);
set_result(sendResult);
return tx.GetHash();
}
std::pair<uint256, uint256> AsyncRPCOperation_sendmany::SelectOVKs(const SpendableInputs& spendable) const {
uint256 internalOVK;
uint256 externalOVK;
if (!spendable.orchardNoteMetadata.empty()) {
std::optional<OrchardFullViewingKey> fvk;
std::visit(match {
[&](const UnifiedAddress& addr) {
auto ufvk = pwalletMain->GetUFVKForAddress(addr);
// This is safe because spending key checks will have ensured that we
// have a UFVK corresponding to this address, and Orchard notes will
// not have been selected if the UFVK does not contain an Orchard key.
fvk = ufvk.value().GetOrchardKey().value();
},
[&](const UnifiedFullViewingKey& ufvk) {
// Orchard notes will not have been selected if the UFVK does not contain
// an Orchard key.
fvk = ufvk.GetOrchardKey().value();
},
[&](const AccountZTXOPattern& acct) {
// By definition, we have a UFVK for every known account.
auto ufvk = pwalletMain->GetUnifiedFullViewingKeyByAccount(acct.GetAccountId());
// Orchard notes will not have been selected if the UFVK does not contain
// an Orchard key.
fvk = ufvk.value().GetOrchardKey().value();
},
[&](const auto& other) {
throw std::runtime_error("SelectOVKs: Selector cannot select Orchard notes.");
}
}, this->ztxoSelector_.GetPattern());
assert(fvk.has_value());
internalOVK = fvk.value().ToInternalOutgoingViewingKey();
externalOVK = fvk.value().ToExternalOutgoingViewingKey();
} else if (!spendable.saplingNoteEntries.empty()) {
std::optional<SaplingDiversifiableFullViewingKey> dfvk;
std::visit(match {
[&](const libzcash::SaplingPaymentAddress& addr) {
libzcash::SaplingExtendedSpendingKey extsk;
assert(pwalletMain->GetSaplingExtendedSpendingKey(addr, extsk));
dfvk = extsk.ToXFVK();
},
[&](const UnifiedAddress& addr) {
auto ufvk = pwalletMain->GetUFVKForAddress(addr);
// This is safe because spending key checks will have ensured that we
// have a UFVK corresponding to this address, and Sapling notes will
// not have been selected if the UFVK does not contain a Sapling key.
dfvk = ufvk.value().GetSaplingKey().value();
},
[&](const UnifiedFullViewingKey& ufvk) {
// Sapling notes will not have been selected if the UFVK does not contain
// a Sapling key.
dfvk = ufvk.GetSaplingKey().value();
},
[&](const AccountZTXOPattern& acct) {
// By definition, we have a UFVK for every known account.
auto ufvk = pwalletMain->GetUnifiedFullViewingKeyByAccount(acct.GetAccountId());
// Sapling notes will not have been selected if the UFVK does not contain
// a Sapling key.
dfvk = ufvk.value().GetSaplingKey().value();
},
[&](const auto& other) {
throw std::runtime_error("SelectOVKs: Selector cannot select Sapling notes.");
}
}, this->ztxoSelector_.GetPattern());
assert(dfvk.has_value());
auto ovks = dfvk.value().GetOVKs();
internalOVK = ovks.first;
externalOVK = ovks.second;
} else if (!spendable.utxos.empty()) {
std::optional<transparent::AccountPubKey> tfvk;
std::visit(match {
[&](const CKeyID& keyId) {
tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
},
[&](const CScriptID& keyId) {
tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
},
[&](const UnifiedAddress& addr) {
// This is safe because spending key checks will have ensured that we
// have a UFVK corresponding to this address, and transparent UTXOs will
// not have been selected if the UFVK does not contain a transparent key.
auto ufvk = pwalletMain->GetUFVKForAddress(addr);
tfvk = ufvk.value().GetTransparentKey().value();
},
[&](const UnifiedFullViewingKey& ufvk) {
// Transparent UTXOs will not have been selected if the UFVK does not contain
// a transparent key.
tfvk = ufvk.GetTransparentKey().value();
},
[&](const AccountZTXOPattern& acct) {
if (acct.GetAccountId() == ZCASH_LEGACY_ACCOUNT) {
tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
} else {
// By definition, we have a UFVK for every known account.
auto ufvk = pwalletMain->GetUnifiedFullViewingKeyByAccount(acct.GetAccountId()).value();
// Transparent UTXOs will not have been selected if the UFVK does not contain
// a transparent key.
tfvk = ufvk.GetTransparentKey().value();
}
},
[&](const auto& other) {
throw std::runtime_error("SelectOVKs: Selector cannot select transparent UTXOs.");
}
}, this->ztxoSelector_.GetPattern());
assert(tfvk.has_value());
auto ovks = tfvk.value().GetOVKsForShielding();
internalOVK = ovks.first;
externalOVK = ovks.second;
} else if (!spendable.sproutNoteEntries.empty()) {
// use the legacy transparent account OVKs when sending from Sprout
auto tfvk = pwalletMain->GetLegacyAccountKey().ToAccountPubKey();
auto ovks = tfvk.GetOVKsForShielding();
internalOVK = ovks.first;
externalOVK = ovks.second;
} else {
// This should be unreachable; it is left in place as a guard to ensure
// that when new input types are added to SpendableInputs in the future
// that we do not accidentally return the all-zeros OVK.
throw std::runtime_error("No spendable inputs.");
}
return std::make_pair(internalOVK, externalOVK);
}
/**
* Compute a dust threshold based upon a standard p2pkh txout.
*/
CAmount AsyncRPCOperation_sendmany::DefaultDustThreshold() {
CKey secret{CKey::TestOnlyRandomKey(true)};
CScript scriptPubKey = GetScriptForDestination(secret.GetPubKey().GetID());
CTxOut txout(CAmount(1), scriptPubKey);
// TODO: use a local for minRelayTxFee rather than a global
return txout.GetDustThreshold(minRelayTxFee);
} }
/** /**

View File

@ -26,24 +26,18 @@
using namespace libzcash; using namespace libzcash;
class TxOutputAmounts {
public:
CAmount t_outputs_total{0};
CAmount sapling_outputs_total{0};
CAmount orchard_outputs_total{0};
};
class AsyncRPCOperation_sendmany : public AsyncRPCOperation { class AsyncRPCOperation_sendmany : public AsyncRPCOperation {
public: public:
AsyncRPCOperation_sendmany( AsyncRPCOperation_sendmany(
TransactionBuilder builder, WalletTxBuilder builder,
ZTXOSelector ztxoSelector, ZTXOSelector ztxoSelector,
std::vector<ResolvedPayment> recipients, std::vector<Payment> recipients,
int minDepth, int minDepth,
unsigned int anchorDepth, unsigned int anchorDepth,
TransactionStrategy strategy, TransactionStrategy strategy,
CAmount fee = DEFAULT_FEE, CAmount fee = DEFAULT_FEE,
UniValue contextInfo = NullUniValue); UniValue contextInfo = NullUniValue);
virtual ~AsyncRPCOperation_sendmany(); virtual ~AsyncRPCOperation_sendmany();
// We don't want to be copied or moved around // We don't want to be copied or moved around
@ -61,29 +55,15 @@ public:
private: private:
friend class TEST_FRIEND_AsyncRPCOperation_sendmany; // class for unit testing friend class TEST_FRIEND_AsyncRPCOperation_sendmany; // class for unit testing
TransactionBuilder builder_; WalletTxBuilder builder_;
ZTXOSelector ztxoSelector_; ZTXOSelector ztxoSelector_;
std::vector<ResolvedPayment> recipients_; std::vector<Payment> recipients_;
TransactionStrategy strategy_;
int mindepth_{1}; int mindepth_{1};
unsigned int anchordepth_{nAnchorConfirmations}; unsigned int anchordepth_{nAnchorConfirmations};
CAmount fee_; CAmount fee_;
UniValue contextinfo_; // optional data to include in return value from getStatus() UniValue contextinfo_; // optional data to include in return value from getStatus()
bool isfromsprout_{false};
bool isfromsapling_{false};
TransactionStrategy strategy_;
AccountId sendFromAccount_;
std::set<OutputPool> recipientPools_;
TxOutputAmounts txOutputAmounts_;
/**
* Compute the internal and external OVKs to use in transaction construction, given
* the spendable inputs.
*/
std::pair<uint256, uint256> SelectOVKs(const SpendableInputs& spendable) const;
static CAmount DefaultDustThreshold();
uint256 main_impl(); uint256 main_impl();
}; };

View File

@ -284,12 +284,18 @@ TEST(WalletRPCTests, RPCZsendmanyTaddrToSapling)
pwalletMain->LoadWalletTx(wtx); pwalletMain->LoadWalletTx(wtx);
// Context that z_sendmany requires // Context that z_sendmany requires
auto builder = TransactionBuilder(consensusParams, nextBlockHeight, std::nullopt, pwalletMain); auto builder = WalletTxBuilder(Params(), *pwalletMain, minRelayTxFee);
mtx = CreateNewContextualCMutableTransaction(consensusParams, nextBlockHeight, false); mtx = CreateNewContextualCMutableTransaction(consensusParams, nextBlockHeight, false);
auto selector = pwalletMain->ZTXOSelectorForAddress(taddr, true, false).value(); // we need AllowFullyTransparent because the transaction will result
std::vector<ResolvedPayment> recipients = { ResolvedPayment(std::nullopt, pa, 1*COIN, Memo::FromHexOrThrow("ABCD")) }; // in transparent change as a consequence of sending from a legacy taddr
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedSenders); TransactionStrategy strategy(PrivacyPolicy::AllowFullyTransparent);
auto selector = pwalletMain->ZTXOSelectorForAddress(
taddr,
true,
TransparentCoinbasePolicy::Disallow,
false).value();
std::vector<Payment> recipients = { Payment(pa, 1*COIN, Memo::FromHexOrThrow("ABCD")) };
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 0, 0, strategy)); std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 0, 0, strategy));
std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation); std::shared_ptr<AsyncRPCOperation_sendmany> ptr = std::dynamic_pointer_cast<AsyncRPCOperation_sendmany> (operation);

View File

@ -3869,13 +3869,13 @@ UniValue z_getbalance(const UniValue& params, bool fHelp)
nBalance = getBalanceZaddr(addr, std::nullopt, nMinDepth, INT_MAX, false); nBalance = getBalanceZaddr(addr, std::nullopt, nMinDepth, INT_MAX, false);
}, },
[&](const libzcash::UnifiedAddress& addr) { [&](const libzcash::UnifiedAddress& addr) {
auto selector = pwalletMain->ZTXOSelectorForAddress(addr, true, false); auto selector = pwalletMain->ZTXOSelectorForAddress(addr, true, TransparentCoinbasePolicy::Allow, false);
if (!selector.has_value()) { if (!selector.has_value()) {
throw JSONRPCError( throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY, RPC_INVALID_ADDRESS_OR_KEY,
"Unified address does not correspond to an account in the wallet"); "Unified address does not correspond to an account in the wallet");
} }
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, nMinDepth, std::nullopt); auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), nMinDepth, std::nullopt);
for (const auto& t : spendableInputs.utxos) { for (const auto& t : spendableInputs.utxos) {
nBalance += t.Value(); nBalance += t.Value();
@ -3960,14 +3960,14 @@ UniValue z_getbalanceforviewingkey(const UniValue& params, bool fHelp)
// FVKs make it possible to correctly determine balance without having the // FVKs make it possible to correctly determine balance without having the
// spending key, so we permit that here. // spending key, so we permit that here.
bool requireSpendingKey = std::holds_alternative<libzcash::SproutViewingKey>(fvk); bool requireSpendingKey = std::holds_alternative<libzcash::SproutViewingKey>(fvk);
auto selector = pwalletMain->ZTXOSelectorForViewingKey(fvk, requireSpendingKey); auto selector = pwalletMain->ZTXOSelectorForViewingKey(fvk, requireSpendingKey, TransparentCoinbasePolicy::Allow);
if (!selector.has_value()) { if (!selector.has_value()) {
throw JSONRPCError( throw JSONRPCError(
RPC_INVALID_PARAMETER, RPC_INVALID_PARAMETER,
"Error: the wallet does not recognize the specified viewing key."); "Error: the wallet does not recognize the specified viewing key.");
} }
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf, asOfHeight); auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), minconf, asOfHeight);
CAmount transparentBalance = 0; CAmount transparentBalance = 0;
CAmount sproutBalance = 0; CAmount sproutBalance = 0;
@ -4058,14 +4058,14 @@ UniValue z_getbalanceforaccount(const UniValue& params, bool fHelp)
LOCK2(cs_main, pwalletMain->cs_wallet); LOCK2(cs_main, pwalletMain->cs_wallet);
// Get the receivers for this account. // Get the receivers for this account.
auto selector = pwalletMain->ZTXOSelectorForAccount(account, false); auto selector = pwalletMain->ZTXOSelectorForAccount(account, false, TransparentCoinbasePolicy::Allow);
if (!selector.has_value()) { if (!selector.has_value()) {
throw JSONRPCError( throw JSONRPCError(
RPC_INVALID_PARAMETER, RPC_INVALID_PARAMETER,
tfm::format("Error: account %d has not been generated by z_getnewaccount.", account)); tfm::format("Error: account %d has not been generated by z_getnewaccount.", account));
} }
auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), true, minconf, asOfHeight); auto spendableInputs = pwalletMain->FindSpendableInputs(selector.value(), minconf, asOfHeight);
// Accounts never contain Sprout notes. // Accounts never contain Sprout notes.
assert(spendableInputs.sproutNoteEntries.empty()); assert(spendableInputs.sproutNoteEntries.empty());
@ -4637,7 +4637,7 @@ UniValue z_getoperationstatus_IMPL(const UniValue& params, bool fRemoveFinishedO
size_t EstimateTxSize( size_t EstimateTxSize(
const ZTXOSelector& ztxoSelector, const ZTXOSelector& ztxoSelector,
const std::vector<ResolvedPayment>& recipients, const std::vector<Payment>& recipients,
int nextBlockHeight) { int nextBlockHeight) {
CMutableTransaction mtx; CMutableTransaction mtx;
mtx.fOverwintered = true; mtx.fOverwintered = true;
@ -4651,7 +4651,7 @@ size_t EstimateTxSize(
size_t txsize = 0; size_t txsize = 0;
size_t taddrRecipientCount = 0; size_t taddrRecipientCount = 0;
size_t orchardRecipientCount = 0; size_t orchardRecipientCount = 0;
for (const ResolvedPayment& recipient : recipients) { for (const Payment& recipient : recipients) {
std::visit(match { std::visit(match {
[&](const CKeyID&) { [&](const CKeyID&) {
taddrRecipientCount += 1; taddrRecipientCount += 1;
@ -4667,15 +4667,17 @@ size_t EstimateTxSize(
jsdesc.proof = GrothProof(); jsdesc.proof = GrothProof();
mtx.vJoinSplit.push_back(jsdesc); mtx.vJoinSplit.push_back(jsdesc);
}, },
[&](const libzcash::OrchardRawAddress& addr) { [&](const libzcash::UnifiedAddress& addr) {
if (fromSprout) { if (addr.GetOrchardReceiver().has_value()) {
throw JSONRPCError( orchardRecipientCount += 1;
RPC_INVALID_PARAMETER, } else if (addr.GetSaplingReceiver().has_value()) {
"Sending funds from a Sprout address to a Unified Address is not supported by z_sendmany"); mtx.vShieldedOutput.push_back(OutputDescription());
} else if (addr.GetP2PKHReceiver().has_value()
|| addr.GetP2SHReceiver().has_value()) {
taddrRecipientCount += 1;
} }
orchardRecipientCount += 1;
} }
}, recipient.address); }, recipient.GetAddress());
} }
bool nu5Active = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_NU5); bool nu5Active = Params().GetConsensus().NetworkUpgradeActive(nextBlockHeight, Consensus::UPGRADE_NU5);
@ -4798,65 +4800,18 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
} }
} }
bool involvesUnifiedAddress = false;
bool involvesOrchard = false;
// Check that the from address is valid.
// Unified address (UA) allowed here (#5185)
auto fromaddress = params[0].get_str();
ZTXOSelector ztxoSelector = [&]() {
if (fromaddress == "ANY_TADDR") {
return CWallet::LegacyTransparentZTXOSelector(true);
} else {
auto decoded = keyIO.DecodePaymentAddress(fromaddress);
if (!decoded.has_value()) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address: should be a taddr, zaddr, UA, or the string 'ANY_TADDR'.");
}
auto ztxoSelectorOpt = pwalletMain->ZTXOSelectorForAddress(
decoded.value(),
true,
// LegacyCompat does not include AllowLinkingAccountAddresses.
maybeStrategy.has_value() ? maybeStrategy.value().AllowLinkingAccountAddresses() : false);
if (!ztxoSelectorOpt.has_value()) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address, no payment source found for address.");
}
auto selectorAccount = pwalletMain->FindAccountForSelector(ztxoSelectorOpt.value());
std::visit(match {
[&](const libzcash::UnifiedAddress& ua) {
if (!selectorAccount.has_value() || selectorAccount.value() == ZCASH_LEGACY_ACCOUNT) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address, UA does not correspond to a known account.");
}
involvesUnifiedAddress = true;
involvesOrchard = ua.GetOrchardReceiver().has_value();
},
[&](const auto& other) {
if (selectorAccount.has_value() && selectorAccount.value() != ZCASH_LEGACY_ACCOUNT) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address: is a bare receiver from a Unified Address in this wallet. Provide the UA as returned by z_getaddressforaccount instead.");
}
}
}, decoded.value());
return ztxoSelectorOpt.value();
}
}();
UniValue outputs = params[1].get_array(); 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."); throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amounts array is empty.");
} }
std::set<RecipientAddress> recipientAddrs; bool involvesUnifiedAddress = false;
std::vector<ResolvedPayment> recipients; auto tcoinbasePolicy =
maybeStrategy.has_value() && maybeStrategy.value().AllowRevealedSenders()
? TransparentCoinbasePolicy::Allow
: TransparentCoinbasePolicy::Disallow;
std::set<PaymentAddress> recipientAddrs;
std::vector<Payment> recipients;
CAmount nTotalOut = 0; CAmount nTotalOut = 0;
size_t nOrchardOutputs = 0; size_t nOrchardOutputs = 0;
for (const UniValue& o : outputs.getValues()) { for (const UniValue& o : outputs.getValues()) {
@ -4870,29 +4825,13 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
} }
std::string addrStr = find_value(o, "address").get_str(); std::string addrStr = find_value(o, "address").get_str();
auto decoded = keyIO.DecodePaymentAddress(addrStr); auto addr = keyIO.DecodePaymentAddress(addrStr);
if (!decoded.has_value()) { if (!addr.has_value()) {
throw JSONRPCError( throw JSONRPCError(
RPC_INVALID_PARAMETER, RPC_INVALID_PARAMETER,
std::string("Invalid parameter, unknown address format: ") + addrStr); std::string("Invalid parameter, unknown address format: ") + addrStr);
} }
std::optional<RecipientAddress> addr = std::visit(
SelectRecipientAddress(chainparams.GetConsensus(), nextBlockHeight),
decoded.value());
if (!addr.has_value()) {
bool toSprout = std::holds_alternative<libzcash::SproutPaymentAddress>(decoded.value());
if (toSprout) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Sending funds into the Sprout value pool is not supported by z_sendmany");
} else {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
"Unified address contained only receiver types that are unrecognized or for which the required consensus feature is not yet active.");
}
}
if (!recipientAddrs.insert(addr.value()).second) { if (!recipientAddrs.insert(addr.value()).second) {
throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated recipient address: ") + addrStr); throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, duplicated recipient address: ") + addrStr);
} }
@ -4901,7 +4840,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
std::optional<Memo> memo; std::optional<Memo> memo;
if (!memoValue.isNull()) { if (!memoValue.isNull()) {
auto memoHex = memoValue.get_str(); auto memoHex = memoValue.get_str();
if (!std::visit(libzcash::IsShieldedRecipient(), addr.value())) { if (!std::visit(libzcash::HasShieldedRecipient(), addr.value())) {
throw JSONRPCError( throw JSONRPCError(
RPC_INVALID_PARAMETER, RPC_INVALID_PARAMETER,
"Invalid parameter, memos cannot be sent to transparent addresses."); "Invalid parameter, memos cannot be sent to transparent addresses.");
@ -4934,36 +4873,89 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive"); throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, amount must be positive");
} }
std::optional<libzcash::UnifiedAddress> ua = std::nullopt; std::visit(match {
if (std::holds_alternative<libzcash::UnifiedAddress>(decoded.value())) { [&](const CKeyID &) {
ua = std::get<libzcash::UnifiedAddress>(decoded.value()); tcoinbasePolicy = TransparentCoinbasePolicy::Disallow;
involvesUnifiedAddress = true; },
involvesOrchard = involvesOrchard || ua.value().GetOrchardReceiver().has_value(); [&](const CScriptID &) {
} tcoinbasePolicy = TransparentCoinbasePolicy::Disallow;
},
[&](const UnifiedAddress &ua) {
involvesUnifiedAddress = true;
auto preferredRecipient =
ua.GetPreferredRecipientAddress(chainparams.GetConsensus(), nextBlockHeight);
if (preferredRecipient.has_value()) {
std::visit(match {
[&](const CKeyID &) {
tcoinbasePolicy = TransparentCoinbasePolicy::Disallow;
},
[&](const CScriptID &) {
tcoinbasePolicy = TransparentCoinbasePolicy::Disallow;
},
[](const auto &) { }
}, preferredRecipient.value());
}
},
[](const auto &) { }
}, addr.value());
if (std::holds_alternative<libzcash::OrchardRawAddress>(addr.value())) { recipients.push_back(Payment(addr.value(), nAmount, memo));
nOrchardOutputs += 1;
if (nOrchardOutputs > nOrchardActionLimit) {
throw JSONRPCError(
RPC_INVALID_PARAMETER,
strprintf(
"Attempting to create %u Orchard outputs would exceed the current limit "
"of %u notes, which exists to prevent memory exhaustion. Restart with "
"`-orchardactionlimit=N` where N >= %u to allow the wallet to attempt "
"to construct this transaction.",
nOrchardOutputs,
nOrchardActionLimit,
nOrchardOutputs));
}
}
recipients.push_back(ResolvedPayment(ua, addr.value(), nAmount, memo));
nTotalOut += nAmount; nTotalOut += nAmount;
} }
if (recipients.empty()) { if (recipients.empty()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "No recipients"); throw JSONRPCError(RPC_INVALID_PARAMETER, "No recipients");
} }
// Check that the from address is valid.
// Unified address (UA) allowed here (#5185)
auto fromaddress = params[0].get_str();
ZTXOSelector ztxoSelector = [&]() {
if (fromaddress == "ANY_TADDR") {
return CWallet::LegacyTransparentZTXOSelector(true, TransparentCoinbasePolicy::Disallow);
} else {
auto decoded = keyIO.DecodePaymentAddress(fromaddress);
if (!decoded.has_value()) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address: should be a taddr, zaddr, UA, or the string 'ANY_TADDR'.");
}
auto ztxoSelectorOpt = pwalletMain->ZTXOSelectorForAddress(
decoded.value(),
true,
tcoinbasePolicy,
// LegacyCompat does not include AllowLinkingAccountAddresses.
maybeStrategy.has_value() ? maybeStrategy.value().AllowLinkingAccountAddresses() : false);
if (!ztxoSelectorOpt.has_value()) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address, no payment source found for address.");
}
auto selectorAccount = pwalletMain->FindAccountForSelector(ztxoSelectorOpt.value());
bool unknownOrLegacy = !selectorAccount.has_value() || selectorAccount.value() == ZCASH_LEGACY_ACCOUNT;
std::visit(match {
[&](const libzcash::UnifiedAddress& ua) {
if (unknownOrLegacy) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address, UA does not correspond to a known account.");
}
involvesUnifiedAddress = true;
},
[&](const auto& other) {
if (!unknownOrLegacy) {
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"Invalid from address: is a bare receiver from a Unified Address in this wallet. Provide the UA as returned by z_getaddressforaccount instead.");
}
}
}, decoded.value());
return ztxoSelectorOpt.value();
}
}();
// Now that we've set involvesUnifiedAddress correctly, we can finish // Now that we've set involvesUnifiedAddress correctly, we can finish
// evaluating the strategy. // evaluating the strategy.
TransactionStrategy strategy = maybeStrategy.value_or( TransactionStrategy strategy = maybeStrategy.value_or(
@ -5021,22 +5013,10 @@ UniValue z_sendmany(const UniValue& params, bool fHelp)
o.pushKV("fee", std::stod(FormatMoney(nFee))); o.pushKV("fee", std::stod(FormatMoney(nFee)));
UniValue contextInfo = o; UniValue contextInfo = o;
std::optional<uint256> orchardAnchor;
auto nAnchorDepth = std::min((unsigned int) nMinDepth, nAnchorConfirmations);
if ((ztxoSelector.SelectsOrchard() || nOrchardOutputs > 0) && nAnchorDepth == 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot select Orchard notes or send to Orchard recipients when minconf=0.");
}
if (!ztxoSelector.SelectsSprout() && (involvesOrchard || nPreferredTxVersion >= ZIP225_MIN_TX_VERSION) && nAnchorDepth > 0) {
auto orchardAnchorHeight = nextBlockHeight - nAnchorDepth;
if (chainparams.GetConsensus().NetworkUpgradeActive(orchardAnchorHeight, Consensus::UPGRADE_NU5)) {
auto anchorBlockIndex = chainActive[orchardAnchorHeight];
assert(anchorBlockIndex != nullptr);
orchardAnchor = anchorBlockIndex->hashFinalOrchardRoot;
}
}
TransactionBuilder builder(chainparams.GetConsensus(), nextBlockHeight, orchardAnchor, pwalletMain);
// Create operation and add to global queue // Create operation and add to global queue
auto nAnchorDepth = std::min((unsigned int) nMinDepth, nAnchorConfirmations);
WalletTxBuilder builder(Params(), *pwalletMain, minRelayTxFee);
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue(); std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation( std::shared_ptr<AsyncRPCOperation> operation(
new AsyncRPCOperation_sendmany( new AsyncRPCOperation_sendmany(

View File

@ -150,13 +150,13 @@ BOOST_AUTO_TEST_CASE(rpc_wallet)
CTxDestination setaccountDemoAddress(CTxDestination(setaccountDemoPubkey.GetID())); CTxDestination setaccountDemoAddress(CTxDestination(setaccountDemoPubkey.GetID()));
/********************************* /*********************************
* getbalance * getbalance
*********************************/ *********************************/
BOOST_CHECK_NO_THROW(CallRPC("getbalance")); BOOST_CHECK_NO_THROW(CallRPC("getbalance"));
BOOST_CHECK_THROW(CallRPC("getbalance " + keyIO.EncodeDestination(demoAddress)), runtime_error); BOOST_CHECK_THROW(CallRPC("getbalance " + keyIO.EncodeDestination(demoAddress)), runtime_error);
/********************************* /*********************************
* listunspent * listunspent
*********************************/ *********************************/
BOOST_CHECK_NO_THROW(CallRPC("listunspent")); BOOST_CHECK_NO_THROW(CallRPC("listunspent"));
BOOST_CHECK_THROW(CallRPC("listunspent string"), runtime_error); BOOST_CHECK_THROW(CallRPC("listunspent string"), runtime_error);
@ -167,7 +167,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet)
BOOST_CHECK(r.get_array().empty()); BOOST_CHECK(r.get_array().empty());
/********************************* /*********************************
* listreceivedbyaddress * listreceivedbyaddress
*********************************/ *********************************/
BOOST_CHECK_NO_THROW(CallRPC("listreceivedbyaddress")); BOOST_CHECK_NO_THROW(CallRPC("listreceivedbyaddress"));
BOOST_CHECK_NO_THROW(CallRPC("listreceivedbyaddress 0")); BOOST_CHECK_NO_THROW(CallRPC("listreceivedbyaddress 0"));
@ -201,22 +201,22 @@ BOOST_AUTO_TEST_CASE(rpc_wallet)
BOOST_CHECK_NO_THROW(CallRPC("listaddressgroupings")); BOOST_CHECK_NO_THROW(CallRPC("listaddressgroupings"));
/********************************* /*********************************
* walletconfirmbackup * walletconfirmbackup
*********************************/ *********************************/
BOOST_CHECK_THROW(CallRPC(string("walletconfirmbackup \"badmnemonic\"")), runtime_error); BOOST_CHECK_THROW(CallRPC(string("walletconfirmbackup \"badmnemonic\"")), runtime_error);
/********************************* /*********************************
* getrawchangeaddress * getrawchangeaddress
*********************************/ *********************************/
BOOST_CHECK_NO_THROW(CallRPC("getrawchangeaddress")); BOOST_CHECK_NO_THROW(CallRPC("getrawchangeaddress"));
/********************************* /*********************************
* getnewaddress * getnewaddress
*********************************/ *********************************/
BOOST_CHECK_NO_THROW(CallRPC("getnewaddress")); BOOST_CHECK_NO_THROW(CallRPC("getnewaddress"));
/********************************* /*********************************
* signmessage + verifymessage * signmessage + verifymessage
*********************************/ *********************************/
BOOST_CHECK_NO_THROW(retValue = CallRPC("signmessage " + keyIO.EncodeDestination(demoAddress) + " mymessage")); BOOST_CHECK_NO_THROW(retValue = CallRPC("signmessage " + keyIO.EncodeDestination(demoAddress) + " mymessage"));
BOOST_CHECK_THROW(CallRPC("signmessage"), runtime_error); BOOST_CHECK_THROW(CallRPC("signmessage"), runtime_error);
@ -236,7 +236,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet)
BOOST_CHECK(CallRPC("verifymessage " + keyIO.EncodeDestination(demoAddress) + " " + retValue.get_str() + " mymessage").get_bool() == true); BOOST_CHECK(CallRPC("verifymessage " + keyIO.EncodeDestination(demoAddress) + " " + retValue.get_str() + " mymessage").get_bool() == true);
/********************************* /*********************************
* listaddresses * listaddresses
*********************************/ *********************************/
BOOST_CHECK_NO_THROW(retValue = CallRPC("listaddresses")); BOOST_CHECK_NO_THROW(retValue = CallRPC("listaddresses"));
UniValue arr = retValue.get_array(); UniValue arr = retValue.get_array();
@ -258,7 +258,7 @@ BOOST_AUTO_TEST_CASE(rpc_wallet)
} }
/********************************* /*********************************
* fundrawtransaction * fundrawtransaction
*********************************/ *********************************/
BOOST_CHECK_THROW(CallRPC("fundrawtransaction 28z"), runtime_error); BOOST_CHECK_THROW(CallRPC("fundrawtransaction 28z"), runtime_error);
BOOST_CHECK_THROW(CallRPC("fundrawtransaction 01000000000180969800000000001976a91450ce0a4b0ee0ddeb633da85199728b940ac3fe9488ac00000000"), runtime_error); BOOST_CHECK_THROW(CallRPC("fundrawtransaction 01000000000180969800000000001976a91450ce0a4b0ee0ddeb633da85199728b940ac3fe9488ac00000000"), runtime_error);
@ -802,7 +802,11 @@ void CheckHaveAddr(const std::optional<libzcash::PaymentAddress>& addr) {
auto addr_of_type = std::get_if<ADDR_TYPE>(&(addr.value())); auto addr_of_type = std::get_if<ADDR_TYPE>(&(addr.value()));
BOOST_ASSERT(addr_of_type != nullptr); BOOST_ASSERT(addr_of_type != nullptr);
BOOST_CHECK(pwalletMain->ZTXOSelectorForAddress(*addr_of_type, true, false).has_value()); BOOST_CHECK(pwalletMain->ZTXOSelectorForAddress(
*addr_of_type,
true,
TransparentCoinbasePolicy::Allow,
false).has_value());
} }
BOOST_AUTO_TEST_CASE(rpc_wallet_z_getnewaddress) { BOOST_AUTO_TEST_CASE(rpc_wallet_z_getnewaddress) {
@ -1236,10 +1240,17 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// there are no utxos to spend // there are no utxos to spend
{ {
auto selector = pwalletMain->ZTXOSelectorForAddress(taddr1, true, false).value(); auto selector = pwalletMain->ZTXOSelectorForAddress(
TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain); taddr1,
std::vector<ResolvedPayment> recipients = { ResolvedPayment(std::nullopt, zaddr1, 100*COIN, Memo::FromHexOrThrow("DEADBEEF")) }; true,
TransactionStrategy strategy; // In the real transaction builder we use either Require or Disallow, but here we
// are checking that there are no UTXOs at all, so we allow either to be selected to
// confirm this.
TransparentCoinbasePolicy::Allow,
false).value();
WalletTxBuilder builder(Params(), *pwalletMain, minRelayTxFee);
std::vector<Payment> recipients = { Payment(zaddr1, 100*COIN, Memo::FromHexOrThrow("DEADBEEF")) };
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedSenders);
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy)); std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy));
operation->main(); operation->main();
BOOST_CHECK(operation->isFailed()); BOOST_CHECK(operation->isFailed());
@ -1249,9 +1260,13 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// there are no unspent notes to spend // there are no unspent notes to spend
{ {
auto selector = pwalletMain->ZTXOSelectorForAddress(zaddr1, true, false).value(); auto selector = pwalletMain->ZTXOSelectorForAddress(
TransactionBuilder builder(consensusParams, nHeight + 1, std::nullopt, pwalletMain); zaddr1,
std::vector<ResolvedPayment> recipients = { ResolvedPayment(std::nullopt, taddr1, 100*COIN, Memo::FromHexOrThrow("DEADBEEF")) }; true,
TransparentCoinbasePolicy::Disallow,
false).value();
WalletTxBuilder builder(Params(), *pwalletMain, minRelayTxFee);
std::vector<Payment> recipients = { Payment(taddr1, 100*COIN, Memo::FromHexOrThrow("DEADBEEF")) };
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedRecipients); TransactionStrategy strategy(PrivacyPolicy::AllowRevealedRecipients);
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy)); std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy));
operation->main(); operation->main();

View File

@ -1871,6 +1871,7 @@ void CWallet::SyncMetaData(pair<typename TxSpendMap<T>::iterator, typename TxSpe
std::optional<ZTXOSelector> CWallet::ZTXOSelectorForAccount( std::optional<ZTXOSelector> CWallet::ZTXOSelectorForAccount(
libzcash::AccountId account, libzcash::AccountId account,
bool requireSpendingKey, bool requireSpendingKey,
TransparentCoinbasePolicy transparentCoinbasePolicy,
std::set<libzcash::ReceiverType> receiverTypes) const std::set<libzcash::ReceiverType> receiverTypes) const
{ {
if (mnemonicHDChain.has_value() && if (mnemonicHDChain.has_value() &&
@ -1878,7 +1879,10 @@ std::optional<ZTXOSelector> CWallet::ZTXOSelectorForAccount(
std::make_pair(mnemonicHDChain.value().GetSeedFingerprint(), account) std::make_pair(mnemonicHDChain.value().GetSeedFingerprint(), account)
) > 0) ) > 0)
{ {
return ZTXOSelector(AccountZTXOPattern(account, receiverTypes), requireSpendingKey); return ZTXOSelector(
AccountZTXOPattern(account, receiverTypes),
requireSpendingKey,
transparentCoinbasePolicy);
} else { } else {
return std::nullopt; return std::nullopt;
} }
@ -1887,6 +1891,7 @@ std::optional<ZTXOSelector> CWallet::ZTXOSelectorForAccount(
std::optional<ZTXOSelector> CWallet::ZTXOSelectorForAddress( std::optional<ZTXOSelector> CWallet::ZTXOSelectorForAddress(
const libzcash::PaymentAddress& addr, const libzcash::PaymentAddress& addr,
bool requireSpendingKey, bool requireSpendingKey,
TransparentCoinbasePolicy transparentCoinbasePolicy,
bool allowAddressLinkability) const bool allowAddressLinkability) const
{ {
auto self = this; auto self = this;
@ -1928,14 +1933,12 @@ std::optional<ZTXOSelector> CWallet::ZTXOSelectorForAddress(
pattern = ua; pattern = ua;
} }
} }
} else {
pattern = ua;
} }
} }
}, addr); }, addr);
if (pattern.has_value()) { if (pattern.has_value()) {
return ZTXOSelector(pattern.value(), requireSpendingKey); return ZTXOSelector(pattern.value(), requireSpendingKey, transparentCoinbasePolicy);
} else { } else {
return std::nullopt; return std::nullopt;
} }
@ -1943,7 +1946,8 @@ std::optional<ZTXOSelector> CWallet::ZTXOSelectorForAddress(
std::optional<ZTXOSelector> CWallet::ZTXOSelectorForViewingKey( std::optional<ZTXOSelector> CWallet::ZTXOSelectorForViewingKey(
const libzcash::ViewingKey& vk, const libzcash::ViewingKey& vk,
bool requireSpendingKey) const bool requireSpendingKey,
TransparentCoinbasePolicy transparentCoinbasePolicy) const
{ {
auto self = this; auto self = this;
std::optional<ZTXOPattern> pattern = std::nullopt; std::optional<ZTXOPattern> pattern = std::nullopt;
@ -1970,16 +1974,17 @@ std::optional<ZTXOSelector> CWallet::ZTXOSelectorForViewingKey(
}, vk); }, vk);
if (pattern.has_value()) { if (pattern.has_value()) {
return ZTXOSelector(pattern.value(), requireSpendingKey); return ZTXOSelector(pattern.value(), requireSpendingKey, transparentCoinbasePolicy);
} else { } else {
return std::nullopt; return std::nullopt;
} }
} }
ZTXOSelector CWallet::LegacyTransparentZTXOSelector(bool requireSpendingKey) { ZTXOSelector CWallet::LegacyTransparentZTXOSelector(bool requireSpendingKey, TransparentCoinbasePolicy transparentCoinbasePolicy) {
return ZTXOSelector( return ZTXOSelector(
AccountZTXOPattern(ZCASH_LEGACY_ACCOUNT, {ReceiverType::P2PKH, ReceiverType::P2SH}), AccountZTXOPattern(ZCASH_LEGACY_ACCOUNT, {ReceiverType::P2PKH, ReceiverType::P2SH}),
requireSpendingKey); requireSpendingKey,
transparentCoinbasePolicy);
} }
std::optional<libzcash::AccountId> CWallet::FindAccountForSelector(const ZTXOSelector& selector) const { std::optional<libzcash::AccountId> CWallet::FindAccountForSelector(const ZTXOSelector& selector) const {
@ -2207,7 +2212,6 @@ std::optional<RecipientAddress> CWallet::GenerateChangeAddressForAccount(
SpendableInputs CWallet::FindSpendableInputs( SpendableInputs CWallet::FindSpendableInputs(
ZTXOSelector selector, ZTXOSelector selector,
bool allowTransparentCoinbase,
uint32_t minDepth, uint32_t minDepth,
const std::optional<int>& asOfHeight) const { const std::optional<int>& asOfHeight) const {
AssertLockHeld(cs_main); AssertLockHeld(cs_main);
@ -2216,7 +2220,6 @@ SpendableInputs CWallet::FindSpendableInputs(
KeyIO keyIO(Params()); KeyIO keyIO(Params());
bool selectTransparent{selector.SelectsTransparent()}; bool selectTransparent{selector.SelectsTransparent()};
bool selectTransparentCoinbase{selector.SelectsTransparentCoinbase()};
bool selectSprout{selector.SelectsSprout()}; bool selectSprout{selector.SelectsSprout()};
bool selectSapling{selector.SelectsSapling()}; bool selectSapling{selector.SelectsSapling()};
bool selectOrchard{selector.SelectsOrchard()}; bool selectOrchard{selector.SelectsOrchard()};
@ -2230,10 +2233,18 @@ SpendableInputs CWallet::FindSpendableInputs(
if (!CheckFinalTx(wtx)) continue; if (!CheckFinalTx(wtx)) continue;
if (nDepth < 0 || nDepth < minDepth) continue; if (nDepth < 0 || nDepth < minDepth) continue;
if (selectTransparent && if (selectTransparent && (
// skip transparent utxo selection if coinbase spend restrictions are not met (
(!isCoinbase || (selectTransparentCoinbase && allowTransparentCoinbase && wtx.GetBlocksToMaturity(asOfHeight) <= 0))) { // Only select coinbase transparent utxos if spend restrictions are met.
isCoinbase &&
selector.transparentCoinbasePolicy != TransparentCoinbasePolicy::Disallow &&
wtx.GetBlocksToMaturity(asOfHeight) <= 0
) || (
// Only select non-coinbase transparent utxos if we are allowed to.
!isCoinbase &&
selector.transparentCoinbasePolicy != TransparentCoinbasePolicy::Require
)
)) {
for (int i = 0; i < wtx.vout.size(); i++) { for (int i = 0; i < wtx.vout.size(); i++) {
const auto& output = wtx.vout[i]; const auto& output = wtx.vout[i];
isminetype mine = IsMine(output); isminetype mine = IsMine(output);
@ -7738,32 +7749,15 @@ bool ZTXOSelector::SelectsTransparent() const {
[](const AccountZTXOPattern& acct) { return acct.IncludesP2PKH() || acct.IncludesP2SH(); } [](const AccountZTXOPattern& acct) { return acct.IncludesP2PKH() || acct.IncludesP2SH(); }
}, this->pattern); }, this->pattern);
} }
bool ZTXOSelector::SelectsTransparentCoinbase() const {
return std::visit(match {
[](const CKeyID& keyId) { return true; },
[](const CScriptID& scriptId) { return true; },
[](const libzcash::SproutPaymentAddress& addr) { return false; },
[](const libzcash::SproutViewingKey& vk) { return false; },
[](const libzcash::SaplingPaymentAddress& addr) { return false; },
[](const libzcash::SaplingExtendedFullViewingKey& vk) { return false; },
[](const libzcash::UnifiedAddress& ua) {
return ua.GetP2PKHReceiver().has_value() || ua.GetP2SHReceiver().has_value();
},
[](const libzcash::UnifiedFullViewingKey& ufvk) { return ufvk.GetTransparentKey().has_value(); },
[](const AccountZTXOPattern& acct) {
return (acct.IncludesP2PKH() || acct.IncludesP2SH()) && acct.GetAccountId() != ZCASH_LEGACY_ACCOUNT;
}
}, this->pattern);
}
bool ZTXOSelector::SelectsSprout() const { bool ZTXOSelector::SelectsSprout() const {
return std::visit(match { return transparentCoinbasePolicy != TransparentCoinbasePolicy::Require && std::visit(match {
[](const libzcash::SproutViewingKey& addr) { return true; }, [](const libzcash::SproutViewingKey& addr) { return true; },
[](const libzcash::SproutPaymentAddress& extfvk) { return true; }, [](const libzcash::SproutPaymentAddress& extfvk) { return true; },
[](const auto& addr) { return false; } [](const auto& addr) { return false; }
}, this->pattern); }, this->pattern);
} }
bool ZTXOSelector::SelectsSapling() const { bool ZTXOSelector::SelectsSapling() const {
return std::visit(match { return transparentCoinbasePolicy != TransparentCoinbasePolicy::Require && std::visit(match {
[](const libzcash::SaplingPaymentAddress& addr) { return true; }, [](const libzcash::SaplingPaymentAddress& addr) { return true; },
[](const libzcash::SaplingExtendedSpendingKey& extfvk) { return true; }, [](const libzcash::SaplingExtendedSpendingKey& extfvk) { return true; },
[](const libzcash::UnifiedAddress& ua) { return ua.GetSaplingReceiver().has_value(); }, [](const libzcash::UnifiedAddress& ua) { return ua.GetSaplingReceiver().has_value(); },
@ -7773,7 +7767,7 @@ bool ZTXOSelector::SelectsSapling() const {
}, this->pattern); }, this->pattern);
} }
bool ZTXOSelector::SelectsOrchard() const { bool ZTXOSelector::SelectsOrchard() const {
return std::visit(match { return transparentCoinbasePolicy != TransparentCoinbasePolicy::Require && std::visit(match {
[](const libzcash::UnifiedAddress& ua) { return ua.GetOrchardReceiver().has_value(); }, [](const libzcash::UnifiedAddress& ua) { return ua.GetOrchardReceiver().has_value(); },
[](const libzcash::UnifiedFullViewingKey& ufvk) { return ufvk.GetOrchardKey().has_value(); }, [](const libzcash::UnifiedFullViewingKey& ufvk) { return ufvk.GetOrchardKey().has_value(); },
[](const AccountZTXOPattern& acct) { return acct.IncludesOrchard(); }, [](const AccountZTXOPattern& acct) { return acct.IncludesOrchard(); },

View File

@ -904,13 +904,27 @@ typedef std::variant<
libzcash::UnifiedFullViewingKey, libzcash::UnifiedFullViewingKey,
AccountZTXOPattern> ZTXOPattern; AccountZTXOPattern> ZTXOPattern;
/**
* For transactions, either `Disallow` or `Require` must be used, but `Allow` is generally used when
* calculating balances.
*/
enum class TransparentCoinbasePolicy {
Disallow, //!< Do not select transparent coinbase
Allow, //!< Make transparent coinbase available to the selector
Require //!< Only select transparent coinbase
};
class ZTXOSelector { class ZTXOSelector {
private: private:
ZTXOPattern pattern; ZTXOPattern pattern;
bool requireSpendingKeys; bool requireSpendingKeys;
TransparentCoinbasePolicy transparentCoinbasePolicy;
ZTXOSelector(ZTXOPattern patternIn, bool requireSpendingKeysIn): ZTXOSelector(ZTXOPattern patternIn, bool requireSpendingKeysIn, TransparentCoinbasePolicy transparentCoinbasePolicy):
pattern(patternIn), requireSpendingKeys(requireSpendingKeysIn) {} pattern(patternIn), requireSpendingKeys(requireSpendingKeysIn), transparentCoinbasePolicy(transparentCoinbasePolicy) {
// We cant require transparent coinbase unless were selecting transparent funds.
assert(SelectsTransparent() || transparentCoinbasePolicy != TransparentCoinbasePolicy::Require);
}
friend class CWallet; friend class CWallet;
public: public:
@ -922,8 +936,11 @@ public:
return requireSpendingKeys; return requireSpendingKeys;
} }
TransparentCoinbasePolicy TransparentCoinbasePolicy() const {
return transparentCoinbasePolicy;
}
bool SelectsTransparent() const; bool SelectsTransparent() const;
bool SelectsTransparentCoinbase() const;
bool SelectsSprout() const; bool SelectsSprout() const;
bool SelectsSapling() const; bool SelectsSapling() const;
bool SelectsOrchard() const; bool SelectsOrchard() const;
@ -967,14 +984,14 @@ public:
*/ */
CAmount Total() const { CAmount Total() const {
CAmount result = 0; CAmount result = 0;
result += GetTransparentBalance(); result += GetTransparentTotal();
result += GetSproutBalance(); result += GetSproutTotal();
result += GetSaplingBalance(); result += GetSaplingTotal();
result += GetOrchardBalance(); result += GetOrchardTotal();
return result; return result;
} }
CAmount GetTransparentBalance() const { CAmount GetTransparentTotal() const {
CAmount result = 0; CAmount result = 0;
for (const auto& t : utxos) { for (const auto& t : utxos) {
result += t.Value(); result += t.Value();
@ -982,7 +999,7 @@ public:
return result; return result;
} }
CAmount GetSproutBalance() const { CAmount GetSproutTotal() const {
CAmount result = 0; CAmount result = 0;
for (const auto& t : sproutNoteEntries) { for (const auto& t : sproutNoteEntries) {
result += t.note.value(); result += t.note.value();
@ -990,7 +1007,7 @@ public:
return result; return result;
} }
CAmount GetSaplingBalance() const { CAmount GetSaplingTotal() const {
CAmount result = 0; CAmount result = 0;
for (const auto& t : saplingNoteEntries) { for (const auto& t : saplingNoteEntries) {
result += t.note.value(); result += t.note.value();
@ -998,7 +1015,7 @@ public:
return result; return result;
} }
CAmount GetOrchardBalance() const { CAmount GetOrchardTotal() const {
CAmount result = 0; CAmount result = 0;
for (const auto& t : orchardNoteMetadata) { for (const auto& t : orchardNoteMetadata) {
result += t.GetNoteValue(); result += t.GetNoteValue();
@ -1541,6 +1558,7 @@ public:
std::optional<ZTXOSelector> ZTXOSelectorForAccount( std::optional<ZTXOSelector> ZTXOSelectorForAccount(
libzcash::AccountId account, libzcash::AccountId account,
bool requireSpendingKey, bool requireSpendingKey,
TransparentCoinbasePolicy transparentCoinbasePolicy,
std::set<libzcash::ReceiverType> receiverTypes={}) const; std::set<libzcash::ReceiverType> receiverTypes={}) const;
/** /**
@ -1552,6 +1570,7 @@ public:
std::optional<ZTXOSelector> ZTXOSelectorForAddress( std::optional<ZTXOSelector> ZTXOSelectorForAddress(
const libzcash::PaymentAddress& addr, const libzcash::PaymentAddress& addr,
bool requireSpendingKey, bool requireSpendingKey,
TransparentCoinbasePolicy transparentCoinbasePolicy,
bool allowAddressLinkability) const; bool allowAddressLinkability) const;
/** /**
@ -1562,13 +1581,14 @@ public:
*/ */
std::optional<ZTXOSelector> ZTXOSelectorForViewingKey( std::optional<ZTXOSelector> ZTXOSelectorForViewingKey(
const libzcash::ViewingKey& vk, const libzcash::ViewingKey& vk,
bool requireSpendingKey) const; bool requireSpendingKey,
TransparentCoinbasePolicy transparentCoinbasePolicy) const;
/** /**
* Returns the ZTXO selector that will select UTXOs sent to legacy * Returns the ZTXO selector that will select UTXOs sent to legacy
* transparent addresses managed by this wallet. * transparent addresses managed by this wallet.
*/ */
static ZTXOSelector LegacyTransparentZTXOSelector(bool requireSpendingKey); static ZTXOSelector LegacyTransparentZTXOSelector(bool requireSpendingKey, TransparentCoinbasePolicy transparentCoinbasePolicy);
/** /**
* Look up the account for a given selector. This resolves the account ID * Look up the account for a given selector. This resolves the account ID
@ -1598,7 +1618,6 @@ public:
SpendableInputs FindSpendableInputs( SpendableInputs FindSpendableInputs(
ZTXOSelector paymentSource, ZTXOSelector paymentSource,
bool allowTransparentCoinbase,
uint32_t minDepth, uint32_t minDepth,
const std::optional<int>& asOfHeight) const; const std::optional<int>& asOfHeight) const;

View File

@ -0,0 +1,630 @@
// Copyright (c) 2022 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
#include "wallet/wallet_tx_builder.h"
using namespace libzcash;
int GetAnchorHeight(const CChain& chain, uint32_t anchorConfirmations)
{
int nextBlockHeight = chain.Height() + 1;
return nextBlockHeight - anchorConfirmations;
}
PrepareTransactionResult WalletTxBuilder::PrepareTransaction(
const ZTXOSelector& selector,
SpendableInputs& spendable,
const std::vector<Payment>& payments,
const CChain& chain,
TransactionStrategy strategy,
CAmount fee,
uint32_t anchorConfirmations) const
{
assert(fee < MAX_MONEY);
int anchorHeight = GetAnchorHeight(chain, anchorConfirmations);
auto selected = ResolveInputsAndPayments(selector, spendable, payments, chain, strategy, fee, anchorHeight);
if (std::holds_alternative<InputSelectionError>(selected)) {
return std::get<InputSelectionError>(selected);
}
auto resolvedSelection = std::get<InputSelection>(selected);
auto resolvedPayments = resolvedSelection.GetPayments();
// We do not set a change address if there is no change.
std::optional<ChangeAddress> changeAddr;
auto changeAmount = spendable.Total() - resolvedPayments.Total() - fee;
if (changeAmount > 0) {
// Determine the account we're sending from.
auto sendFromAccount = wallet.FindAccountForSelector(selector).value_or(ZCASH_LEGACY_ACCOUNT);
auto getAllowedChangePools = [&](const std::set<ReceiverType>& receiverTypes) {
std::set<OutputPool> result{resolvedPayments.GetRecipientPools()};
// We always allow shielded change when not sending from the legacy account.
if (sendFromAccount != ZCASH_LEGACY_ACCOUNT) {
result.insert(OutputPool::Sapling);
}
for (ReceiverType rtype : receiverTypes) {
switch (rtype) {
case ReceiverType::P2PKH:
case ReceiverType::P2SH:
// TODO: This is the correct policy, but its a breaking change from
// previous behavior, so enable it separately. (#6409)
// if (strategy.AllowRevealedRecipients()) {
if (!spendable.utxos.empty() || strategy.AllowRevealedRecipients()) {
result.insert(OutputPool::Transparent);
}
break;
case ReceiverType::Sapling:
if (!spendable.saplingNoteEntries.empty() || strategy.AllowRevealedAmounts()) {
result.insert(OutputPool::Sapling);
}
break;
case ReceiverType::Orchard:
if (params.GetConsensus().NetworkUpgradeActive(anchorHeight, Consensus::UPGRADE_NU5)
&& (!spendable.orchardNoteMetadata.empty() || strategy.AllowRevealedAmounts())) {
result.insert(OutputPool::Orchard);
}
break;
}
}
return result;
};
auto addChangePayment = [&](const std::optional<RecipientAddress>& sendTo) {
assert(sendTo.has_value());
resolvedPayments.AddPayment(
ResolvedPayment(std::nullopt, sendTo.value(), changeAmount, std::nullopt, true));
return sendTo.value();
};
auto changeAddressForTransparentSelector = [&](const std::set<ReceiverType>& receiverTypes) {
return addChangePayment(
pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount,
getAllowedChangePools(receiverTypes)));
};
auto changeAddressForSaplingAddress = [&](const libzcash::SaplingPaymentAddress& addr) {
// for Sapling, if using a legacy address, return change to the
// originating address; otherwise return it to the Sapling internal
// address corresponding to the UFVK.
return addChangePayment(
sendFromAccount == ZCASH_LEGACY_ACCOUNT
? addr
: pwalletMain->GenerateChangeAddressForAccount(
sendFromAccount,
getAllowedChangePools({ReceiverType::Sapling})));
};
auto changeAddressForZUFVK = [&](
const ZcashdUnifiedFullViewingKey& zufvk,
const std::set<ReceiverType>& receiverTypes) {
return addChangePayment(zufvk.GetChangeAddress(getAllowedChangePools(receiverTypes)));
};
changeAddr = std::visit(match {
[&](const CKeyID&) -> ChangeAddress {
return changeAddressForTransparentSelector({ReceiverType::P2PKH});
},
[&](const CScriptID&) -> ChangeAddress {
return changeAddressForTransparentSelector({ReceiverType::P2SH});
},
[](const libzcash::SproutPaymentAddress& addr) -> ChangeAddress {
// for Sprout, we return change to the originating address using the tx builder.
return addr;
},
[](const libzcash::SproutViewingKey& vk) -> ChangeAddress {
// for Sprout, we return change to the originating address using the tx builder.
return vk.address();
},
[&](const libzcash::SaplingPaymentAddress& addr) -> ChangeAddress {
return changeAddressForSaplingAddress(addr);
},
[&](const libzcash::SaplingExtendedFullViewingKey& fvk) -> ChangeAddress {
return changeAddressForSaplingAddress(fvk.DefaultAddress());
},
[&](const libzcash::UnifiedAddress& ua) -> ChangeAddress {
auto zufvk = pwalletMain->GetUFVKForAddress(ua);
assert(zufvk.has_value());
return changeAddressForZUFVK(zufvk.value(), ua.GetKnownReceiverTypes());
},
[&](const libzcash::UnifiedFullViewingKey& fvk) -> ChangeAddress {
return changeAddressForZUFVK(
ZcashdUnifiedFullViewingKey::FromUnifiedFullViewingKey(params, fvk),
fvk.GetKnownReceiverTypes());
},
[&](const AccountZTXOPattern& acct) -> ChangeAddress {
return addChangePayment(
pwalletMain->GenerateChangeAddressForAccount(
acct.GetAccountId(),
getAllowedChangePools(acct.GetReceiverTypes())));
}
}, selector.GetPattern());
}
auto ovks = SelectOVKs(selector, spendable);
return TransactionEffects(
anchorConfirmations,
spendable,
resolvedPayments,
changeAddr,
fee,
ovks.first,
ovks.second,
anchorHeight);
}
Payments InputSelection::GetPayments() const {
return this->payments;
}
CAmount WalletTxBuilder::DefaultDustThreshold() const {
CKey secret{CKey::TestOnlyRandomKey(true)};
CScript scriptPubKey = GetScriptForDestination(secret.GetPubKey().GetID());
CTxOut txout(CAmount(1), scriptPubKey);
return txout.GetDustThreshold(minRelayFee);
}
SpendableInputs WalletTxBuilder::FindAllSpendableInputs(
const ZTXOSelector& selector,
int32_t minDepth) const
{
LOCK2(cs_main, wallet.cs_wallet);
return wallet.FindSpendableInputs(selector, minDepth, std::nullopt);
}
InputSelectionResult WalletTxBuilder::ResolveInputsAndPayments(
const ZTXOSelector& selector,
SpendableInputs& spendableMut,
const std::vector<Payment>& payments,
const CChain& chain,
TransactionStrategy strategy,
CAmount fee,
int anchorHeight) const
{
LOCK2(cs_main, wallet.cs_wallet);
// Determine the target totals
CAmount sendAmount{0};
for (const auto& payment : payments) {
sendAmount += payment.GetAmount();
}
CAmount targetAmount = sendAmount + fee;
// This is a simple greedy algorithm to attempt to preserve requested
// transactional privacy while moving as much value to the most recent pool
// as possible. This will also perform opportunistic shielding if the
// transaction strategy permits.
CAmount maxSaplingAvailable = spendableMut.GetSaplingTotal();
CAmount maxOrchardAvailable = spendableMut.GetOrchardTotal();
uint32_t orchardOutputs{0};
// we can only select Orchard addresses if there are sufficient non-Sprout
// funds to cover the total payments + fee.
bool canResolveOrchard =
params.GetConsensus().NetworkUpgradeActive(anchorHeight, Consensus::UPGRADE_NU5)
&& spendableMut.Total() - spendableMut.GetSproutTotal() >= targetAmount;
std::vector<ResolvedPayment> resolvedPayments;
std::optional<AddressResolutionError> resolutionError;
for (const auto& payment : payments) {
std::visit(match {
[&](const CKeyID& p2pkh) {
if (strategy.AllowRevealedRecipients()) {
resolvedPayments.emplace_back(
std::nullopt, p2pkh, payment.GetAmount(), payment.GetMemo(), false);
} else {
resolutionError = AddressResolutionError::TransparentRecipientNotAllowed;
}
},
[&](const CScriptID& p2sh) {
if (strategy.AllowRevealedRecipients()) {
resolvedPayments.emplace_back(
std::nullopt, p2sh, payment.GetAmount(), payment.GetMemo(), false);
} else {
resolutionError = AddressResolutionError::TransparentRecipientNotAllowed;
}
},
[&](const SproutPaymentAddress&) {
resolutionError = AddressResolutionError::SproutRecipientsNotSupported;
},
[&](const SaplingPaymentAddress& addr) {
if (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxSaplingAvailable) {
resolvedPayments.emplace_back(
std::nullopt, addr, payment.GetAmount(), payment.GetMemo(), false);
if (!strategy.AllowRevealedAmounts()) {
maxSaplingAvailable -= payment.GetAmount();
}
} else {
resolutionError = AddressResolutionError::RevealingSaplingAmountNotAllowed;
}
},
[&](const UnifiedAddress& ua) {
if (canResolveOrchard
&& ua.GetOrchardReceiver().has_value()
&& (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxOrchardAvailable)
) {
resolvedPayments.emplace_back(
ua, ua.GetOrchardReceiver().value(), payment.GetAmount(), payment.GetMemo(), false);
if (!strategy.AllowRevealedAmounts()) {
maxOrchardAvailable -= payment.GetAmount();
}
orchardOutputs += 1;
} else if (ua.GetSaplingReceiver().has_value()
&& (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxSaplingAvailable)
) {
resolvedPayments.emplace_back(
ua, ua.GetSaplingReceiver().value(), payment.GetAmount(), payment.GetMemo(), false);
if (!strategy.AllowRevealedAmounts()) {
maxSaplingAvailable -= payment.GetAmount();
}
} else {
if (strategy.AllowRevealedRecipients()) {
if (ua.GetP2SHReceiver().has_value()) {
resolvedPayments.emplace_back(
ua, ua.GetP2SHReceiver().value(), payment.GetAmount(), std::nullopt, false);
} else if (ua.GetP2PKHReceiver().has_value()) {
resolvedPayments.emplace_back(
ua, ua.GetP2PKHReceiver().value(), payment.GetAmount(), std::nullopt, false);
} else {
// This should only occur when we have
// • an Orchard-only UA,
// • `AllowRevealedRecipients`, and
// • cant resolve Orchard (which means either insufficient non-Sprout
// funds or pre-NU5).
resolutionError = AddressResolutionError::CouldNotResolveReceiver;
}
} else if (strategy.AllowRevealedAmounts()) {
resolutionError = AddressResolutionError::TransparentReceiverNotAllowed;
} else {
resolutionError = AddressResolutionError::RevealingReceiverAmountsNotAllowed;
}
}
}
}, payment.GetAddress());
if (resolutionError.has_value()) {
return resolutionError.value();
}
}
auto resolved = Payments(resolvedPayments);
if (orchardOutputs > this->maxOrchardActions) {
return ExcessOrchardActionsError(
ActionSide::Output,
orchardOutputs,
this->maxOrchardActions);
}
// Set the dust threshold so that we can select enough inputs to avoid
// creating dust change amounts.
CAmount dustThreshold{this->DefaultDustThreshold()};
// TODO: the set of recipient pools is not quite sufficient information here; we should
// probably perform note selection at the same time as we're performing resolved payment
// construction above.
if (!spendableMut.LimitToAmount(targetAmount, dustThreshold, resolved.GetRecipientPools())) {
CAmount changeAmount{spendableMut.Total() - targetAmount};
return InvalidFundsError(
spendableMut.Total(),
changeAmount > 0 && changeAmount < dustThreshold
// TODO: we should provide the option for the caller to explicitly
// forego change (definitionally an amount below the dust amount)
// and send the extra to the recipient or the miner fee to avoid
// creating dust change, rather than prohibit them from sending
// entirely in this circumstance.
// (Daira disagrees, as this could leak information to the recipient
// or publically in the fee.)
? InvalidFundsReason(DustThresholdError(dustThreshold, changeAmount))
: InvalidFundsReason(InsufficientFundsError(targetAmount)));
}
// When spending transparent coinbase outputs, all inputs must be fully
// consumed, and they may only be sent to shielded recipients.
if (spendableMut.HasTransparentCoinbase()) {
if (spendableMut.Total() != targetAmount) {
return ChangeNotAllowedError(spendableMut.Total(), targetAmount);
} else if (resolved.HasTransparentRecipient()) {
return AddressResolutionError::TransparentRecipientNotAllowed;
}
}
if (spendableMut.orchardNoteMetadata.size() > this->maxOrchardActions) {
return ExcessOrchardActionsError(
ActionSide::Input,
spendableMut.orchardNoteMetadata.size(),
this->maxOrchardActions);
}
return InputSelection(resolved, anchorHeight);
}
std::pair<uint256, uint256>
GetOVKsForUFVK(const UnifiedFullViewingKey& ufvk, const SpendableInputs& spendable)
{
if (!spendable.orchardNoteMetadata.empty()) {
auto fvk = ufvk.GetOrchardKey();
// Orchard notes will not have been selected if the UFVK does not contain an Orchard key.
assert(fvk.has_value());
return std::make_pair(
fvk.value().ToInternalOutgoingViewingKey(),
fvk.value().ToExternalOutgoingViewingKey());
} else if (!spendable.saplingNoteEntries.empty()) {
auto dfvk = ufvk.GetSaplingKey();
// Sapling notes will not have been selected if the UFVK does not contain a Sapling key.
assert(dfvk.has_value());
return dfvk.value().GetOVKs();
} else if (!spendable.utxos.empty()) {
// Transparent UTXOs will not have been selected if the UFVK does not contain a transparent
// key.
auto tfvk = ufvk.GetTransparentKey();
assert(tfvk.has_value());
return tfvk.value().GetOVKsForShielding();
} else {
// This should be unreachable.
throw std::runtime_error("No spendable inputs.");
}
}
std::pair<uint256, uint256> WalletTxBuilder::SelectOVKs(
const ZTXOSelector& selector,
const SpendableInputs& spendable) const
{
return std::visit(match {
[&](const CKeyID& keyId) {
return wallet.GetLegacyAccountKey().ToAccountPubKey().GetOVKsForShielding();
},
[&](const CScriptID& keyId) {
return wallet.GetLegacyAccountKey().ToAccountPubKey().GetOVKsForShielding();
},
[&](const libzcash::SproutPaymentAddress&) {
return wallet.GetLegacyAccountKey().ToAccountPubKey().GetOVKsForShielding();
},
[&](const libzcash::SproutViewingKey&) {
return wallet.GetLegacyAccountKey().ToAccountPubKey().GetOVKsForShielding();
},
[&](const libzcash::SaplingPaymentAddress& addr) {
libzcash::SaplingExtendedSpendingKey extsk;
assert(wallet.GetSaplingExtendedSpendingKey(addr, extsk));
return extsk.ToXFVK().GetOVKs();
},
[](const libzcash::SaplingExtendedFullViewingKey& sxfvk) {
return sxfvk.GetOVKs();
},
[&](const UnifiedAddress& ua) {
auto ufvk = wallet.GetUFVKForAddress(ua);
// This is safe because spending key checks will have ensured that we have a UFVK
// corresponding to this address.
assert(ufvk.has_value());
return GetOVKsForUFVK(ufvk.value().ToFullViewingKey(), spendable);
},
[&](const UnifiedFullViewingKey& ufvk) {
return GetOVKsForUFVK(ufvk, spendable);
},
[&](const AccountZTXOPattern& acct) {
if (acct.GetAccountId() == ZCASH_LEGACY_ACCOUNT) {
return wallet.GetLegacyAccountKey().ToAccountPubKey().GetOVKsForShielding();
} else {
auto ufvk = wallet.GetUnifiedFullViewingKeyByAccount(acct.GetAccountId());
// By definition, we have a UFVK for every known non-legacy account.
assert(ufvk.has_value());
return GetOVKsForUFVK(ufvk.value().ToFullViewingKey(), spendable);
}
},
}, selector.GetPattern());
}
PrivacyPolicy TransactionEffects::GetRequiredPrivacyPolicy() const
{
if (!spendable.utxos.empty()) {
// TODO: Add a check for whether we need AllowLinkingAccountAddresses here. (#6467)
if (payments.HasTransparentRecipient()) {
// TODO: AllowFullyTransparent is the correct policy, but its a breaking change from
// previous behavior, so enable it separately. (#6409)
// maxPrivacy = PrivacyPolicy::AllowFullyTransparent;
return PrivacyPolicy::AllowRevealedSenders;
} else {
return PrivacyPolicy::AllowRevealedSenders;
}
} else if (payments.HasTransparentRecipient()) {
return PrivacyPolicy::AllowRevealedRecipients;
} else if (!spendable.orchardNoteMetadata.empty() && payments.HasSaplingRecipient()
|| !spendable.saplingNoteEntries.empty() && payments.HasOrchardRecipient()
|| !spendable.sproutNoteEntries.empty() && payments.HasSaplingRecipient()) {
// TODO: This should only trigger when there is a non-zero valueBalance.
return PrivacyPolicy::AllowRevealedAmounts;
} else {
return PrivacyPolicy::FullPrivacy;
}
}
bool TransactionEffects::InvolvesOrchard() const
{
return spendable.GetOrchardTotal() > 0 || payments.HasOrchardRecipient();
}
TransactionBuilderResult TransactionEffects::ApproveAndBuild(
const Consensus::Params& consensus,
const CWallet& wallet,
const CChain& chain,
const TransactionStrategy& strategy) const
{
auto requiredPrivacy = this->GetRequiredPrivacyPolicy();
if (!strategy.IsCompatibleWith(requiredPrivacy)) {
return TransactionBuilderResult(strprintf(
"The specified privacy policy, %s, does not permit the creation of "
"the requested transaction. Select %s to allow this transaction "
"to be constructed.",
strategy.PolicyName(),
TransactionStrategy::ToString(requiredPrivacy)
+ (requiredPrivacy == PrivacyPolicy::NoPrivacy ? "" : " or weaker")));
}
int nextBlockHeight = chain.Height() + 1;
// Allow Orchard recipients by setting an Orchard anchor.
std::optional<uint256> orchardAnchor;
if (spendable.sproutNoteEntries.empty()
&& (InvolvesOrchard() || nPreferredTxVersion > ZIP225_MIN_TX_VERSION)
&& this->anchorConfirmations > 0)
{
LOCK(cs_main);
auto anchorBlockIndex = chain[this->anchorHeight];
assert(anchorBlockIndex != nullptr);
orchardAnchor = anchorBlockIndex->hashFinalOrchardRoot;
}
auto builder = TransactionBuilder(consensus, nextBlockHeight, orchardAnchor, &wallet);
builder.SetFee(fee);
// Track the total of notes that we've added to the builder. This
// shouldn't strictly be necessary, given `spendable.LimitToAmount`
CAmount totalSpend = 0;
// Create Sapling outpoints
std::vector<SaplingOutPoint> saplingOutPoints;
std::vector<SaplingNote> saplingNotes;
std::vector<SaplingExtendedSpendingKey> saplingKeys;
for (const auto& t : spendable.saplingNoteEntries) {
saplingOutPoints.push_back(t.op);
saplingNotes.push_back(t.note);
libzcash::SaplingExtendedSpendingKey saplingKey;
assert(wallet.GetSaplingExtendedSpendingKey(t.address, saplingKey));
saplingKeys.push_back(saplingKey);
totalSpend += t.note.value();
}
// Fetch Sapling anchor and witnesses, and Orchard Merkle paths.
uint256 anchor;
std::vector<std::optional<SaplingWitness>> witnesses;
std::vector<std::pair<libzcash::OrchardSpendingKey, orchard::SpendInfo>> orchardSpendInfo;
{
LOCK(wallet.cs_wallet);
if (!wallet.GetSaplingNoteWitnesses(saplingOutPoints, anchorConfirmations, witnesses, anchor)) {
// This error should not appear once we're nAnchorConfirmations blocks past
// Sapling activation.
return TransactionBuilderResult("Insufficient Sapling witnesses.");
}
if (builder.GetOrchardAnchor().has_value()) {
orchardSpendInfo = wallet.GetOrchardSpendInfo(spendable.orchardNoteMetadata, builder.GetOrchardAnchor().value());
}
}
// Add Orchard spends
for (size_t i = 0; i < orchardSpendInfo.size(); i++) {
auto spendInfo = std::move(orchardSpendInfo[i]);
if (!builder.AddOrchardSpend(
std::move(spendInfo.first),
std::move(spendInfo.second)))
{
return TransactionBuilderResult(
strprintf("Failed to add Orchard note to transaction (check %s for details)", GetDebugLogPath())
);
} else {
totalSpend += spendInfo.second.Value();
}
}
// Add Sapling spends
for (size_t i = 0; i < saplingNotes.size(); i++) {
if (!witnesses[i]) {
return TransactionBuilderResult(strprintf(
"Missing witness for Sapling note at outpoint %s",
spendable.saplingNoteEntries[i].op.ToString()
));
}
builder.AddSaplingSpend(saplingKeys[i].expsk, saplingNotes[i], anchor, witnesses[i].value());
}
// Add outputs
for (const auto& r : payments.GetResolvedPayments()) {
std::visit(match {
[&](const CKeyID& keyId) {
builder.AddTransparentOutput(keyId, r.amount);
},
[&](const CScriptID& scriptId) {
builder.AddTransparentOutput(scriptId, r.amount);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
builder.AddSaplingOutput(
r.isInternal ? internalOVK : externalOVK, addr, r.amount,
r.memo.has_value() ? r.memo.value().ToBytes() : Memo::NoMemo().ToBytes());
},
[&](const libzcash::OrchardRawAddress& addr) {
builder.AddOrchardOutput(
r.isInternal ? internalOVK : externalOVK, addr, r.amount,
r.memo.has_value() ? std::optional(r.memo.value().ToBytes()) : std::nullopt);
}
}, r.address);
}
// Add transparent utxos
for (const auto& out : spendable.utxos) {
const CTxOut& txOut = out.tx->vout[out.i];
builder.AddTransparentInput(COutPoint(out.tx->GetHash(), out.i), txOut.scriptPubKey, txOut.nValue);
totalSpend += txOut.nValue;
}
// Find Sprout witnesses
// When spending notes, take a snapshot of note witnesses and anchors as the treestate will
// change upon arrival of new blocks which contain joinsplit transactions. This is likely
// to happen as creating a chained joinsplit transaction can take longer than the block interval.
// So, we need to take locks on cs_main and wallet.cs_wallet so that the witnesses aren't
// updated.
//
// TODO: these locks would ideally be shared for selection of Sapling anchors and witnesses
// as well.
std::vector<std::optional<SproutWitness>> vSproutWitnesses;
{
LOCK2(cs_main, wallet.cs_wallet);
std::vector<JSOutPoint> vOutPoints;
for (const auto& t : spendable.sproutNoteEntries) {
vOutPoints.push_back(t.jsop);
}
// inputAnchor is not needed by builder.AddSproutInput as it is for Sapling.
uint256 inputAnchor;
if (!wallet.GetSproutNoteWitnesses(vOutPoints, anchorConfirmations, vSproutWitnesses, inputAnchor)) {
// This error should not appear once we're nAnchorConfirmations blocks past
// Sprout activation.
return TransactionBuilderResult("Insufficient Sprout witnesses.");
}
}
// Add Sprout spends
for (int i = 0; i < spendable.sproutNoteEntries.size(); i++) {
const auto& t = spendable.sproutNoteEntries[i];
libzcash::SproutSpendingKey sk;
assert(wallet.GetSproutSpendingKey(t.address, sk));
builder.AddSproutInput(sk, t.note, vSproutWitnesses[i].value());
totalSpend += t.note.value();
}
// TODO: We currently cant store Sprout change in `Payments`, so we only validate the
// spend/output balance in the case that `TransactionBuilder` doesnt need to
// (re)calculate the change. In future, we shouldnt rely on `TransactionBuilder` ever
// calculating change.
if (changeAddr.has_value()) {
std::visit(match {
[&](const SproutPaymentAddress& addr) {
builder.SendChangeToSprout(addr);
},
[&](const RecipientAddress&) {
assert(totalSpend == payments.Total() + fee);
}
}, changeAddr.value());
}
// Build the transaction
return builder.Build();
}

View File

@ -5,24 +5,352 @@
#ifndef ZCASH_WALLET_WALLET_TX_BUILDER_H #ifndef ZCASH_WALLET_WALLET_TX_BUILDER_H
#define ZCASH_WALLET_WALLET_TX_BUILDER_H #define ZCASH_WALLET_WALLET_TX_BUILDER_H
#include "consensus/params.h"
#include "transaction_builder.h"
#include "wallet/memo.h" #include "wallet/memo.h"
#include "wallet/wallet.h"
using namespace libzcash; using namespace libzcash;
int GetAnchorHeight(const CChain& chain, int anchorConfirmations);
/** /**
* A payment that has been resolved to send to a specific * A payment that has been resolved to send to a specific recipient address in a single pool. This
* recipient address in a single pool. * is an internal type that represents both user-requested payment addresses and generated
* (internal) payments (like change).
*/ */
class ResolvedPayment : public RecipientMapping { class ResolvedPayment : public RecipientMapping {
public: public:
CAmount amount; CAmount amount;
std::optional<Memo> memo; std::optional<Memo> memo;
bool isInternal;
ResolvedPayment( ResolvedPayment(
std::optional<libzcash::UnifiedAddress> ua, std::optional<libzcash::UnifiedAddress> ua,
libzcash::RecipientAddress address, libzcash::RecipientAddress address,
CAmount amount, CAmount amount,
std::optional<Memo> memo) : std::optional<Memo> memo,
RecipientMapping(ua, address), amount(amount), memo(memo) {} bool isInternal) :
RecipientMapping(ua, address), amount(amount), memo(memo), isInternal(isInternal) {}
}; };
/**
* A requested payment that has not yet been resolved to a
* specific recipient address.
*/
class Payment {
private:
PaymentAddress address;
CAmount amount;
std::optional<Memo> memo;
public:
Payment(
PaymentAddress address,
CAmount amount,
std::optional<Memo> memo) :
address(address), amount(amount), memo(memo) {}
const PaymentAddress& GetAddress() const {
return address;
}
CAmount GetAmount() const {
return amount;
}
const std::optional<Memo>& GetMemo() const {
return memo;
}
};
class Payments {
private:
std::vector<ResolvedPayment> payments;
std::set<OutputPool> recipientPools;
CAmount t_outputs_total{0};
CAmount sapling_outputs_total{0};
CAmount orchard_outputs_total{0};
public:
Payments(std::vector<ResolvedPayment> payments) {
for (const ResolvedPayment& payment : payments) {
AddPayment(payment);
}
}
void AddPayment(ResolvedPayment payment) {
std::visit(match {
[&](const CKeyID& addr) {
t_outputs_total += payment.amount;
recipientPools.insert(OutputPool::Transparent);
},
[&](const CScriptID& addr) {
t_outputs_total += payment.amount;
recipientPools.insert(OutputPool::Transparent);
},
[&](const libzcash::SaplingPaymentAddress& addr) {
sapling_outputs_total += payment.amount;
recipientPools.insert(OutputPool::Sapling);
},
[&](const libzcash::OrchardRawAddress& addr) {
orchard_outputs_total += payment.amount;
recipientPools.insert(OutputPool::Orchard);
}
}, payment.address);
payments.push_back(payment);
}
std::set<OutputPool> GetRecipientPools() const {
return recipientPools;
}
bool HasTransparentRecipient() const {
return recipientPools.count(OutputPool::Transparent) > 0;
}
bool HasSaplingRecipient() const {
return recipientPools.count(OutputPool::Sapling) > 0;
}
bool HasOrchardRecipient() const {
return recipientPools.count(OutputPool::Orchard) > 0;
}
const std::vector<ResolvedPayment>& GetResolvedPayments() const {
return payments;
}
CAmount GetTransparentTotal() const {
return t_outputs_total;
}
CAmount GetSaplingTotal() const {
return sapling_outputs_total;
}
CAmount GetOrchardTotal() const {
return orchard_outputs_total;
}
CAmount Total() const {
return
t_outputs_total +
sapling_outputs_total +
orchard_outputs_total;
}
};
typedef std::variant<
RecipientAddress,
SproutPaymentAddress> ChangeAddress;
class TransactionEffects {
private:
uint32_t anchorConfirmations{1};
SpendableInputs spendable;
Payments payments;
std::optional<ChangeAddress> changeAddr;
CAmount fee{0};
uint256 internalOVK;
uint256 externalOVK;
// TODO: This needs to be richer, like an `anchorBlock`, so the `TransactionEffects` can
// be recalculated if the state of the chain has changed significantly between the time of
// preparation and the time of approval.
int anchorHeight;
public:
TransactionEffects(
uint32_t anchorConfirmations,
SpendableInputs spendable,
Payments payments,
std::optional<ChangeAddress> changeAddr,
CAmount fee,
uint256 internalOVK,
uint256 externalOVK,
int anchorHeight) :
anchorConfirmations(anchorConfirmations),
spendable(spendable),
payments(payments),
changeAddr(changeAddr),
fee(fee),
internalOVK(internalOVK),
externalOVK(externalOVK),
anchorHeight(anchorHeight) {}
/**
* Returns the strongest `PrivacyPolicy` that is compatible with the transactions effects.
*/
PrivacyPolicy GetRequiredPrivacyPolicy() const;
const SpendableInputs& GetSpendable() const {
return spendable;
}
const Payments& GetPayments() const {
return payments;
}
CAmount GetFee() const {
return fee;
}
bool InvolvesOrchard() const;
TransactionBuilderResult ApproveAndBuild(
const Consensus::Params& consensus,
const CWallet& wallet,
const CChain& chain,
const TransactionStrategy& strategy) const;
};
enum class AddressResolutionError {
//! Zcashd no longer supports sending to Sprout.
SproutRecipientsNotSupported,
//! Requested `PrivacyPolicy` doesnt include `AllowRevealedRecipients`
TransparentRecipientNotAllowed,
//! Requested `PrivacyPolicy` doesnt include `AllowRevealedAmounts`, but we dont have enough
//! Sapling funds to avoid revealing amounts
RevealingSaplingAmountNotAllowed,
//! Despite a lax `PrivacyPolicy`, other factors made it impossible to find a receiver for a
//! recipient UA
CouldNotResolveReceiver,
//! Requested `PrivacyPolicy` doesnt include `AllowRevealedRecipients`, but we are trying to
//! pay a UA where we can only select a transparent receiver
TransparentReceiverNotAllowed,
//! Requested `PrivacyPolicy` doesnt include `AllowRevealedAmounts`, but we are trying to pay a
//! UA where we dont have enough funds in any single pool that it has a receiver for
RevealingReceiverAmountsNotAllowed,
};
class InsufficientFundsError {
public:
CAmount required;
InsufficientFundsError(CAmount required):
required(required) { }
};
class DustThresholdError {
public:
CAmount dustThreshold;
CAmount changeAmount;
DustThresholdError(CAmount dustThreshold, CAmount changeAmount):
dustThreshold(dustThreshold), changeAmount(changeAmount) { }
};
typedef std::variant<
InsufficientFundsError,
DustThresholdError> InvalidFundsReason;
class InvalidFundsError {
public:
CAmount available;
const InvalidFundsReason reason;
InvalidFundsError(CAmount available, const InvalidFundsReason reason):
available(available), reason(reason) { }
};
class ChangeNotAllowedError {
public:
CAmount available;
CAmount required;
ChangeNotAllowedError(CAmount available, CAmount required):
available(available), required(required) { }
};
enum ActionSide {
Input,
Output,
Both,
};
class ExcessOrchardActionsError {
public:
ActionSide side;
uint32_t orchardNotes;
uint32_t maxNotes;
ExcessOrchardActionsError(ActionSide side, uint32_t orchardNotes, uint32_t maxNotes):
side(side), orchardNotes(orchardNotes), maxNotes(maxNotes) { }
};
typedef std::variant<
AddressResolutionError,
InvalidFundsError,
ChangeNotAllowedError,
ExcessOrchardActionsError> InputSelectionError;
class InputSelection {
private:
Payments payments;
int orchardAnchorHeight;
public:
InputSelection(Payments payments, int orchardAnchorHeight):
payments(payments), orchardAnchorHeight(orchardAnchorHeight) {}
Payments GetPayments() const;
};
typedef std::variant<
InputSelectionError,
InputSelection> InputSelectionResult;
typedef std::variant<
InputSelectionError,
TransactionEffects> PrepareTransactionResult;
class WalletTxBuilder {
private:
const CChainParams& params;
const CWallet& wallet;
CFeeRate minRelayFee;
uint32_t maxOrchardActions;
/**
* Compute the default dust threshold
*/
CAmount DefaultDustThreshold() const;
/**
* Select inputs sufficient to fulfill the specified requested payments,
* and choose unified address receivers based upon the available inputs
* and the requested transaction strategy.
*/
InputSelectionResult ResolveInputsAndPayments(
const ZTXOSelector& selector,
SpendableInputs& spendable,
const std::vector<Payment>& payments,
const CChain& chain,
TransactionStrategy strategy,
CAmount fee,
int anchorHeight) const;
/**
* Compute the internal and external OVKs to use in transaction construction, given
* the spendable inputs.
*/
std::pair<uint256, uint256> SelectOVKs(
const ZTXOSelector& selector,
const SpendableInputs& spendable) const;
public:
WalletTxBuilder(const CChainParams& params, const CWallet& wallet, CFeeRate minRelayFee):
params(params), wallet(wallet), minRelayFee(minRelayFee), maxOrchardActions(nOrchardActionLimit) {}
SpendableInputs FindAllSpendableInputs(
const ZTXOSelector& selector,
int32_t minDepth) const;
PrepareTransactionResult PrepareTransaction(
const ZTXOSelector& selector,
SpendableInputs& spendable,
const std::vector<Payment>& payments,
const CChain& chain,
TransactionStrategy strategy,
CAmount fee,
uint32_t anchorConfirmations) const;
};
#endif #endif

View File

@ -270,13 +270,13 @@ typedef std::variant<
SproutSpendingKey, SproutSpendingKey,
SaplingExtendedSpendingKey> SpendingKey; SaplingExtendedSpendingKey> SpendingKey;
class IsShieldedRecipient { class HasShieldedRecipient {
public: public:
bool operator()(const CKeyID& p2pkh) { return false; } bool operator()(const CKeyID& p2pkh) { return false; }
bool operator()(const CScriptID& p2sh) { return false; } bool operator()(const CScriptID& p2sh) { return false; }
bool operator()(const SproutPaymentAddress& addr) { return true; } bool operator()(const SproutPaymentAddress& addr) { return true; }
bool operator()(const SaplingPaymentAddress& addr) { return true; } bool operator()(const SaplingPaymentAddress& addr) { return true; }
bool operator()(const OrchardRawAddress& addr) { return true; } bool operator()(const UnifiedAddress& addr) { return true; }
}; };
class SelectRecipientAddress { class SelectRecipientAddress {