Many z_mergetoaddress updates

- add ZIP 317 support
- address review comments
- restructure `AsyncRPCOperation_mergetoaddress` (removing the just-added
  `prepare`)
This commit is contained in:
Greg Pfeil 2023-04-19 16:25:40 -06:00
parent 94f5bfc595
commit d0522df5c0
No known key found for this signature in database
GPG Key ID: 1193ACD196ED61F2
11 changed files with 192 additions and 323 deletions

View File

@ -21,24 +21,20 @@ 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 - Several `z_sendmany`, `z_shieldcoinbase` and `z_mergetoaddress` failures have
while you should already be checking the async operation status, there are now moved from synchronous to asynchronous, so while you should already be
more cases that may trigger failure at that stage. checking the async operation status, there are now more cases that may trigger
failure at that stage.
- The `AllowRevealedRecipients` privacy policy is now required in order to choose a - The `AllowRevealedRecipients` privacy policy is now required in order to choose a
transparent change address for a transaction. This will only occur when the wallet transparent change address for a transaction. This will only occur when the wallet
is unable to construct the transaction without selecting funds from the transparent is unable to construct the transaction without selecting funds from the transparent
pool, so the impact of this change is that for such transactions, the user must specify pool, so the impact of this change is that for such transactions, the user must specify
`AllowFullyTransparent`. `AllowFullyTransparent`.
- The `z_shieldcoinbase` and `z_mergetoaddress` RPC methods now support an
optional privacy policy.
- The `estimatepriority` RPC call has been removed. - The `estimatepriority` RPC call has been removed.
- The `priority_delta` argument to the `prioritisetransaction` RPC call now has - The `priority_delta` argument to the `prioritisetransaction` RPC call now has
no effect and must be set to a dummy value (0 or null). no effect and must be set to a dummy value (0 or null).
- The `z_shieldcoinbase` and `z_mergetoaddress` RPC methods no longer support
transfers of funds to Sprout recipients. While Sprout change may still be
produced in the process of spending Sprout funds, it is no longer possible
to transfer funds between Sprout addresses. Also, some 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.
Changes to Transaction Fee Selection Changes to Transaction Fee Selection
------------------------------------ ------------------------------------

View File

@ -24,7 +24,7 @@ def assert_mergetoaddress_exception(expected_error_msg, merge_to_address_lambda)
except Exception as e: except Exception as e:
fail("Expected JSONRPCException. Found %s" % repr(e)) fail("Expected JSONRPCException. Found %s" % repr(e))
else: else:
fail("Expected exception: %s" % expected_error_msg) fail("Expected exception: %s”, but didnt fail" % expected_error_msg)
class MergeToAddressHelper: class MergeToAddressHelper:
@ -129,9 +129,9 @@ class MergeToAddressHelper:
"Amount out of range", "Amount out of range",
lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, Decimal('21000000.00000001'))) lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, Decimal('21000000.00000001')))
# Merging will fail because fee is larger than sum of UTXOs # Merging will fail because fee is larger than `-maxtxfee`
assert_mergetoaddress_exception( assert_mergetoaddress_exception(
"Insufficient funds, have 50.00, which is less than miners fee 999.00", "Fee (999.00 ZEC) is greater than the maximum fee allowed by this instance (0.10 ZEC). Run zcashd with `-maxtxfee` to adjust this limit.",
lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, 999)) lambda: test.nodes[0].z_mergetoaddress(self.any_zaddr_or_utxo, myzaddr, 999))
# Merging will fail because transparent limit parameter must be at least 0 # Merging will fail because transparent limit parameter must be at least 0
@ -174,7 +174,7 @@ class MergeToAddressHelper:
# Confirm balances and that do_not_shield_taddr containing funds of 10 was left alone # Confirm balances and that do_not_shield_taddr containing funds of 10 was left alone
assert_equal(test.nodes[0].getbalance(), Decimal('10')) assert_equal(test.nodes[0].getbalance(), Decimal('10'))
assert_equal(test.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('10')) assert_equal(test.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('10'))
assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('40') - DEFAULT_FEE) assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('40') - conventional_fee(4))
assert_equal(test.nodes[1].getbalance(), Decimal('40')) assert_equal(test.nodes[1].getbalance(), Decimal('40'))
assert_equal(test.nodes[2].getbalance(), Decimal('30')) assert_equal(test.nodes[2].getbalance(), Decimal('30'))
@ -191,7 +191,7 @@ class MergeToAddressHelper:
test.sync_all() test.sync_all()
assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('0')) assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('0'))
assert_equal(test.nodes[0].z_getbalance(myzaddr2), Decimal('40') - DEFAULT_FEE) assert_equal(test.nodes[0].z_getbalance(myzaddr2), Decimal('40') - conventional_fee(4))
# Shield coinbase UTXOs from any node 2 taddr, and set fee to 0 # Shield coinbase UTXOs from any node 2 taddr, and set fee to 0
result = test.nodes[2].z_shieldcoinbase("*", myzaddr, 0) result = test.nodes[2].z_shieldcoinbase("*", myzaddr, 0)
@ -202,7 +202,7 @@ class MergeToAddressHelper:
assert_equal(test.nodes[0].getbalance(), Decimal('10')) assert_equal(test.nodes[0].getbalance(), Decimal('10'))
assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('30')) assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('30'))
assert_equal(test.nodes[0].z_getbalance(myzaddr2), Decimal('40') - DEFAULT_FEE) assert_equal(test.nodes[0].z_getbalance(myzaddr2), Decimal('40') - conventional_fee(4))
assert_equal(test.nodes[1].getbalance(), Decimal('60')) assert_equal(test.nodes[1].getbalance(), Decimal('60'))
assert_equal(test.nodes[2].getbalance(), Decimal('0')) assert_equal(test.nodes[2].getbalance(), Decimal('0'))
@ -213,9 +213,9 @@ class MergeToAddressHelper:
generate_and_check(test.nodes[1], 2) generate_and_check(test.nodes[1], 2)
test.sync_all() test.sync_all()
assert_equal(test.nodes[0].getbalance(), Decimal('80') - DEFAULT_FEE) assert_equal(test.nodes[0].getbalance(), Decimal('80') - conventional_fee(4))
assert_equal(test.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('10')) assert_equal(test.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('10'))
assert_equal(test.nodes[0].z_getbalance(mytaddr), Decimal('70') - DEFAULT_FEE) assert_equal(test.nodes[0].z_getbalance(mytaddr), Decimal('70') - conventional_fee(4))
assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('0')) assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('0'))
assert_equal(test.nodes[0].z_getbalance(myzaddr2), Decimal('0')) assert_equal(test.nodes[0].z_getbalance(myzaddr2), Decimal('0'))
assert_equal(test.nodes[1].getbalance(), Decimal('70')) assert_equal(test.nodes[1].getbalance(), Decimal('70'))
@ -235,8 +235,8 @@ class MergeToAddressHelper:
assert_equal(test.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('0')) assert_equal(test.nodes[0].z_getbalance(do_not_shield_taddr), Decimal('0'))
assert_equal(test.nodes[0].z_getbalance(mytaddr), Decimal('0')) assert_equal(test.nodes[0].z_getbalance(mytaddr), Decimal('0'))
assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('0')) assert_equal(test.nodes[0].z_getbalance(myzaddr), Decimal('0'))
assert_equal(test.nodes[1].getbalance(), Decimal('160') - DEFAULT_FEE) assert_equal(test.nodes[1].getbalance(), Decimal('160') - conventional_fee(4))
assert_equal(test.nodes[1].z_getbalance(n1taddr), Decimal('80') - DEFAULT_FEE) assert_equal(test.nodes[1].z_getbalance(n1taddr), Decimal('80') - conventional_fee(4))
assert_equal(test.nodes[2].getbalance(), Decimal('0')) assert_equal(test.nodes[2].getbalance(), Decimal('0'))
# Generate 5 regular UTXOs on node 0, and 20 regular UTXOs on node 2. # Generate 5 regular UTXOs on node 0, and 20 regular UTXOs on node 2.

