diff --git a/src/wallet/wallet_tx_builder.cpp b/src/wallet/wallet_tx_builder.cpp index 91020fffc..f9ad2991d 100644 --- a/src/wallet/wallet_tx_builder.cpp +++ b/src/wallet/wallet_tx_builder.cpp @@ -14,6 +14,188 @@ int GetAnchorHeight(const CChain& chain, uint32_t anchorConfirmations) return nextBlockHeight - anchorConfirmations; } +static size_t PadCount(size_t n) +{ + return n == 1 ? 2 : n; +} + +static CAmount +CalcZIP317Fee( + const std::optional& inputs, + const std::vector& payments, + const std::optional& changeAddr) +{ + std::vector vout{}; + size_t sproutOutputCount{}, saplingOutputCount{}, orchardOutputCount{}; + for (const auto& payment : payments) { + std::visit(match { + [&](const CKeyID& addr) { + vout.emplace_back(payment.amount, GetScriptForDestination(addr)); + }, + [&](const CScriptID& addr) { + vout.emplace_back(payment.amount, GetScriptForDestination(addr)); + }, + [&](const libzcash::SaplingPaymentAddress&) { + ++saplingOutputCount; + }, + [&](const libzcash::OrchardRawAddress&) { + ++orchardOutputCount; + } + }, payment.address); + } + + if (changeAddr.has_value()) { + examine(changeAddr.value(), match { + [&](const SproutPaymentAddress&) { ++sproutOutputCount; }, + [&](const RecipientAddress& addr) { + examine(addr, match { + [&](const CKeyID& taddr) { + vout.emplace_back(0, GetScriptForDestination(taddr)); + }, + [&](const CScriptID taddr) { + vout.emplace_back(0, GetScriptForDestination(taddr)); + }, + [&](const libzcash::SaplingPaymentAddress&) { ++saplingOutputCount; }, + [&](const libzcash::OrchardRawAddress&) { ++orchardOutputCount; } + }); + } + }); + } + + std::vector vin{}; + size_t sproutInputCount = 0; + size_t saplingInputCount = 0; + size_t orchardInputCount = 0; + if (inputs.has_value()) { + for (const auto& utxo : inputs.value().utxos) { + vin.emplace_back( + COutPoint(utxo.tx->GetHash(), utxo.i), + utxo.tx->vout[utxo.i].scriptPubKey); + } + sproutInputCount = inputs.value().sproutNoteEntries.size(); + saplingInputCount = inputs.value().saplingNoteEntries.size(); + orchardInputCount = inputs.value().orchardNoteMetadata.size(); + } + size_t logicalActionCount = CalculateLogicalActionCount( + vin, + vout, + std::max(sproutInputCount, sproutOutputCount), + saplingInputCount, + PadCount(saplingOutputCount), + PadCount(std::max(orchardInputCount, orchardOutputCount))); + + return CalculateConventionalFee(logicalActionCount); +} + +static tl::expected +ResolvePayment( + const Payment& payment, + bool canResolveOrchard, + const TransactionStrategy& strategy, + CAmount& maxSaplingAvailable, + CAmount& maxOrchardAvailable, + uint32_t& orchardOutputs) +{ + return examine(payment.GetAddress(), match { + [&](const CKeyID& p2pkh) -> tl::expected { + if (strategy.AllowRevealedRecipients()) { + return {{std::nullopt, p2pkh, payment.GetAmount(), payment.GetMemo(), false}}; + } else { + return tl::make_unexpected(AddressResolutionError::TransparentRecipientNotAllowed); + } + }, + [&](const CScriptID& p2sh) -> tl::expected { + if (strategy.AllowRevealedRecipients()) { + return {{std::nullopt, p2sh, payment.GetAmount(), payment.GetMemo(), false}}; + } else { + return tl::make_unexpected(AddressResolutionError::TransparentRecipientNotAllowed); + } + }, + [&](const SproutPaymentAddress&) -> tl::expected { + return tl::make_unexpected(AddressResolutionError::SproutRecipientsNotSupported); + }, + [&](const SaplingPaymentAddress& addr) + -> tl::expected { + if (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxSaplingAvailable) { + if (!strategy.AllowRevealedAmounts()) { + maxSaplingAvailable -= payment.GetAmount(); + } + return {{std::nullopt, addr, payment.GetAmount(), payment.GetMemo(), false}}; + } else { + return tl::make_unexpected(AddressResolutionError::RevealingSaplingAmountNotAllowed); + } + }, + [&](const UnifiedAddress& ua) -> tl::expected { + if (canResolveOrchard + && ua.GetOrchardReceiver().has_value() + && (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxOrchardAvailable) + ) { + if (!strategy.AllowRevealedAmounts()) { + maxOrchardAvailable -= payment.GetAmount(); + } + orchardOutputs += 1; + return {{ + ua, + ua.GetOrchardReceiver().value(), + payment.GetAmount(), + payment.GetMemo(), + false + }}; + } else if (ua.GetSaplingReceiver().has_value() + && (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxSaplingAvailable) + ) { + if (!strategy.AllowRevealedAmounts()) { + maxSaplingAvailable -= payment.GetAmount(); + } + return {{ua, ua.GetSaplingReceiver().value(), payment.GetAmount(), payment.GetMemo(), false}}; + } else { + if (strategy.AllowRevealedRecipients()) { + if (ua.GetP2SHReceiver().has_value()) { + return {{ + ua, ua.GetP2SHReceiver().value(), payment.GetAmount(), std::nullopt, false}}; + } else if (ua.GetP2PKHReceiver().has_value()) { + return {{ + ua, ua.GetP2PKHReceiver().value(), payment.GetAmount(), std::nullopt, false}}; + } else { + // This should only occur when we have + // • an Orchard-only UA, + // • `AllowRevealedRecipients`, and + // • can’t resolve Orchard (which means either a Sprout selector or pre-NU5). + return tl::make_unexpected(AddressResolutionError::CouldNotResolveReceiver); + } + } else if (strategy.AllowRevealedAmounts()) { + return tl::make_unexpected(AddressResolutionError::TransparentReceiverNotAllowed); + } else { + return tl::make_unexpected(AddressResolutionError::RevealingReceiverAmountsNotAllowed); + } + } + } + }); +} + +InvalidFundsError ReportInvalidFunds( + const SpendableInputs& spendable, + bool hasPhantomChange, + CAmount fee, + CAmount dustThreshold, + CAmount targetAmount, + CAmount changeAmount) +{ + return InvalidFundsError( + spendable.Total(), + hasPhantomChange + // TODO: NEED TESTS TO EXERCISE THIS + ? InvalidFundsReason(PhantomChangeError(fee, dustThreshold)) + : (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 publicly in the fee.) + ? InvalidFundsReason(DustThresholdError(dustThreshold, changeAmount)) + : InvalidFundsReason(InsufficientFundsError(targetAmount)))); +} + static tl::expected ValidateAmount(const SpendableInputs& spendable, const CAmount& fee) { @@ -283,79 +465,6 @@ SpendableInputs WalletTxBuilder::FindAllSpendableInputs( return wallet.FindSpendableInputs(selector, minDepth, std::nullopt); } -static size_t PadCount(size_t n) -{ - return n == 1 ? 2 : n; -} - -static CAmount -CalcZIP317Fee( - const std::optional& inputs, - const std::vector& payments, - const std::optional& changeAddr) -{ - std::vector vout{}; - size_t sproutOutputCount{}, saplingOutputCount{}, orchardOutputCount{}; - for (const auto& payment : payments) { - std::visit(match { - [&](const CKeyID& addr) { - vout.emplace_back(payment.amount, GetScriptForDestination(addr)); - }, - [&](const CScriptID& addr) { - vout.emplace_back(payment.amount, GetScriptForDestination(addr)); - }, - [&](const libzcash::SaplingPaymentAddress&) { - ++saplingOutputCount; - }, - [&](const libzcash::OrchardRawAddress&) { - ++orchardOutputCount; - } - }, payment.address); - } - - if (changeAddr.has_value()) { - examine(changeAddr.value(), match { - [&](const SproutPaymentAddress&) { ++sproutOutputCount; }, - [&](const RecipientAddress& addr) { - examine(addr, match { - [&](const CKeyID& taddr) { - vout.emplace_back(0, GetScriptForDestination(taddr)); - }, - [&](const CScriptID taddr) { - vout.emplace_back(0, GetScriptForDestination(taddr)); - }, - [&](const libzcash::SaplingPaymentAddress&) { ++saplingOutputCount; }, - [&](const libzcash::OrchardRawAddress&) { ++orchardOutputCount; } - }); - } - }); - } - - std::vector vin{}; - size_t sproutInputCount = 0; - size_t saplingInputCount = 0; - size_t orchardInputCount = 0; - if (inputs.has_value()) { - for (const auto& utxo : inputs.value().utxos) { - vin.emplace_back( - COutPoint(utxo.tx->GetHash(), utxo.i), - utxo.tx->vout[utxo.i].scriptPubKey); - } - sproutInputCount = inputs.value().sproutNoteEntries.size(); - saplingInputCount = inputs.value().saplingNoteEntries.size(); - orchardInputCount = inputs.value().orchardNoteMetadata.size(); - } - size_t logicalActionCount = CalculateLogicalActionCount( - vin, - vout, - std::max(sproutInputCount, sproutOutputCount), - saplingInputCount, - PadCount(saplingOutputCount), - PadCount(std::max(orchardInputCount, orchardOutputCount))); - - return CalculateConventionalFee(logicalActionCount); -} - CAmount GetConstrainedFee( const std::optional& inputs, const std::vector& payments, @@ -369,29 +478,6 @@ CAmount GetConstrainedFee( return CWallet::ConstrainFee(CalcZIP317Fee(inputs, payments, changeAddr), DUMMY_TX_SIZE); } -InvalidFundsError ReportInvalidFunds( - const SpendableInputs& spendable, - bool hasPhantomChange, - CAmount fee, - CAmount dustThreshold, - CAmount targetAmount, - CAmount changeAmount) -{ - return InvalidFundsError( - spendable.Total(), - hasPhantomChange - // TODO: NEED TESTS TO EXERCISE THIS - ? InvalidFundsReason(PhantomChangeError(fee, dustThreshold)) - : (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 publicly in the fee.) - ? InvalidFundsReason(DustThresholdError(dustThreshold, changeAmount)) - : InvalidFundsReason(InsufficientFundsError(targetAmount)))); -} - static tl::expected AddChangePayment( const SpendableInputs& spendable, @@ -525,92 +611,6 @@ WalletTxBuilder::IterateLimit( return std::make_tuple(spendableMut, updatedFee, changeAddr); } -static tl::expected -ResolvePayment( - const Payment& payment, - bool canResolveOrchard, - const TransactionStrategy& strategy, - CAmount& maxSaplingAvailable, - CAmount& maxOrchardAvailable, - uint32_t& orchardOutputs) -{ - return examine(payment.GetAddress(), match { - [&](const CKeyID& p2pkh) -> tl::expected { - if (strategy.AllowRevealedRecipients()) { - return {{std::nullopt, p2pkh, payment.GetAmount(), payment.GetMemo(), false}}; - } else { - return tl::make_unexpected(AddressResolutionError::TransparentRecipientNotAllowed); - } - }, - [&](const CScriptID& p2sh) -> tl::expected { - if (strategy.AllowRevealedRecipients()) { - return {{std::nullopt, p2sh, payment.GetAmount(), payment.GetMemo(), false}}; - } else { - return tl::make_unexpected(AddressResolutionError::TransparentRecipientNotAllowed); - } - }, - [&](const SproutPaymentAddress&) -> tl::expected { - return tl::make_unexpected(AddressResolutionError::SproutRecipientsNotSupported); - }, - [&](const SaplingPaymentAddress& addr) - -> tl::expected { - if (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxSaplingAvailable) { - if (!strategy.AllowRevealedAmounts()) { - maxSaplingAvailable -= payment.GetAmount(); - } - return {{std::nullopt, addr, payment.GetAmount(), payment.GetMemo(), false}}; - } else { - return tl::make_unexpected(AddressResolutionError::RevealingSaplingAmountNotAllowed); - } - }, - [&](const UnifiedAddress& ua) -> tl::expected { - if (canResolveOrchard - && ua.GetOrchardReceiver().has_value() - && (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxOrchardAvailable) - ) { - if (!strategy.AllowRevealedAmounts()) { - maxOrchardAvailable -= payment.GetAmount(); - } - orchardOutputs += 1; - return {{ - ua, - ua.GetOrchardReceiver().value(), - payment.GetAmount(), - payment.GetMemo(), - false - }}; - } else if (ua.GetSaplingReceiver().has_value() - && (strategy.AllowRevealedAmounts() || payment.GetAmount() <= maxSaplingAvailable) - ) { - if (!strategy.AllowRevealedAmounts()) { - maxSaplingAvailable -= payment.GetAmount(); - } - return {{ua, ua.GetSaplingReceiver().value(), payment.GetAmount(), payment.GetMemo(), false}}; - } else { - if (strategy.AllowRevealedRecipients()) { - if (ua.GetP2SHReceiver().has_value()) { - return {{ - ua, ua.GetP2SHReceiver().value(), payment.GetAmount(), std::nullopt, false}}; - } else if (ua.GetP2PKHReceiver().has_value()) { - return {{ - ua, ua.GetP2PKHReceiver().value(), payment.GetAmount(), std::nullopt, false}}; - } else { - // This should only occur when we have - // • an Orchard-only UA, - // • `AllowRevealedRecipients`, and - // • can’t resolve Orchard (which means either a Sprout selector or pre-NU5). - return tl::make_unexpected(AddressResolutionError::CouldNotResolveReceiver); - } - } else if (strategy.AllowRevealedAmounts()) { - return tl::make_unexpected(AddressResolutionError::TransparentReceiverNotAllowed); - } else { - return tl::make_unexpected(AddressResolutionError::RevealingReceiverAmountsNotAllowed); - } - } - } - }); -} - tl::expected WalletTxBuilder::ResolveInputsAndPayments( CWallet& wallet,