View File

@ -317,18 +317,18 @@ TEST(TransactionBuilder, FailsWithNegativeChange)
// 0.00005 z-ZEC out, default fee // 0.00005 z-ZEC out, default fee
auto builder = TransactionBuilder(consensusParams, 1, std::nullopt); auto builder = TransactionBuilder(consensusParams, 1, std::nullopt);
builder.AddSaplingOutput(fvk.ovk, pa, 5000, {}); builder.AddSaplingOutput(fvk.ovk, pa, 5000, {});
EXPECT_EQ("Change cannot be negative", builder.Build().GetError()); EXPECT_EQ("Change cannot be negative: -0.00006 ZEC", builder.Build().GetError());
// Fail if there is only a transparent output // Fail if there is only a transparent output
// 0.00005 t-ZEC out, default fee // 0.00005 t-ZEC out, default fee
builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore); builder = TransactionBuilder(consensusParams, 1, std::nullopt, &keystore);
builder.AddTransparentOutput(taddr, 5000); builder.AddTransparentOutput(taddr, 5000);
EXPECT_EQ("Change cannot be negative", builder.Build().GetError()); EXPECT_EQ("Change cannot be negative: -0.00006 ZEC", builder.Build().GetError());
// Fails if there is insufficient input // Fails if there is insufficient input
// 0.00005 t-ZEC out, default fee, 0.00005999 z-ZEC in // 0.00005 t-ZEC out, default fee, 0.00005999 z-ZEC in
builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness()); builder.AddSaplingSpend(expsk, testNote.note, testNote.tree.root(), testNote.tree.witness());
EXPECT_EQ("Change cannot be negative", builder.Build().GetError()); EXPECT_EQ("Change cannot be negative: -0.00000001 ZEC", builder.Build().GetError());
// Succeeds if there is sufficient input // Succeeds if there is sufficient input
builder.AddTransparentInput(COutPoint(), scriptPubKey, 1); builder.AddTransparentInput(COutPoint(), scriptPubKey, 1);

View File

@ -501,7 +501,8 @@ TransactionBuilderResult TransactionBuilder::Build()
change -= tOut.nValue; change -= tOut.nValue;
} }
if (change < 0) { if (change < 0) {
return TransactionBuilderResult("Change cannot be negative"); return TransactionBuilderResult(
strprintf("Change cannot be negative: %s", DisplayMoney(change)));
} }
// //

View File

@ -27,7 +27,6 @@
#include "util/time.h" #include "util/time.h"
#include "wallet.h" #include "wallet.h"
#include "walletdb.h" #include "walletdb.h"
#include "wallet/paymentdisclosuredb.h"
#include "zcash/IncrementalMerkleTree.hpp" #include "zcash/IncrementalMerkleTree.hpp"
#include <chrono> #include <chrono>
@ -40,53 +39,16 @@
using namespace libzcash; using namespace libzcash;
int mta_find_output(UniValue obj, int n)
{
UniValue outputMapValue = find_value(obj, "outputmap");
if (!outputMapValue.isArray()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Missing outputmap for JoinSplit operation");
}
UniValue outputMap = outputMapValue.get_array();
assert(outputMap.size() == ZC_NUM_JS_OUTPUTS);
for (size_t i = 0; i < outputMap.size(); i++) {
if (outputMap[i].get_int() == n) {
return i;
}
}
throw std::logic_error("n is not present in outputmap");
}
AsyncRPCOperation_mergetoaddress::AsyncRPCOperation_mergetoaddress( AsyncRPCOperation_mergetoaddress::AsyncRPCOperation_mergetoaddress(
WalletTxBuilder builder, CWallet& wallet,
ZTXOSelector ztxoSelector, TransactionStrategy strategy,
SpendableInputs allInputs, TransactionEffects effects,
MergeToAddressRecipient recipient, UniValue contextInfo)
TransactionStrategy strategy, : strategy_(strategy), effects_(effects), contextinfo_(contextInfo)
CAmount fee,
UniValue contextInfo) :
builder_(builder), ztxoSelector_(ztxoSelector), allInputs_(allInputs), toPaymentAddress_(recipient.first), memo_(recipient.second), fee_(fee), strategy_(strategy), contextinfo_(contextInfo)
{ {
if (!MoneyRange(fee)) { effects.LockSpendable(*pwalletMain);
throw JSONRPCError(RPC_INVALID_PARAMETER, "Fee is out of range");
}
KeyIO keyIO(Params()); KeyIO keyIO(Params());
isToTaddr_ = false;
examine(toPaymentAddress_, match {
[&](const CKeyID&) { isToTaddr_ = true; },
[&](const CScriptID&) { isToTaddr_ = true; },
[](const SproutPaymentAddress&) { },
[](const libzcash::SaplingPaymentAddress&) { },
[](const libzcash::UnifiedAddress&) {
// TODO: This should be removable with WalletTxBuilder, but need to test it.
throw JSONRPCError(
RPC_INVALID_ADDRESS_OR_KEY,
"z_mergetoaddress does not yet support sending to unified addresses");
},
});
// Log the context info i.e. the call parameters to z_mergetoaddress // Log the context info i.e. the call parameters to z_mergetoaddress
if (LogAcceptCategory("zrpcunsafe")) { if (LogAcceptCategory("zrpcunsafe")) {
@ -94,15 +56,21 @@ AsyncRPCOperation_mergetoaddress::AsyncRPCOperation_mergetoaddress(
} else { } else {
LogPrint("zrpc", "%s: z_mergetoaddress initialized\n", getId()); LogPrint("zrpc", "%s: z_mergetoaddress initialized\n", getId());
} }
// Enable payment disclosure if requested
paymentDisclosureMode = fExperimentalPaymentDisclosure;
} }
AsyncRPCOperation_mergetoaddress::~AsyncRPCOperation_mergetoaddress() AsyncRPCOperation_mergetoaddress::~AsyncRPCOperation_mergetoaddress()
{ {
} }
std::pair<uint256, UniValue>
main_impl(
const CChainParams& chainparams,
CWallet& wallet,
const TransactionStrategy& strategy,
const TransactionEffects& effects,
const std::string& id,
bool testmode);
void AsyncRPCOperation_mergetoaddress::main() void AsyncRPCOperation_mergetoaddress::main()
{ {
set_state(OperationStatus::EXECUTING); set_state(OperationStatus::EXECUTING);
@ -116,7 +84,10 @@ void AsyncRPCOperation_mergetoaddress::main()
std::optional<uint256> txid; std::optional<uint256> txid;
try { try {
txid = main_impl(*pwalletMain, Params()); UniValue sendResult;
std::tie(txid, sendResult) =
main_impl(Params(), *pwalletMain, strategy_, effects_, getId(), testmode);
set_result(sendResult);
} catch (const UniValue& objError) { } catch (const UniValue& objError) {
int code = find_value(objError, "code").get_int(); int code = find_value(objError, "code").get_int();
std::string message = find_value(objError, "message").get_str(); std::string message = find_value(objError, "message").get_str();
@ -157,111 +128,68 @@ void AsyncRPCOperation_mergetoaddress::main()
LogPrintf("%s", s); LogPrintf("%s", s);
} }
tl::expected<void, InputSelectionError> AsyncRPCOperation_mergetoaddress::prepare(const CChainParams& chainparams, CWallet& wallet) { std::pair<uint256, UniValue>
CAmount minersFee = fee_; main_impl(
const CChainParams& chainparams,
size_t numInputs = allInputs_.utxos.size(); CWallet& wallet,
const TransactionStrategy& strategy,
CAmount t_inputs_total = 0; const TransactionEffects& effects,
for (COutput& t : allInputs_.utxos) { const std::string& id,
t_inputs_total += t.Value(); bool testmode)
}
CAmount z_inputs_total = 0;
for (const SproutNoteEntry& t : allInputs_.sproutNoteEntries) {
z_inputs_total += t.note.value();
}
for (const SaplingNoteEntry& t : allInputs_.saplingNoteEntries) {
z_inputs_total += t.note.value();
}
for (const OrchardNoteMetadata& t : allInputs_.orchardNoteMetadata) {
z_inputs_total += t.GetNoteValue();
}
CAmount targetAmount = z_inputs_total + t_inputs_total;
CAmount sendAmount = targetAmount - minersFee;
if (sendAmount <= 0) {
return tl::make_unexpected(InvalidFundsError(sendAmount, InsufficientFundsError(minersFee)));
}
// Grab the current consensus branch ID
{
LOCK(cs_main);
consensusBranchId_ = CurrentEpochBranchId(chainActive.Height() + 1, chainparams.GetConsensus());
}
auto payment = Payment(toPaymentAddress_, sendAmount, memo_);
return builder_.PrepareTransaction(
wallet,
ztxoSelector_,
allInputs_,
{payment},
chainActive,
strategy_,
minersFee,
nAnchorConfirmations)
.map([&](const TransactionEffects& effects) -> void {
effects.LockSpendable(wallet);
effects_ = effects;
});
}
// Notes:
// 1. #1359 Currently there is no limit set on the number of joinsplits, so size of tx could be invalid.
// 2. #1277 Spendable notes are not locked, so an operation running in parallel could also try to use them.
uint256 AsyncRPCOperation_mergetoaddress::main_impl(CWallet& wallet, const CChainParams& chainparams)
{ {
// The caller must call `prepare` before `main_impl` is invoked.
const auto effects = effects_.value();
try { try {
const auto& spendable = effects.GetSpendable(); const auto& spendable = effects.GetSpendable();
const auto& payments = effects.GetPayments(); const auto& payments = effects.GetPayments();
spendable.LogInputs(getId()); spendable.LogInputs(id);
if (allInputs_.sproutNoteEntries.empty() bool sendsToShielded = false;
&& allInputs_.saplingNoteEntries.empty() for (const auto& resolvedPayment : payments.GetResolvedPayments()) {
&& allInputs_.orchardNoteMetadata.empty() sendsToShielded = sendsToShielded || examine(resolvedPayment.address, match {
&& isToTaddr_) { [](const CKeyID&) { return true; },
[](const CScriptID&) { return true; },
[](const libzcash::SaplingPaymentAddress&) { return false; },
[](const libzcash::OrchardRawAddress&) { return false; },
});
}
if (spendable.sproutNoteEntries.empty()
&& spendable.saplingNoteEntries.empty()
&& spendable.orchardNoteMetadata.empty()
&& !sendsToShielded) {
LogPrint("zrpc", "%s: spending %s to send %s with fee %s\n", LogPrint("zrpc", "%s: spending %s to send %s with fee %s\n",
getId(), id,
FormatMoney(payments.Total()), FormatMoney(payments.Total()),
FormatMoney(spendable.Total()), FormatMoney(spendable.Total()),
FormatMoney(effects.GetFee())); FormatMoney(effects.GetFee()));
} else { } else {
LogPrint("zrpcunsafe", "%s: spending %s to send %s with fee %s\n", LogPrint("zrpcunsafe", "%s: spending %s to send %s with fee %s\n",
getId(), id,
FormatMoney(payments.Total()), FormatMoney(payments.Total()),
FormatMoney(spendable.Total()), FormatMoney(spendable.Total()),
FormatMoney(effects.GetFee())); FormatMoney(effects.GetFee()));
} }
LogPrint("zrpc", "%s: total transparent input: %s\n", getId(), LogPrint("zrpc", "%s: total transparent input: %s\n", id,
FormatMoney(spendable.GetTransparentTotal())); FormatMoney(spendable.GetTransparentTotal()));
LogPrint("zrpcunsafe", "%s: total shielded input: %s\n", getId(), LogPrint("zrpcunsafe", "%s: total shielded input: %s\n", id,
FormatMoney(spendable.GetSaplingTotal() + spendable.GetOrchardTotal())); FormatMoney(spendable.GetSaplingTotal() + spendable.GetOrchardTotal()));
LogPrint("zrpc", "%s: total transparent output: %s\n", getId(), LogPrint("zrpc", "%s: total transparent output: %s\n", id,
FormatMoney(payments.GetTransparentTotal())); FormatMoney(payments.GetTransparentTotal()));
LogPrint("zrpcunsafe", "%s: total shielded Sapling output: %s\n", getId(), LogPrint("zrpcunsafe", "%s: total shielded Sapling output: %s\n", id,
FormatMoney(payments.GetSaplingTotal())); FormatMoney(payments.GetSaplingTotal()));
LogPrint("zrpcunsafe", "%s: total shielded Orchard output: %s\n", getId(), LogPrint("zrpcunsafe", "%s: total shielded Orchard output: %s\n", id,
FormatMoney(payments.GetOrchardTotal())); FormatMoney(payments.GetOrchardTotal()));
LogPrint("zrpc", "%s: fee: %s\n", getId(), FormatMoney(effects.GetFee())); LogPrint("zrpc", "%s: fee: %s\n", id, FormatMoney(effects.GetFee()));
auto buildResult = effects.ApproveAndBuild( auto buildResult = effects.ApproveAndBuild(
chainparams.GetConsensus(), chainparams.GetConsensus(),
wallet, wallet,
chainActive, chainActive,
strategy_); strategy);
auto tx = buildResult.GetTxOrThrow(); auto tx = buildResult.GetTxOrThrow();
UniValue sendResult = SendTransaction(tx, payments.GetResolvedPayments(), std::nullopt, testmode); UniValue sendResult = SendTransaction(tx, payments.GetResolvedPayments(), std::nullopt, testmode);
set_result(sendResult);
effects.UnlockSpendable(wallet); effects.UnlockSpendable(wallet);
return tx.GetHash(); return {tx.GetHash(), sendResult};
} catch (...) { } catch (...) {
effects.UnlockSpendable(wallet); effects.UnlockSpendable(wallet);
throw; throw;

View File

@ -27,9 +27,6 @@
using namespace libzcash; using namespace libzcash;
// A recipient is a tuple of address, memo (optional if zaddr)
typedef std::pair<libzcash::PaymentAddress, std::optional<Memo>> MergeToAddressRecipient;
// Package of info which is passed to perform_joinsplit methods. // Package of info which is passed to perform_joinsplit methods.
struct MergeToAddressJSInfo { struct MergeToAddressJSInfo {
std::vector<JSInput> vjsin; std::vector<JSInput> vjsin;
@ -50,13 +47,10 @@ class AsyncRPCOperation_mergetoaddress : public AsyncRPCOperation
{ {
public: public:
AsyncRPCOperation_mergetoaddress( AsyncRPCOperation_mergetoaddress(
WalletTxBuilder builder, CWallet& wallet,
ZTXOSelector ztxoSelector, TransactionStrategy strategy,
SpendableInputs allInputs, TransactionEffects effects,
MergeToAddressRecipient recipient, UniValue contextInfo = NullUniValue);
TransactionStrategy strategy,
CAmount fee = DEFAULT_FEE,
UniValue contextInfo = NullUniValue);
virtual ~AsyncRPCOperation_mergetoaddress(); virtual ~AsyncRPCOperation_mergetoaddress();
// We don't want to be copied or moved around // We don't want to be copied or moved around
@ -65,69 +59,20 @@ public:
AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress const&) = delete; // Copy assign AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress const&) = delete; // Copy assign
AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress&&) = delete; // Move assign AsyncRPCOperation_mergetoaddress& operator=(AsyncRPCOperation_mergetoaddress&&) = delete; // Move assign
tl::expected<void, InputSelectionError> prepare(const CChainParams& chainparams, CWallet& wallet);
virtual void main(); virtual void main();
virtual UniValue getStatus() const; virtual UniValue getStatus() const;
bool testmode = false; // Set to true to disable sending txs and generating proofs /// Set to true to disable sending txs and generating proofs
bool testmode = false;
bool paymentDisclosureMode = false; // Set to true to save esk for encrypted notes in payment disclosure database.
private: private:
friend class TEST_FRIEND_AsyncRPCOperation_mergetoaddress; // class for unit testing const TransactionStrategy strategy_;
UniValue contextinfo_; // optional data to include in return value from getStatus() const TransactionEffects effects_;
uint32_t consensusBranchId_; /// optional data to include in return value from getStatus()
CAmount fee_; UniValue contextinfo_;
int mindepth_;
bool isToTaddr_;
bool isToZaddr_;
PaymentAddress toPaymentAddress_;
std::optional<Memo> memo_;
ed25519::VerificationKey joinSplitPubKey_;
ed25519::SigningKey joinSplitPrivKey_;
// The key is the result string from calling JSOutPoint::ToString()
std::unordered_map<std::string, MergeToAddressWitnessAnchorData> jsopWitnessAnchorMap;
WalletTxBuilder builder_;
ZTXOSelector ztxoSelector_;
SpendableInputs allInputs_;
TransactionStrategy strategy_;
std::optional<TransactionEffects> effects_;
uint256 main_impl(CWallet& wallet, const CChainParams& chainparams);
// payment disclosure!
std::vector<PaymentDisclosureKeyInfo> paymentDisclosureData_;
}; };
// To test private methods, a friend class can act as a proxy
class TEST_FRIEND_AsyncRPCOperation_mergetoaddress
{
public:
std::shared_ptr<AsyncRPCOperation_mergetoaddress> delegate;
TEST_FRIEND_AsyncRPCOperation_mergetoaddress(std::shared_ptr<AsyncRPCOperation_mergetoaddress> ptr) : delegate(ptr) {}
// Delegated methods
uint256 main_impl(CWallet& wallet, const CChainParams& chainparams)
{
return delegate->main_impl(wallet, chainparams);
}
void set_state(OperationStatus state)
{
delegate->state_.store(state);
}
};
#endif // ZCASH_WALLET_ASYNCRPCOPERATION_MERGETOADDRESS_H #endif // ZCASH_WALLET_ASYNCRPCOPERATION_MERGETOADDRESS_H

View File

@ -8,7 +8,6 @@
#include "transaction_builder.h" #include "transaction_builder.h"
#include "util/test.h" #include "util/test.h"
#include "gtest/utils.h" #include "gtest/utils.h"
#include "wallet/asyncrpcoperation_mergetoaddress.h"
#include "wallet/asyncrpcoperation_shieldcoinbase.h" #include "wallet/asyncrpcoperation_shieldcoinbase.h"
#include "wallet/asyncrpcoperation_sendmany.h" #include "wallet/asyncrpcoperation_sendmany.h"
#include "wallet/memo.h" #include "wallet/memo.h"
@ -57,9 +56,9 @@ TEST(WalletRPCTests, RPCZMergeToAddressInternals)
auto taddr = pwalletMain->GenerateNewKey(true).GetID(); auto taddr = pwalletMain->GenerateNewKey(true).GetID();
std::string taddr_string = keyIO.EncodeDestination(taddr); std::string taddr_string = keyIO.EncodeDestination(taddr);
MergeToAddressRecipient taddr1(keyIO.DecodePaymentAddress(taddr_string).value(), Memo()); NetAmountRecipient taddr1(keyIO.DecodePaymentAddress(taddr_string).value(), Memo());
auto pa = pwalletMain->GenerateNewSproutZKey(); auto pa = pwalletMain->GenerateNewSproutZKey();
MergeToAddressRecipient zaddr1(pa, Memo()); NetAmountRecipient zaddr1(pa, Memo());
WalletTxBuilder builder(Params(), minRelayTxFee); WalletTxBuilder builder(Params(), minRelayTxFee);
auto selector = CWallet::LegacyTransparentZTXOSelector( auto selector = CWallet::LegacyTransparentZTXOSelector(
@ -72,11 +71,24 @@ TEST(WalletRPCTests, RPCZMergeToAddressInternals)
SpendableInputs inputs; SpendableInputs inputs;
auto wtx = FakeWalletTx(); auto wtx = FakeWalletTx();
inputs.utxos.emplace_back(COutput(&wtx, 0, 100, true)); inputs.utxos.emplace_back(COutput(&wtx, 0, 100, true));
auto operation = AsyncRPCOperation_mergetoaddress(std::move(builder), selector, inputs, zaddr1, strategy, 0); builder.PrepareTransaction(
operation.main(); *pwalletMain,
EXPECT_TRUE(operation.isFailed()); selector,
std::string msg = operation.getErrorMessage(); inputs,
EXPECT_EQ(msg, "Sending funds into the Sprout pool is no longer supported."); zaddr1,
chainActive,
strategy,
0,
1)
.map_error([](const auto& err) {
EXPECT_TRUE(examine(err, match {
[](const AddressResolutionError& are) {
return are == AddressResolutionError::SproutRecipientsNotSupported;
},
[](const auto&) { return false; },
}));
})
.map([](const auto&) { EXPECT_TRUE(false); });
} }
} }
UnloadGlobalWallet(); UnloadGlobalWallet();

View File

@ -5370,8 +5370,8 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
" ,...\n" " ,...\n"
" ]\n" " ]\n"
"2. \"toaddress\" (string, required) The taddr or zaddr to send the funds to.\n" "2. \"toaddress\" (string, required) The taddr or zaddr to send the funds to.\n"
"3. fee (numeric, optional, default=" "3. fee (numeric, optional, default=null) The fee amount in " + CURRENCY_UNIT + " to attach to this transaction. The default behavior\n"
+ strprintf("%s", FormatMoney(DEFAULT_FEE)) + ") The fee amount to attach to this transaction.\n" " is to use a fee calculated according to ZIP 317.\n"
"4. transparent_limit (numeric, optional, default=" "4. transparent_limit (numeric, optional, default="
+ strprintf("%d", MERGE_TO_ADDRESS_DEFAULT_TRANSPARENT_LIMIT) + ") Limit on the maximum number of UTXOs to merge. Set to 0 to use as many as will fit in the transaction.\n" + strprintf("%d", MERGE_TO_ADDRESS_DEFAULT_TRANSPARENT_LIMIT) + ") Limit on the maximum number of UTXOs to merge. Set to 0 to use as many as will fit in the transaction.\n"
"5. shielded_limit (numeric, optional, default=" "5. shielded_limit (numeric, optional, default="
@ -5529,14 +5529,9 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
string("Invalid parameter, unknown address format: ") + destStr); string("Invalid parameter, unknown address format: ") + destStr);
} }
// Convert fee from currency format to zatoshis std::optional<CAmount> nFee;
CAmount nFee = DEFAULT_FEE; if (params.size() > 2 && !params[2].isNull()) {
if (params.size() > 2) { nFee = AmountFromValue( params[2] );
if (params[2].get_real() == 0.0) {
nFee = 0;
} else {
nFee = AmountFromValue( params[2] );
}
} }
int nUTXOLimit = MERGE_TO_ADDRESS_DEFAULT_TRANSPARENT_LIMIT; int nUTXOLimit = MERGE_TO_ADDRESS_DEFAULT_TRANSPARENT_LIMIT;
@ -5563,7 +5558,7 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
memo = Memo::FromHexOrThrow(params[5].get_str()); memo = Memo::FromHexOrThrow(params[5].get_str());
} }
MergeToAddressRecipient recipient(destaddress.value(), memo); NetAmountRecipient recipient(destaddress.value(), memo);
// Prepare to get UTXOs and notes // Prepare to get UTXOs and notes
SpendableInputs allInputs; SpendableInputs allInputs;
@ -5737,28 +5732,18 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
// - We only have one from address // - We only have one from address
// - It's equal to toaddress // - It's equal to toaddress
// - The address only contains a single UTXO or note // - The address only contains a single UTXO or note
// TODO: Move this to WalletTxBuilder
if (setAddress.size() == 1 && setAddress.count(destStr) && (numUtxos + numNotes) == 1) { if (setAddress.size() == 1 && setAddress.count(destStr) && (numUtxos + numNotes) == 1) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Destination address is also the only source address, and all its funds are already merged."); throw JSONRPCError(RPC_INVALID_PARAMETER, "Destination address is also the only source address, and all its funds are already merged.");
} }
CAmount mergedValue = mergedUTXOValue + mergedNoteValue;
if (mergedValue < nFee) {
throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS,
strprintf("Insufficient funds, have %s, which is less than miners fee %s",
FormatMoney(mergedValue), FormatMoney(nFee)));
}
// Check that the user specified fee is sane (if too high, it can result in error -25 absurd fee)
CAmount netAmount = mergedValue - nFee;
if (nFee > netAmount) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Fee %s is greater than the net amount to be shielded %s", FormatMoney(nFee), FormatMoney(netAmount)));
}
// Keep record of parameters in context object // Keep record of parameters in context object
UniValue contextInfo(UniValue::VOBJ); UniValue contextInfo(UniValue::VOBJ);
contextInfo.pushKV("fromaddresses", params[0]); contextInfo.pushKV("fromaddresses", params[0]);
contextInfo.pushKV("toaddress", params[1]); contextInfo.pushKV("toaddress", params[1]);
contextInfo.pushKV("fee", ValueFromAmount(nFee)); if (nFee.has_value()) {
contextInfo.pushKV("fee", ValueFromAmount(nFee.value()));
}
// The privacy policy is determined early so as to be able to use it // The privacy policy is determined early so as to be able to use it
// for selector construction. // for selector construction.
@ -5769,37 +5754,14 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
params.size() > 6 ? std::optional(params[6].get_str()) : std::nullopt), params.size() > 6 ? std::optional(params[6].get_str()) : std::nullopt),
InterpretLegacyCompat(std::nullopt, {recipient.first})); InterpretLegacyCompat(std::nullopt, {recipient.first}));
bool isSproutShielded = allInputs.sproutNoteEntries.size() > 0;
// Contextual transaction we will build on
CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction(
chainparams.GetConsensus(),
nextBlockHeight,
isSproutShielded || nPreferredTxVersion < ZIP225_MIN_TX_VERSION);
if (contextualTx.nVersion == 1 && isSproutShielded) {
contextualTx.nVersion = 2; // Tx format should support vJoinSplit
}
if (isToSaplingZaddr || allInputs.saplingNoteEntries.size() > 0) {
std::optional<uint256> orchardAnchor;
if (!isSproutShielded && nPreferredTxVersion >= ZIP225_MIN_TX_VERSION && nAnchorConfirmations > 0) {
// Allow Orchard recipients by setting an Orchard anchor.
auto orchardAnchorHeight = nextBlockHeight - nAnchorConfirmations;
if (chainparams.GetConsensus().NetworkUpgradeActive(orchardAnchorHeight, Consensus::UPGRADE_NU5)) {
auto anchorBlockIndex = chainActive[orchardAnchorHeight];
assert(anchorBlockIndex != nullptr);
orchardAnchor = anchorBlockIndex->hashFinalOrchardRoot;
}
}
}
WalletTxBuilder builder(Params(), minRelayTxFee); WalletTxBuilder builder(Params(), minRelayTxFee);
// This currently isnt too critical since we dont yet use it for note selection and we never // The ZTXOSelector here is only used to determine whether or not Sprout can be selected. It
// need a change address. The one thing this does is indicate whether or not Sprout can be // should not be used for other purposes (e.g., note selection or finding a change address).
// selected. However, we eventually want a `ZTXOSelector` that can support this operation fully. // TODO: Add a ZTXOSelector that can support the full range of `z_mergetoaddress` behavior and
// I.e., be able to select from multiple pools in the legacy account. // use it instead of `GetFilteredNotes`.
std::optional<ZTXOSelector> ztxoSelector; std::optional<ZTXOSelector> ztxoSelector;
if (isSproutShielded) { if (allInputs.sproutNoteEntries.size() > 0) {
ztxoSelector = pwalletMain->ZTXOSelectorForAddress( ztxoSelector = pwalletMain->ZTXOSelectorForAddress(
allInputs.sproutNoteEntries[0].address, allInputs.sproutNoteEntries[0].address,
true, true,
@ -5820,20 +5782,23 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp)
} }
// Create operation and add to global queue // Create operation and add to global queue
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue(); auto effects = builder.PrepareTransaction(
auto mergeOp = new AsyncRPCOperation_mergetoaddress( *pwalletMain,
std::move(builder), ztxoSelector.value(),
ztxoSelector.value(), allInputs,
allInputs, recipient,
recipient, chainActive,
strategy, strategy,
nFee, nFee,
contextInfo); nAnchorConfirmations)
mergeOp->prepare(chainparams, *pwalletMain).map_error([&](const InputSelectionError& err) { .map_error([&](const auto& err) {
ThrowInputSelectionError(err, ztxoSelector.value(), strategy); ThrowInputSelectionError(err, ztxoSelector.value(), strategy);
}); })
.value();
std::shared_ptr<AsyncRPCOperation> operation(mergeOp); std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation(
new AsyncRPCOperation_mergetoaddress(*pwalletMain, strategy, effects, contextInfo));
q->addOperation(operation); q->addOperation(operation);
AsyncRPCOperationId operationId = operation->getId(); AsyncRPCOperationId operationId = operation->getId();

View File

@ -17,7 +17,6 @@
#include "asyncrpcqueue.h" #include "asyncrpcqueue.h"
#include "asyncrpcoperation.h" #include "asyncrpcoperation.h"
#include "wallet/asyncrpcoperation_common.h" #include "wallet/asyncrpcoperation_common.h"
#include "wallet/asyncrpcoperation_mergetoaddress.h"
#include "wallet/asyncrpcoperation_sendmany.h" #include "wallet/asyncrpcoperation_sendmany.h"
#include "wallet/asyncrpcoperation_shieldcoinbase.h" #include "wallet/asyncrpcoperation_shieldcoinbase.h"
#include "wallet/memo.h" #include "wallet/memo.h"
@ -1226,6 +1225,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
SelectParams(CBaseChainParams::TESTNET); SelectParams(CBaseChainParams::TESTNET);
const Consensus::Params& consensusParams = Params().GetConsensus(); const Consensus::Params& consensusParams = Params().GetConsensus();
KeyIO keyIO(Params()); KeyIO keyIO(Params());
WalletTxBuilder builder(Params(), minRelayTxFee);
LOCK2(cs_main, pwalletMain->cs_wallet); LOCK2(cs_main, pwalletMain->cs_wallet);
@ -1255,7 +1255,6 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
// confirm this. // confirm this.
TransparentCoinbasePolicy::Allow, TransparentCoinbasePolicy::Allow,
false).value(); false).value();
WalletTxBuilder builder(Params(), minRelayTxFee);
std::vector<Payment> recipients = { Payment(zaddr1, 100*COIN, Memo::FromHexOrThrow("DEADBEEF")) }; std::vector<Payment> recipients = { Payment(zaddr1, 100*COIN, Memo::FromHexOrThrow("DEADBEEF")) };
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedSenders); TransactionStrategy strategy(PrivacyPolicy::AllowRevealedSenders);
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy, std::nullopt)); std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy, std::nullopt));
@ -1272,7 +1271,6 @@ BOOST_AUTO_TEST_CASE(rpc_z_sendmany_internals)
true, true,
TransparentCoinbasePolicy::Disallow, TransparentCoinbasePolicy::Disallow,
false).value(); false).value();
WalletTxBuilder builder(Params(), minRelayTxFee);
std::vector<Payment> recipients = { Payment(taddr1, 100*COIN, Memo::FromHexOrThrow("DEADBEEF")) }; 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::nullopt)); std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sendmany(std::move(builder), selector, recipients, 1, 1, strategy, std::nullopt));
@ -1530,6 +1528,7 @@ BOOST_AUTO_TEST_CASE(rpc_z_shieldcoinbase_parameters)
// Test constructor of AsyncRPCOperation_shieldcoinbase // Test constructor of AsyncRPCOperation_shieldcoinbase
KeyIO keyIO(Params()); KeyIO keyIO(Params());
WalletTxBuilder builder(Params(), minRelayTxFee);
UniValue retValue; UniValue retValue;
BOOST_CHECK_NO_THROW(retValue = CallRPC("getnewaddress")); BOOST_CHECK_NO_THROW(retValue = CallRPC("getnewaddress"));
auto taddr2 = std::get<CKeyID>(keyIO.DecodePaymentAddress(retValue.get_str()).value()); auto taddr2 = std::get<CKeyID>(keyIO.DecodePaymentAddress(retValue.get_str()).value());
@ -1542,7 +1541,6 @@ BOOST_AUTO_TEST_CASE(rpc_z_shieldcoinbase_parameters)
// confirm this. // confirm this.
TransparentCoinbasePolicy::Allow, TransparentCoinbasePolicy::Allow,
false).value(); false).value();
WalletTxBuilder builder(Params(), minRelayTxFee);
try { try {
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_shieldcoinbase(std::move(builder), selector, testnetzaddr, PrivacyPolicy::AllowRevealedSenders, SHIELD_COINBASE_DEFAULT_LIMIT, 1)); std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_shieldcoinbase(std::move(builder), selector, testnetzaddr, PrivacyPolicy::AllowRevealedSenders, SHIELD_COINBASE_DEFAULT_LIMIT, 1));
@ -1634,29 +1632,44 @@ BOOST_AUTO_TEST_CASE(rpc_z_mergetoaddress_parameters)
int nHeight = retValue.get_int(); int nHeight = retValue.get_int();
CMutableTransaction mtx = CreateNewContextualCMutableTransaction(Params().GetConsensus(), nHeight + 1, false); CMutableTransaction mtx = CreateNewContextualCMutableTransaction(Params().GetConsensus(), nHeight + 1, false);
// Test constructor of AsyncRPCOperation_mergetoaddress
KeyIO keyIO(Params()); KeyIO keyIO(Params());
MergeToAddressRecipient testnetzaddr( WalletTxBuilder builder(Params(), minRelayTxFee);
NetAmountRecipient testnetzaddr(
keyIO.DecodePaymentAddress("ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP").value(), keyIO.DecodePaymentAddress("ztjiDe569DPNbyTE6TSdJTaSDhoXEHLGvYoUnBU1wfVNU52TEyT6berYtySkd21njAeEoh8fFJUT42kua9r8EnhBaEKqCpP").value(),
Memo()); Memo());
WalletTxBuilder builder(Params(), minRelayTxFee);
auto selector = CWallet::LegacyTransparentZTXOSelector( auto selector = CWallet::LegacyTransparentZTXOSelector(
true, true,
TransparentCoinbasePolicy::Disallow); TransparentCoinbasePolicy::Disallow);
TransactionStrategy strategy(PrivacyPolicy::AllowRevealedRecipients); TransactionStrategy strategy(PrivacyPolicy::AllowRevealedRecipients);
try { builder.PrepareTransaction(*pwalletMain, selector, {}, testnetzaddr, chainActive, strategy, -1, 1)
auto operation = AsyncRPCOperation_mergetoaddress(builder, selector, {}, testnetzaddr, strategy, -1); .map_error([&](const auto& err) {
BOOST_FAIL("Fee value of -1 expected to be out of the valid range of values."); // TODO: Provide `operator==` on `InputSelectionError` and use that here.
} catch (const UniValue& objError) { BOOST_CHECK(examine(err, match {
BOOST_CHECK( find_error(objError, "Fee is out of range")); [](AddressResolutionError are) {
} // FIXME: Should be failing because of negative fee change recipient address to not be Sprout.
return are == AddressResolutionError::SproutRecipientsNotSupported;
},
[](const auto&) { return false; },
}));
})
.map([](const auto&) {
BOOST_FAIL("Fee value of -1 expected to be out of the valid range of values.");
});
{ builder.PrepareTransaction(*pwalletMain, selector, {}, testnetzaddr, chainActive, strategy, 1, 1)
auto operation = AsyncRPCOperation_mergetoaddress(builder, selector, {}, testnetzaddr, strategy, 1); .map_error([&](const auto& err) {
operation.main(); // TODO: Provide `operator==` on `InputSelectionError` and use that here.
BOOST_CHECK_EQUAL(operation.getErrorMessage(), "Insufficient funds: have -0.00000001, need 0.00000001; 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."); BOOST_CHECK(examine(err, match {
} [](const InvalidFundsError& ife) {
return std::holds_alternative<InsufficientFundsError>(ife.reason);
},
[](const auto&) { return false; },
}));
})
.map([](const auto&) {
BOOST_FAIL("Expected an error.");
});
auto wtx = FakeWalletTx(); auto wtx = FakeWalletTx();
SpendableInputs inputs; SpendableInputs inputs;
@ -1664,11 +1677,20 @@ BOOST_AUTO_TEST_CASE(rpc_z_mergetoaddress_parameters)
inputs.sproutNoteEntries.emplace_back(SproutNoteEntry {JSOutPoint(), SproutPaymentAddress(), SproutNote(), "", 0}); inputs.sproutNoteEntries.emplace_back(SproutNoteEntry {JSOutPoint(), SproutPaymentAddress(), SproutNote(), "", 0});
inputs.saplingNoteEntries.emplace_back(SaplingNoteEntry {SaplingOutPoint(), SaplingPaymentAddress(), SaplingNote({}, uint256(), 0, uint256(), Zip212Enabled::BeforeZip212), "", 0}); inputs.saplingNoteEntries.emplace_back(SaplingNoteEntry {SaplingOutPoint(), SaplingPaymentAddress(), SaplingNote({}, uint256(), 0, uint256(), Zip212Enabled::BeforeZip212), "", 0});
{ builder.PrepareTransaction(*pwalletMain, selector, inputs, testnetzaddr, chainActive, strategy, 1, 1)
auto operation = AsyncRPCOperation_mergetoaddress(builder, selector, inputs, testnetzaddr, strategy, 1); .map_error([&](const auto& err) {
operation.main(); // TODO: Provide `operator==` on `InputSelectionError` and use that here.
BOOST_CHECK_EQUAL(operation.getErrorMessage(), "Insufficient funds: have 0.00, need 0.00000001; 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."); BOOST_CHECK(examine(err, match {
} [](AddressResolutionError are) {
// FIXME: Should be failing because of insufficient funds change recipient address to not be Sprout.
return are == AddressResolutionError::SproutRecipientsNotSupported;
},
[](const auto&) { return false; },
}));
})
.map([](const auto&) {
BOOST_FAIL("Expected an error.");
});
} }
void TestWTxStatus(const Consensus::Params consensusParams, const int delta) { void TestWTxStatus(const Consensus::Params consensusParams, const int delta) {

View File

@ -260,7 +260,7 @@ tl::expected<ChangeAddress, AddressResolutionError>
WalletTxBuilder::GetChangeAddress( WalletTxBuilder::GetChangeAddress(
CWallet& wallet, CWallet& wallet,
const ZTXOSelector& selector, const ZTXOSelector& selector,
SpendableInputs& spendable, const SpendableInputs& spendable,
const Payments& resolvedPayments, const Payments& resolvedPayments,
const TransactionStrategy& strategy, const TransactionStrategy& strategy,
bool afterNU5) const bool afterNU5) const
@ -385,11 +385,11 @@ tl::expected<TransactionEffects, InputSelectionError>
WalletTxBuilder::PrepareTransaction( WalletTxBuilder::PrepareTransaction(
CWallet& wallet, CWallet& wallet,
const ZTXOSelector& selector, const ZTXOSelector& selector,
SpendableInputs& spendable, const SpendableInputs& spendable,
const Recipients& payments, const Recipients& payments,
const CChain& chain, const CChain& chain,
TransactionStrategy strategy, const TransactionStrategy& strategy,
std::optional<CAmount> fee, const std::optional<CAmount>& fee,
uint32_t anchorConfirmations) const uint32_t anchorConfirmations) const
{ {
if (fee.has_value() && maxTxFee < fee.value()) { if (fee.has_value() && maxTxFee < fee.value()) {
@ -521,7 +521,7 @@ tl::expected<
WalletTxBuilder::IterateLimit( WalletTxBuilder::IterateLimit(
CWallet& wallet, CWallet& wallet,
const ZTXOSelector& selector, const ZTXOSelector& selector,
const TransactionStrategy strategy, const TransactionStrategy& strategy,
CAmount sendAmount, CAmount sendAmount,
CAmount dustThreshold, CAmount dustThreshold,
const SpendableInputs& spendable, const SpendableInputs& spendable,
@ -615,11 +615,11 @@ tl::expected<InputSelection, InputSelectionError>
WalletTxBuilder::ResolveInputsAndPayments( WalletTxBuilder::ResolveInputsAndPayments(
CWallet& wallet, CWallet& wallet,
const ZTXOSelector& selector, const ZTXOSelector& selector,
SpendableInputs& spendableMut, SpendableInputs spendableMut,
const std::vector<Payment>& payments, const std::vector<Payment>& payments,
const CChain& chain, const CChain& chain,
const TransactionStrategy& strategy, const TransactionStrategy& strategy,
std::optional<CAmount> fee, const std::optional<CAmount>& fee,
bool afterNU5) const bool afterNU5) const
{ {
LOCK2(cs_main, wallet.cs_wallet); LOCK2(cs_main, wallet.cs_wallet);

View File

@ -376,7 +376,7 @@ private:
GetChangeAddress( GetChangeAddress(
CWallet& wallet, CWallet& wallet,
const ZTXOSelector& selector, const ZTXOSelector& selector,
SpendableInputs& spendable, const SpendableInputs& spendable,
const Payments& resolvedPayments, const Payments& resolvedPayments,
const TransactionStrategy& strategy, const TransactionStrategy& strategy,
bool afterNU5) const; bool afterNU5) const;
@ -387,7 +387,7 @@ private:
IterateLimit( IterateLimit(
CWallet& wallet, CWallet& wallet,
const ZTXOSelector& selector, const ZTXOSelector& selector,
const TransactionStrategy strategy, const TransactionStrategy& strategy,
CAmount sendAmount, CAmount sendAmount,
CAmount dustThreshold, CAmount dustThreshold,
const SpendableInputs& spendable, const SpendableInputs& spendable,
@ -403,11 +403,11 @@ private:
ResolveInputsAndPayments( ResolveInputsAndPayments(
CWallet& wallet, CWallet& wallet,
const ZTXOSelector& selector, const ZTXOSelector& selector,
SpendableInputs& spendable, SpendableInputs spendable,
const std::vector<Payment>& payments, const std::vector<Payment>& payments,
const CChain& chain, const CChain& chain,
const TransactionStrategy& strategy, const TransactionStrategy& strategy,
std::optional<CAmount> fee, const std::optional<CAmount>& fee,
bool afterNU5) const; bool afterNU5) const;
/** /**
* Compute the internal and external OVKs to use in transaction construction, given * Compute the internal and external OVKs to use in transaction construction, given
@ -431,12 +431,12 @@ public:
PrepareTransaction( PrepareTransaction(
CWallet& wallet, CWallet& wallet,
const ZTXOSelector& selector, const ZTXOSelector& selector,
SpendableInputs& spendable, const SpendableInputs& spendable,
const Recipients& payments, const Recipients& payments,
const CChain& chain, const CChain& chain,
TransactionStrategy strategy, const TransactionStrategy& strategy,
/// A fixed fee is used if provided, otherwise it is calculated based on ZIP 317. /// A fixed fee is used if provided, otherwise it is calculated based on ZIP 317.
std::optional<CAmount> fee, const std::optional<CAmount>& fee,
uint32_t anchorConfirmations) const; uint32_t anchorConfirmations) const;
}; };