#![cfg_attr(
feature = "transparent-inputs",
doc = "
Another important high-level operation provided by this module is [`propose_shielding`], which
takes a set of transparent source addresses, and constructs a [`Proposal`] to send those funds
to a wallet-internal shielded address, as described in [ZIP 316](https://zips.z.cash/zip-0316).
[`propose_shielding`]: crate::data_api::wallet::propose_shielding
"
)]
use nonempty::NonEmpty;
use rand::{rand_core::UnwrapErr, rngs::SysRng};
use std::{
num::NonZeroU32,
ops::{Add, Sub},
};
use shardtree::error::{QueryError, ShardTreeError};
pub use super::locking::{LockRequest, unlock_proposal_inputs};
use super::{InputSource, locking::lock_proposal_inputs};
use crate::{
data_api::{
Account, MaxSpendMode, NoteCommitmentTree, SentTransaction, SentTransactionOutput,
WalletCommitmentTrees, WalletRead, WalletWrite, error::Error,
wallet::input_selection::propose_send_max,
},
decrypt_transaction,
fees::{
ChangeStrategy, DustOutputPolicy, StandardFeeRule, standard::SingleOutputChangeStrategy,
},
proposal::{Proposal, ProposalError, Step, StepOutputIndex},
wallet::{Note, OvkPolicy, Recipient},
};
use sapling::{
note_encryption::{PreparedIncomingViewingKey, try_sapling_note_decryption},
prover::{OutputProver, SpendProver},
};
use transparent::{address::TransparentAddress, builder::TransparentSigningSet, bundle::OutPoint};
use zcash_address::ZcashAddress;
use zcash_keys::{
address::Address,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
};
use zcash_primitives::transaction::{
Transaction, TxId, TxVersion,
builder::{BuildConfig, BuildResult, Builder, BundlePadding},
components::sapling::zip212_enforcement,
fees::FeeRule,
};
use zcash_protocol::{
PoolType, ShieldedPool,
consensus::{self, BlockHeight},
memo::MemoBytes,
value::Zatoshis,
zip318::AnchorBucketInterval,
};
use zip32::Scope;
use zip321::Payment;
#[cfg(feature = "orchard")]
use {
crate::data_api::anchor_retention::PoolMigrationParams,
zcash_protocol::{consensus::NetworkUpgrade, zip318::PoolMigrationConstants},
};
#[cfg(feature = "transparent-inputs")]
use {
super::CoinbaseFilter,
crate::{
fees::ChangeValue,
proposal::StepOutput,
wallet::{TransparentAddressMetadata, TransparentAddressSource},
},
core::convert::Infallible,
input_selection::ShieldingSelector,
std::collections::HashMap,
transparent::bundle::TxOut,
};
#[cfg(feature = "transparent-key-import")]
use zcash_script::script::{self as zs_script, Evaluable};
#[cfg(feature = "pczt")]
use {
crate::data_api::error::PcztError,
bip32::ChildNumber,
orchard::note_encryption::{IronwoodDomain, OrchardDomain},
pczt::roles::{
creator::Creator, io_finalizer::IoFinalizer, redactor::Redactor,
spend_finalizer::SpendFinalizer, tx_extractor::TransactionExtractor, updater::Updater,
},
sapling::note_encryption::SaplingDomain,
serde::{Deserialize, Serialize},
std::collections::BTreeMap,
transparent::pczt::Bip32Derivation,
zcash_note_encryption::{
Domain, ENC_CIPHERTEXT_SIZE, ShieldedOutput, try_output_recovery_with_pkd_esk,
},
zcash_protocol::{consensus::NetworkConstants, value::BalanceError},
};
pub mod input_selection;
use input_selection::{
GreedyInputSelector, GreedyInputSelectorError, InputSelector, InputSelectorError,
};
#[cfg(feature = "pczt")]
const PROPRIETARY_PROPOSAL_INFO: &str = "zcash_client_backend:proposal_info";
#[cfg(feature = "pczt")]
const PROPRIETARY_OUTPUT_INFO: &str = "zcash_client_backend:output_info";
#[cfg(feature = "orchard")]
fn ironwood_active_at<ParamsT: consensus::Parameters, H: Into<BlockHeight>>(
params: &ParamsT,
target_height: H,
) -> bool {
params.is_nu_active(NetworkUpgrade::Nu6_3, target_height.into())
}
#[cfg(feature = "pczt")]
fn serialize_target_height<S>(
target_height: &TargetHeight,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let u: u32 = BlockHeight::from(*target_height).into();
u.serialize(serializer)
}
#[cfg(feature = "pczt")]
fn deserialize_target_height<'de, D>(deserializer: D) -> Result<TargetHeight, D::Error>
where
D: serde::Deserializer<'de>,
{
let u = u32::deserialize(deserializer)?;
Ok(BlockHeight::from_u32(u).into())
}
#[cfg(feature = "pczt")]
#[derive(Serialize, Deserialize)]
struct ProposalInfo<AccountId> {
from_account: AccountId,
#[serde(
serialize_with = "serialize_target_height",
deserialize_with = "deserialize_target_height"
)]
target_height: TargetHeight,
}
#[cfg(feature = "pczt")]
#[derive(Serialize, Deserialize)]
enum PcztRecipient<AccountId> {
External,
#[cfg(feature = "transparent-inputs")]
EphemeralTransparent {
receiving_account: AccountId,
},
InternalShielded {
receiving_account: AccountId,
},
#[cfg(feature = "transparent-inputs")]
InternalTransparent {
receiving_account: AccountId,
},
}
#[cfg(feature = "pczt")]
impl<AccountId: Copy> PcztRecipient<AccountId> {
fn from_shielded_recipient(
recipient: ShieldedBuildRecipient<AccountId>,
) -> (Self, Option<ZcashAddress>) {
match recipient {
ShieldedBuildRecipient::External {
recipient_address, ..
} => (PcztRecipient::External, Some(recipient_address)),
ShieldedBuildRecipient::InternalShielded {
receiving_account,
external_address,
} => (
PcztRecipient::InternalShielded { receiving_account },
external_address,
),
}
}
fn from_transparent_recipient(
recipient: TransparentBuildRecipient<AccountId>,
) -> (Self, Option<ZcashAddress>) {
match recipient {
TransparentBuildRecipient::External {
recipient_address, ..
} => (PcztRecipient::External, Some(recipient_address)),
#[cfg(feature = "transparent-inputs")]
TransparentBuildRecipient::EphemeralTransparent {
receiving_account, ..
} => (
PcztRecipient::EphemeralTransparent { receiving_account },
None,
),
#[cfg(feature = "transparent-inputs")]
TransparentBuildRecipient::InternalTransparent {
receiving_account, ..
} => (
PcztRecipient::InternalTransparent { receiving_account },
None,
),
}
}
}
pub fn decrypt_and_store_transaction<ParamsT, DbT>(
params: &ParamsT,
data: &mut DbT,
tx: &Transaction,
mined_height: Option<BlockHeight>,
) -> Result<(), <DbT as WalletRead>::Error>
where
ParamsT: consensus::Parameters,
DbT: WalletWrite,
{
let ufvks = data.get_unified_full_viewing_keys()?;
data.store_decrypted_tx(decrypt_transaction(
params,
mined_height.map_or_else(|| data.get_tx_height(tx.txid()), |h| Ok(Some(h)))?,
data.chain_height()?,
tx,
&ufvks,
))?;
Ok(())
}
pub type ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
<InputsT as InputSelector>::Error,
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
<ChangeT as ChangeStrategy>::Error,
<<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
>;
pub type ProposeSendMaxErrT<DbT, CommitmentTreeErrT, FeeRuleT> = Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
GreedyInputSelectorError,
<FeeRuleT as FeeRule>::Error,
<FeeRuleT as FeeRule>::Error,
<DbT as InputSource>::NoteRef,
>;
#[cfg(feature = "transparent-inputs")]
pub type ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
<InputsT as ShieldingSelector>::Error,
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
<ChangeT as ChangeStrategy>::Error,
Infallible,
>;
pub type CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N> = Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
InputsErrT,
<FeeRuleT as FeeRule>::Error,
ChangeErrT,
N,
>;
pub type TransferErrT<DbT, InputsT, ChangeT> = Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
<InputsT as InputSelector>::Error,
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
<ChangeT as ChangeStrategy>::Error,
<<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
>;
#[cfg(feature = "transparent-inputs")]
pub type ShieldErrT<DbT, InputsT, ChangeT> = Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
<InputsT as ShieldingSelector>::Error,
<<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
<ChangeT as ChangeStrategy>::Error,
Infallible,
>;
#[cfg(feature = "pczt")]
pub type ExtractErrT<DbT, N> = Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
Infallible,
Infallible,
Infallible,
N,
>;
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct TargetHeight(BlockHeight);
impl TargetHeight {
pub fn saturating_sub(self, value: u32) -> BlockHeight {
self.0.saturating_sub(value)
}
}
impl From<BlockHeight> for TargetHeight {
fn from(value: BlockHeight) -> Self {
TargetHeight(value)
}
}
impl From<TargetHeight> for BlockHeight {
fn from(value: TargetHeight) -> Self {
value.0
}
}
impl From<TargetHeight> for u32 {
fn from(value: TargetHeight) -> Self {
u32::from(value.0)
}
}
impl From<u32> for TargetHeight {
fn from(value: u32) -> Self {
TargetHeight(BlockHeight::from_u32(value))
}
}
impl<I> Add<I> for TargetHeight
where
BlockHeight: Add<I>,
{
type Output = <BlockHeight as Add<I>>::Output;
fn add(self, rhs: I) -> Self::Output {
self.0 + rhs
}
}
impl<I> Sub<I> for TargetHeight
where
BlockHeight: Sub<I>,
{
type Output = <BlockHeight as Sub<I>>::Output;
fn sub(self, rhs: I) -> Self::Output {
self.0 - rhs
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ConfirmationsPolicyError;
impl core::fmt::Display for ConfirmationsPolicyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"Trusted confirmations must not exceed untrusted confirmations"
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ConfirmationsPolicy {
trusted: NonZeroU32,
untrusted: NonZeroU32,
#[cfg(feature = "transparent-inputs")]
allow_zero_conf_shielding: bool,
}
impl Default for ConfirmationsPolicy {
fn default() -> Self {
ConfirmationsPolicy {
trusted: NonZeroU32::MIN.saturating_add(2),
untrusted: NonZeroU32::MIN.saturating_add(9),
#[cfg(feature = "transparent-inputs")]
allow_zero_conf_shielding: true,
}
}
}
impl ConfirmationsPolicy {
pub const MIN: Self = ConfirmationsPolicy {
trusted: NonZeroU32::MIN,
untrusted: NonZeroU32::MIN,
#[cfg(feature = "transparent-inputs")]
allow_zero_conf_shielding: true,
};
pub fn new(
trusted: NonZeroU32,
untrusted: NonZeroU32,
#[cfg(feature = "transparent-inputs")] allow_zero_conf_shielding: bool,
) -> Result<Self, ConfirmationsPolicyError> {
if trusted > untrusted {
Err(ConfirmationsPolicyError)
} else {
Ok(Self {
trusted,
untrusted,
#[cfg(feature = "transparent-inputs")]
allow_zero_conf_shielding,
})
}
}
pub fn new_symmetrical(
min_confirmations: NonZeroU32,
#[cfg(feature = "transparent-inputs")] allow_zero_conf_shielding: bool,
) -> Self {
Self {
trusted: min_confirmations,
untrusted: min_confirmations,
#[cfg(feature = "transparent-inputs")]
allow_zero_conf_shielding,
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub fn new_unchecked(
trusted: u32,
untrusted: u32,
#[cfg(feature = "transparent-inputs")] allow_zero_conf_shielding: bool,
) -> Self {
Self::new(
NonZeroU32::new(trusted).expect("trusted must be nonzero"),
NonZeroU32::new(untrusted).expect("untrusted must be nonzero"),
#[cfg(feature = "transparent-inputs")]
allow_zero_conf_shielding,
)
.expect("trusted must be <= untrusted")
}
#[cfg(any(test, feature = "test-dependencies"))]
pub fn new_symmetrical_unchecked(
min_confirmations: u32,
#[cfg(feature = "transparent-inputs")] allow_zero_conf_shielding: bool,
) -> Self {
Self::new_symmetrical(
NonZeroU32::new(min_confirmations).expect("min_confirmations must be nonzero"),
#[cfg(feature = "transparent-inputs")]
allow_zero_conf_shielding,
)
}
pub fn trusted(&self) -> NonZeroU32 {
self.trusted
}
pub fn untrusted(&self) -> NonZeroU32 {
self.untrusted
}
pub fn anchor_height(&self, target_height: TargetHeight) -> BlockHeight {
target_height.saturating_sub(u32::from(self.trusted()))
}
pub fn bucketed(
&self,
interval: AnchorBucketInterval,
target_height: TargetHeight,
activation_height: BlockHeight,
) -> Option<Self> {
let most_recent = interval.boundary_at_or_below(self.anchor_height(target_height));
let boundary = BlockHeight::from_u32(
u32::from(most_recent).checked_sub(interval.block_count().get())?,
);
if boundary <= activation_height {
return None;
}
let bucketed = u32::from(target_height).checked_sub(u32::from(boundary))?;
let trusted = NonZeroU32::new(bucketed)?;
Self::new(
trusted,
core::cmp::max(self.untrusted(), trusted),
#[cfg(feature = "transparent-inputs")]
self.allow_zero_conf_shielding(),
)
.ok()
}
#[cfg(feature = "transparent-inputs")]
pub fn allow_zero_conf_shielding(&self) -> bool {
self.allow_zero_conf_shielding
}
#[allow(clippy::too_many_arguments)]
pub fn confirmations_until_spendable(
&self,
target_height: TargetHeight,
pool_type: PoolType,
receiving_key_scope: Option<Scope>,
mined_height: Option<BlockHeight>,
tx_trusted: bool,
max_shielding_input_height: Option<BlockHeight>,
tx_shielding_inputs_trusted: bool,
) -> u32 {
let trusted_height = target_height.saturating_sub(u32::from(self.trusted));
let untrusted_height = target_height.saturating_sub(u32::from(self.untrusted));
let confs_for_trusted =
mined_height.map_or(u32::from(self.trusted), |h| h - trusted_height);
let confs_for_untrusted =
mined_height.map_or(u32::from(self.untrusted), |h| h - untrusted_height);
match pool_type {
PoolType::Transparent => {
#[cfg(feature = "transparent-inputs")]
let zc_shielding = self.allow_zero_conf_shielding;
#[cfg(not(feature = "transparent-inputs"))]
let zc_shielding = false;
if zc_shielding {
0
} else if tx_trusted || receiving_key_scope == Some(Scope::Internal) {
confs_for_trusted
} else {
confs_for_untrusted
}
}
PoolType::Shielded(_) => {
if tx_trusted {
confs_for_trusted
} else if receiving_key_scope == Some(Scope::Internal) {
if let Some(h) = max_shielding_input_height {
if tx_shielding_inputs_trusted {
h - trusted_height
} else {
h - untrusted_height
}
} else {
confs_for_trusted
}
} else {
confs_for_untrusted
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn propose_transfer<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_from_account: <DbT as InputSource>::AccountId,
input_selector: &InputsT,
change_strategy: &ChangeT,
request: zip321::TransactionRequest,
confirmations_policy: ConfirmationsPolicy,
spend_policy: &input_selection::SpendPolicy,
lock_inputs: Option<LockRequest>,
proposed_version: Option<TxVersion>,
) -> Result<
Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
>
where
DbT: WalletWrite + InputSource<Error = <DbT as WalletRead>::Error>,
<DbT as InputSource>::NoteRef: Copy + Eq + Ord,
ParamsT: consensus::Parameters + Clone,
InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let maybe_intial_heights = wallet_db
.get_target_and_anchor_heights(confirmations_policy.trusted)
.map_err(InputSelectorError::DataSource)?;
let (target_height, anchor_height) =
maybe_intial_heights.ok_or_else(|| InputSelectorError::SyncRequired)?;
let zip318 = wallet_db.pool_migration_params();
#[cfg(feature = "orchard")]
let canonical_fee = crate::fees::canonical_crossing_fee(params, target_height.into()).ok();
#[cfg(feature = "orchard")]
let bucketed_policy = canonical_crossing_candidate(params, &zip318, &request, target_height)
.then(|| params.activation_height(NetworkUpgrade::Nu6_3))
.flatten()
.and_then(|activation| {
confirmations_policy.bucketed(
zip318.anchor_bucket_interval(),
target_height,
activation,
)
})
.filter(|_| spend_policy.permits_shielded(ShieldedPool::Orchard));
#[cfg(feature = "orchard")]
let bucketed_policy = match bucketed_policy {
Some(policy) => wallet_db
.anchor_computable(ShieldedPool::Orchard, policy.anchor_height(target_height))
.map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
.then_some(policy),
None => None,
};
#[cfg(feature = "orchard")]
let canonical_attempt = bucketed_policy.map(|bucketed_policy| {
let orchard_only = input_selection::SpendPolicy::shielded_pools([ShieldedPool::Orchard])
.with_locked_input_policy(spend_policy.locked_input_policy().clone())
.with_note_selection(input_selection::NoteSelection::PreferSingle);
input_selector.propose_transaction(
params,
wallet_db,
target_height,
bucketed_policy.anchor_height(target_height),
&zip318,
bucketed_policy,
spend_from_account,
request.clone(),
change_strategy,
&orchard_only,
proposed_version,
)
});
#[cfg(feature = "orchard")]
let canonical_proposal = match canonical_attempt {
Some(Ok(proposal)) => {
let is_canonical = proposal.steps().len() == 1
&& canonical_fee.is_some_and(|fee| {
proposal.steps().first().is_canonical_crossing(&zip318, fee)
});
is_canonical.then_some(proposal)
}
Some(Err(InputSelectorError::InsufficientFunds { .. })) | None => None,
Some(Err(other)) => return Err(other.into()),
};
#[cfg(not(feature = "orchard"))]
let canonical_proposal = None;
let proposal = match canonical_proposal {
Some(proposal) => proposal,
None => input_selector.propose_transaction(
params,
wallet_db,
target_height,
anchor_height,
&zip318,
confirmations_policy,
spend_from_account,
request,
change_strategy,
spend_policy,
proposed_version,
)?,
};
if let Some(request) = lock_inputs {
let lock_expiry_height = target_height + request.for_blocks();
lock_proposal_inputs(wallet_db, &proposal, request.owner(), lock_expiry_height)?;
}
Ok(proposal.with_proposed_version(proposed_version))
}
#[cfg(feature = "orchard")]
fn canonical_crossing_candidate<ParamsT: consensus::Parameters>(
params: &ParamsT,
zip318: &PoolMigrationParams,
request: &zip321::TransactionRequest,
target_height: TargetHeight,
) -> bool {
params.is_nu_active(NetworkUpgrade::Nu6_3, target_height.into())
&& match request.payments().values().collect::<Vec<_>>()[..] {
[payment] => payment
.amount()
.is_some_and(|amount| zip318.is_canonical_denomination(amount)),
_ => false,
}
}
#[cfg(feature = "orchard")]
fn step_is_canonical_crossing<DbT, ParamsT, N>(
wallet_db: &DbT,
params: &ParamsT,
step: &Step<N>,
target_height: TargetHeight,
) -> bool
where
DbT: WalletRead,
ParamsT: consensus::Parameters,
{
crate::fees::canonical_crossing_fee(params, target_height.into())
.is_ok_and(|fee| step.is_canonical_crossing(&wallet_db.pool_migration_params(), fee))
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
wallet_db: &mut DbT,
params: &ParamsT,
fee_rule: StandardFeeRule,
spend_from_account: <DbT as InputSource>::AccountId,
confirmations_policy: ConfirmationsPolicy,
to: &Address,
amount: Zatoshis,
memo: Option<MemoBytes>,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedPool,
lock_inputs: Option<LockRequest>,
proposed_version: Option<TxVersion>,
) -> Result<
Proposal<StandardFeeRule, DbT::NoteRef>,
ProposeTransferErrT<
DbT,
CommitmentTreeErrT,
GreedyInputSelector<DbT>,
SingleOutputChangeStrategy<DbT>,
>,
>
where
ParamsT: consensus::Parameters + Clone,
DbT: InputSource,
DbT: WalletWrite,
DbT: WalletRead<Error = <DbT as InputSource>::Error, AccountId = <DbT as InputSource>::AccountId>,
DbT::NoteRef: Copy + Eq + Ord,
{
let request = zip321::TransactionRequest::new(vec![
Payment::new(
to.to_zcash_address(params),
Some(amount),
memo,
None,
None,
vec![],
)
.map_err(Error::Payment)?,
])
.expect(
"It should not be possible for this to violate ZIP 321 request construction invariants.",
);
let input_selector = GreedyInputSelector::<DbT>::new();
let change_strategy = SingleOutputChangeStrategy::<DbT>::new(
fee_rule,
change_memo,
fallback_change_pool,
DustOutputPolicy::default(),
);
propose_transfer(
wallet_db,
params,
spend_from_account,
&input_selector,
&change_strategy,
request,
confirmations_policy,
&input_selection::SpendPolicy::default(),
lock_inputs,
proposed_version,
)
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn propose_send_max_transfer<DbT, ParamsT, FeeRuleT, CommitmentTreeErrT>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_from_account: <DbT as InputSource>::AccountId,
spend_pools: &[ShieldedPool],
fee_rule: &FeeRuleT,
recipient: ZcashAddress,
memo: Option<MemoBytes>,
mode: MaxSpendMode,
confirmations_policy: ConfirmationsPolicy,
locked_input_policy: &input_selection::LockedInputPolicy,
lock_inputs: Option<LockRequest>,
) -> Result<
Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
ProposeSendMaxErrT<DbT, CommitmentTreeErrT, FeeRuleT>,
>
where
DbT: WalletWrite + InputSource<Error = <DbT as WalletRead>::Error>,
<DbT as InputSource>::NoteRef: Copy + Eq + Ord,
ParamsT: consensus::Parameters + Clone,
FeeRuleT: FeeRule + Clone,
{
let (target_height, anchor_height) = wallet_db
.get_target_and_anchor_heights(confirmations_policy.trusted())
.map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
.ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
if memo.is_some() && !recipient.can_receive_memo() {
return Err(Error::Payment(zip321::PaymentError::TransparentMemo));
}
let proposal = propose_send_max(
params,
wallet_db,
fee_rule,
spend_from_account,
spend_pools,
target_height,
anchor_height,
mode,
confirmations_policy,
recipient,
memo,
locked_input_policy,
)?;
if let Some(request) = lock_inputs {
let lock_expiry_height = target_height + request.for_blocks();
lock_proposal_inputs(wallet_db, &proposal, request.owner(), lock_expiry_height)?;
}
Ok(proposal)
}
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn propose_shielding<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
wallet_db: &mut DbT,
params: &ParamsT,
input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: Zatoshis,
from_addrs: &[TransparentAddress],
to_account: <DbT as InputSource>::AccountId,
confirmations_policy: ConfirmationsPolicy,
output_filter: CoinbaseFilter,
lock_inputs: Option<LockRequest>,
) -> Result<
Proposal<ChangeT::FeeRule, Infallible>,
ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
>
where
ParamsT: consensus::Parameters,
DbT: WalletWrite + InputSource<Error = <DbT as WalletRead>::Error>,
InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let (target_height, anchor_height) = wallet_db
.get_target_and_anchor_heights(confirmations_policy.trusted)
.map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
.ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
let proposal = input_selector
.propose_shielding(
params,
wallet_db,
change_strategy,
shielding_threshold,
from_addrs,
to_account,
target_height,
anchor_height,
&wallet_db.pool_migration_params(),
confirmations_policy,
output_filter,
)
.map_err(Error::from)?;
if let Some(request) = lock_inputs {
let lock_expiry_height = target_height + request.for_blocks();
lock_proposal_inputs(wallet_db, &proposal, request.owner(), lock_expiry_height)?;
}
Ok(proposal)
}
#[cfg(feature = "transparent-inputs")]
pub type ProposeShieldingCoinbaseErrT<DbT, CommitmentTreeErrT, InputsT, FeeRuleT> = Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
<InputsT as ShieldingSelector>::Error,
<FeeRuleT as FeeRule>::Error,
<FeeRuleT as FeeRule>::Error,
Infallible,
>;
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn propose_shielding_coinbase<DbT, ParamsT, InputsT, FeeRuleT, CommitmentTreeErrT>(
wallet_db: &mut DbT,
params: &ParamsT,
input_selector: &InputsT,
fee_rule: &FeeRuleT,
shielding_threshold: Zatoshis,
from_addrs: &[TransparentAddress],
to_address: ZcashAddress,
memo: Option<MemoBytes>,
limit: Option<usize>,
lock_inputs: Option<LockRequest>,
) -> Result<
Proposal<FeeRuleT, Infallible>,
ProposeShieldingCoinbaseErrT<DbT, CommitmentTreeErrT, InputsT, FeeRuleT>,
>
where
ParamsT: consensus::Parameters,
DbT: WalletWrite + InputSource<Error = <DbT as WalletRead>::Error>,
InputsT: ShieldingSelector<InputSource = DbT>,
FeeRuleT: FeeRule + Clone,
{
let (target_height, anchor_height) = wallet_db
.get_target_and_anchor_heights(ConfirmationsPolicy::default().trusted)
.map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
.ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
let proposal = input_selector
.propose_shielding_coinbase(
params,
wallet_db,
fee_rule,
shielding_threshold,
from_addrs,
to_address,
memo,
limit,
target_height,
anchor_height,
)
.map_err(Error::from)?;
if let Some(request) = lock_inputs {
let lock_expiry_height = target_height + request.for_blocks();
lock_proposal_inputs(wallet_db, &proposal, request.owner(), lock_expiry_height)?;
}
Ok(proposal)
}
struct StepResult<AccountId> {
build_result: BuildResult,
outputs: Vec<SentTransactionOutput<AccountId>>,
fee_amount: Zatoshis,
#[cfg(feature = "transparent-inputs")]
utxos_spent: Vec<OutPoint>,
}
pub struct SpendingKeys {
usk: UnifiedSpendingKey,
#[cfg(feature = "transparent-key-import")]
standalone_transparent_keys: HashMap<TransparentAddress, Vec<secp256k1::SecretKey>>,
}
impl SpendingKeys {
pub fn new(
usk: UnifiedSpendingKey,
#[cfg(feature = "transparent-key-import")] standalone_transparent_keys: HashMap<
TransparentAddress,
Vec<secp256k1::SecretKey>,
>,
) -> Self {
Self {
usk,
#[cfg(feature = "transparent-key-import")]
standalone_transparent_keys,
}
}
pub fn from_unified_spending_key(usk: UnifiedSpendingKey) -> Self {
Self {
usk,
#[cfg(feature = "transparent-key-import")]
standalone_transparent_keys: HashMap::new(),
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
spending_keys: &SpendingKeys,
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, N>,
expiry_height: Option<BlockHeight>,
) -> Result<NonEmpty<TxId>, CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
where
DbT: WalletWrite + WalletCommitmentTrees,
ParamsT: consensus::Parameters + Clone,
FeeRuleT: FeeRule,
{
let proposed_version = proposal.proposed_version();
if let Some(expiry_height) = expiry_height {
let min_target_height = BlockHeight::from(proposal.min_target_height());
if expiry_height != consensus::H0 && expiry_height < min_target_height {
return Err(Error::ExpiryHeightBelowTargetHeight {
expiry_height,
min_target_height,
});
}
}
#[cfg(feature = "transparent-inputs")]
let mut unused_transparent_outputs = HashMap::new();
let account_id = wallet_db
.get_account_for_ufvk(&spending_keys.usk.to_unified_full_viewing_key())
.map_err(Error::DataSource)?
.ok_or(Error::KeyNotRecognized)?
.id();
let mut step_results = Vec::with_capacity(proposal.steps().len());
for step in proposal.steps() {
let step_result: StepResult<_> = create_proposed_transaction(
wallet_db,
params,
spend_prover,
output_prover,
spending_keys,
account_id,
ovk_policy.clone(),
proposal.fee_rule(),
proposal.min_target_height(),
proposal.confirmations_policy(),
&step_results,
step,
#[cfg(feature = "transparent-inputs")]
&mut unused_transparent_outputs,
proposed_version,
expiry_height,
)?;
step_results.push((step, step_result));
}
#[cfg(feature = "transparent-inputs")]
for so in unused_transparent_outputs.into_keys() {
if let StepOutputIndex::Change(i) = so.output_index() {
if step_results[so.step_index()].0.balance().proposed_change()[i].is_ephemeral() {
return Err(ProposalError::EphemeralOutputLeftUnspent(so).into());
}
}
}
let created = time::OffsetDateTime::now_utc();
let mut transactions = Vec::with_capacity(step_results.len());
let mut txids = Vec::with_capacity(step_results.len());
#[allow(unused_variables)]
for (_, step_result) in step_results.iter() {
let tx = step_result.build_result.transaction();
transactions.push(SentTransaction::new(
tx,
created,
proposal.min_target_height(),
account_id,
&step_result.outputs,
step_result.fee_amount,
#[cfg(feature = "transparent-inputs")]
&step_result.utxos_spent,
));
txids.push(tx.txid());
}
wallet_db
.store_transactions_to_be_sent(&transactions)
.map_err(Error::DataSource)?;
Ok(NonEmpty::from_vec(txids).expect("proposal.steps is NonEmpty"))
}
#[derive(Debug, Clone)]
enum ShieldedBuildRecipient<AccountId> {
External {
recipient_address: ZcashAddress,
output_pool: PoolType,
},
InternalShielded {
receiving_account: AccountId,
external_address: Option<ZcashAddress>,
},
}
impl<AccountId> ShieldedBuildRecipient<AccountId> {
fn into_recipient(self, note: impl FnOnce() -> Note) -> Recipient<AccountId> {
match self {
ShieldedBuildRecipient::External {
recipient_address,
output_pool,
} => Recipient::External {
recipient_address,
output_pool,
},
ShieldedBuildRecipient::InternalShielded {
receiving_account,
external_address,
} => Recipient::InternalShielded {
receiving_account,
external_address,
note: Box::new(note()),
},
}
}
}
#[derive(Debug, Clone)]
enum TransparentBuildRecipient<AccountId> {
External {
recipient_address: ZcashAddress,
output_pool: PoolType,
},
#[cfg(feature = "transparent-inputs")]
EphemeralTransparent {
receiving_account: AccountId,
ephemeral_address: TransparentAddress,
},
#[cfg(feature = "transparent-inputs")]
InternalTransparent {
receiving_account: AccountId,
recipient_address: TransparentAddress,
},
#[cfg(not(feature = "transparent-inputs"))]
#[doc(hidden)]
#[allow(dead_code)]
Unconstructible(
core::marker::PhantomData<AccountId>,
core::convert::Infallible,
),
}
impl<AccountId> TransparentBuildRecipient<AccountId> {
fn into_recipient(
self,
#[cfg(feature = "transparent-inputs")] outpoint: OutPoint,
) -> Recipient<AccountId> {
match self {
TransparentBuildRecipient::External {
recipient_address,
output_pool,
} => Recipient::External {
recipient_address,
output_pool,
},
#[cfg(feature = "transparent-inputs")]
TransparentBuildRecipient::EphemeralTransparent {
receiving_account,
ephemeral_address,
} => Recipient::EphemeralTransparent {
receiving_account,
ephemeral_address,
outpoint,
},
#[cfg(feature = "transparent-inputs")]
TransparentBuildRecipient::InternalTransparent {
receiving_account,
recipient_address,
} => Recipient::InternalTransparent {
receiving_account,
recipient_address,
},
#[cfg(not(feature = "transparent-inputs"))]
TransparentBuildRecipient::Unconstructible(_, absurd) => match absurd {},
}
}
}
#[allow(clippy::type_complexity)]
struct BuildState<P, AccountId> {
#[cfg(feature = "transparent-inputs")]
step_index: usize,
builder: Builder<P, ()>,
#[cfg(feature = "transparent-inputs")]
transparent_input_addresses: HashMap<TransparentAddress, TransparentAddressMetadata>,
#[cfg(feature = "orchard")]
orchard_output_meta: Vec<(
ShieldedBuildRecipient<AccountId>,
Zatoshis,
Option<MemoBytes>,
)>,
#[cfg(feature = "orchard")]
ironwood_output_meta: Vec<(
ShieldedBuildRecipient<AccountId>,
Zatoshis,
Option<MemoBytes>,
)>,
sapling_output_meta: Vec<(
ShieldedBuildRecipient<AccountId>,
Zatoshis,
Option<MemoBytes>,
)>,
transparent_output_meta: Vec<(
TransparentBuildRecipient<AccountId>,
TransparentAddress,
Zatoshis,
StepOutputIndex,
)>,
#[cfg(feature = "transparent-inputs")]
utxos_spent: Vec<OutPoint>,
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn build_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
wallet_db: &mut DbT,
params: &ParamsT,
ufvk: &UnifiedFullViewingKey,
account_id: <DbT as WalletRead>::AccountId,
ovk_policy: OvkPolicy,
min_target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
prior_step_results: &[(&Step<N>, StepResult<<DbT as WalletRead>::AccountId>)],
proposal_step: &Step<N>,
#[cfg(feature = "transparent-inputs")] unused_transparent_outputs: &mut HashMap<
StepOutput,
(TransparentAddress, OutPoint),
>,
proposed_version: Option<TxVersion>,
orchard_pool_padding: BundlePadding,
expiry_height: Option<BlockHeight>,
) -> Result<
BuildState<ParamsT, <DbT as WalletRead>::AccountId>,
CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
>
where
DbT: WalletWrite + WalletCommitmentTrees,
ParamsT: consensus::Parameters + Clone,
FeeRuleT: FeeRule,
{
#[cfg(feature = "transparent-inputs")]
let step_index = prior_step_results.len();
#[allow(clippy::never_loop)]
for input_ref in proposal_step.prior_step_inputs() {
let (prior_step, _) = prior_step_results
.get(input_ref.step_index())
.ok_or(ProposalError::ReferenceError(*input_ref))?;
#[allow(unused_variables)]
let output_pool = match input_ref.output_index() {
StepOutputIndex::Payment(i) => prior_step.payment_pools().get(&i).cloned(),
StepOutputIndex::Change(i) => match prior_step.balance().proposed_change().get(i) {
Some(change) if !change.is_ephemeral() => {
return Err(ProposalError::SpendsChange(*input_ref).into());
}
other => other.map(|change| change.output_pool()),
},
}
.ok_or(ProposalError::ReferenceError(*input_ref))?;
#[cfg(feature = "transparent-inputs")]
if output_pool != PoolType::TRANSPARENT {
return Err(Error::ProposalNotSupported);
}
#[cfg(not(feature = "transparent-inputs"))]
return Err(Error::ProposalNotSupported);
}
let anchor_height = proposal_step
.anchor_height()
.unwrap_or_else(|| confirmations_policy.anchor_height(min_target_height));
let (sapling_anchor, sapling_inputs) = if proposal_step
.involves(PoolType::Shielded(ShieldedPool::Sapling))
{
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|sapling_tree| {
let anchor = sapling_tree
.root_at_checkpoint_id(&anchor_height)?
.ok_or(ProposalError::AnchorNotFound(anchor_height))?
.into();
let sapling_inputs = proposal_step
.shielded_inputs()
.map(|inputs| {
inputs
.notes()
.iter()
.filter_map(|selected| match selected.note() {
Note::Sapling(note) => sapling_tree
.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&anchor_height,
)
.and_then(|witness| {
witness
.ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
})
.map(|merkle_path| {
Some((selected.spending_key_scope(), note, merkle_path))
})
.map_err(Error::from)
.transpose(),
#[cfg(feature = "orchard")]
Note::Orchard { .. } => None,
})
.collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()
})
.transpose()?
.unwrap_or_default();
Ok((Some(anchor), sapling_inputs))
})?
} else {
(None, vec![])
};
#[cfg(feature = "orchard")]
let (orchard_anchor, orchard_inputs) = if proposal_step
.involves(PoolType::Shielded(ShieldedPool::Orchard))
{
wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|orchard_tree| {
let anchor = orchard_tree
.root_at_checkpoint_id(&anchor_height)?
.ok_or(ProposalError::AnchorNotFound(anchor_height))?
.into();
let orchard_inputs = proposal_step
.shielded_inputs()
.map(|inputs| {
inputs
.notes()
.iter()
.filter_map(|selected| match selected.note() {
Note::Orchard {
note,
pool: orchard::ValuePool::Orchard,
} => orchard_tree
.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&anchor_height,
)
.and_then(|witness| {
witness
.ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
})
.map(|merkle_path| Some((note, merkle_path.into())))
.map_err(Error::from)
.transpose(),
_ => None,
})
.collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()
})
.transpose()?
.unwrap_or_default();
Ok((Some(anchor), orchard_inputs))
})?
} else {
(None, vec![])
};
#[cfg(not(feature = "orchard"))]
let orchard_anchor = None;
#[cfg(feature = "orchard")]
let (ironwood_anchor, ironwood_inputs) =
if proposal_step.involves(PoolType::Shielded(ShieldedPool::Ironwood)) {
wallet_db
.with_ironwood_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|ironwood_tree| {
let anchor = ironwood_tree
.root_at_checkpoint_id(&anchor_height)?
.ok_or(ProposalError::AnchorNotFound(anchor_height))?
.into();
let ironwood_inputs = proposal_step
.shielded_inputs()
.map(|inputs| {
inputs
.notes()
.iter()
.filter_map(|selected| match selected.note() {
Note::Orchard {
note,
pool: orchard::ValuePool::Ironwood,
} => ironwood_tree
.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&anchor_height,
)
.and_then(|witness| {
witness.ok_or(ShardTreeError::Query(
QueryError::CheckpointPruned,
))
})
.map(|merkle_path| Some((note, merkle_path.into())))
.map_err(Error::from)
.transpose(),
_ => None,
})
.collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()
})
.transpose()?
.unwrap_or_default();
Ok((Some(anchor), ironwood_inputs))
})?
.ok_or(Error::ProposalNotSupported)?
} else {
(None, vec![])
};
#[cfg(not(feature = "orchard"))]
let ironwood_anchor = None;
#[cfg(feature = "orchard")]
let ironwood_padding = proposal_step.ironwood_bundle_padding();
#[cfg(not(feature = "orchard"))]
let ironwood_padding = orchard_pool_padding;
let mut builder = Builder::new(
params.clone(),
BlockHeight::from(min_target_height),
BuildConfig::Standard {
sapling_anchor,
orchard_anchor,
ironwood_anchor,
orchard_padding: orchard_pool_padding,
ironwood_padding,
},
);
#[cfg(feature = "orchard")]
let expiry_height = {
if step_is_canonical_crossing(wallet_db, params, proposal_step, min_target_height) {
match expiry_height {
Some(requested) => {
return Err(Error::ExpiryHeightConflictsWithCanonicalCrossing { requested });
}
None => Some(zcash_protocol::zip318::expiry_height(
min_target_height.into(),
)),
}
} else {
expiry_height
}
};
if let Some(expiry_height) = expiry_height {
builder = builder.with_expiry_height(expiry_height);
}
if let Some(version) = proposed_version {
builder.propose_version(version)?;
}
#[cfg(all(feature = "transparent-inputs", not(feature = "orchard")))]
let has_shielded_inputs = !sapling_inputs.is_empty();
#[cfg(all(feature = "transparent-inputs", feature = "orchard"))]
let has_shielded_inputs =
!(sapling_inputs.is_empty() && orchard_inputs.is_empty() && ironwood_inputs.is_empty());
let input_sources = NonEmpty::from_vec({
let mut sources = vec![];
if !sapling_inputs.is_empty() {
sources.push(PoolType::SAPLING);
}
#[cfg(feature = "orchard")]
if !orchard_inputs.is_empty() || !ironwood_inputs.is_empty() {
sources.push(PoolType::ORCHARD);
}
#[cfg(feature = "transparent-inputs")]
if !(proposal_step.transparent_inputs().is_empty()
&& proposal_step.prior_step_inputs().is_empty())
{
sources.push(PoolType::Transparent);
}
sources
})
.ok_or(Error::ProposalNotSupported)?;
for (_sapling_key_scope, sapling_note, merkle_path) in sapling_inputs.into_iter() {
let key = match _sapling_key_scope {
Scope::External => ufvk.sapling().map(|k| k.fvk().clone()),
Scope::Internal => ufvk.sapling().map(|k| k.to_internal_fvk()),
};
builder.add_sapling_spend(
key.ok_or(Error::KeyNotAvailable(PoolType::SAPLING))?,
sapling_note.clone(),
merkle_path,
)?;
}
#[cfg(feature = "orchard")]
for (orchard_note, merkle_path) in orchard_inputs.into_iter() {
builder.add_orchard_spend(
ufvk.orchard()
.cloned()
.ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?,
*orchard_note,
merkle_path,
)?;
}
#[cfg(feature = "orchard")]
for (ironwood_note, merkle_path) in ironwood_inputs.into_iter() {
builder.add_ironwood_spend(
ufvk.orchard()
.cloned()
.ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?,
*ironwood_note,
merkle_path,
)?;
}
#[cfg(feature = "transparent-inputs")]
let mut transparent_input_addresses =
HashMap::<TransparentAddress, TransparentAddressMetadata>::new();
#[cfg(feature = "transparent-inputs")]
let mut metadata_from_address = |addr: &TransparentAddress| -> Result<
TransparentAddressMetadata,
CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
> {
match transparent_input_addresses.get(addr) {
Some(result) => Ok(result.clone()),
None => {
let result = wallet_db
.get_transparent_address_metadata(account_id, addr)
.map_err(InputSelectorError::DataSource)?
.ok_or(Error::AddressNotRecognized(*addr))?;
transparent_input_addresses.insert(*addr, result.clone());
Ok(result)
}
}
};
#[cfg(feature = "transparent-inputs")]
let utxos_spent = {
let mut utxos_spent: Vec<OutPoint> = vec![];
let mut add_transparent_input =
|builder: &mut Builder<_, _>,
utxos_spent: &mut Vec<_>,
recipient_address: &TransparentAddress,
outpoint: OutPoint,
txout: TxOut|
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
let metadata = metadata_from_address(recipient_address)?;
match metadata.source() {
TransparentAddressSource::Derived {
scope,
address_index,
} => {
let pubkey = ufvk
.transparent()
.ok_or(Error::KeyNotAvailable(PoolType::Transparent))?
.derive_address_pubkey(*scope, *address_index)
.expect("spending key derivation should not fail");
utxos_spent.push(outpoint.clone());
builder.add_transparent_p2pkh_input(pubkey, outpoint, txout)?;
}
#[cfg(feature = "transparent-key-import")]
TransparentAddressSource::StandalonePubkey(pubkey) => {
utxos_spent.push(outpoint.clone());
builder.add_transparent_p2pkh_input(*pubkey, outpoint, txout)?;
}
#[cfg(feature = "transparent-key-import")]
TransparentAddressSource::StandaloneScript(redeem_script) => {
let from_chain =
zs_script::FromChain::parse(&zs_script::Code(redeem_script.to_bytes()))
.map_err(|_| ::transparent::builder::Error::UnsupportedScript)?;
utxos_spent.push(outpoint.clone());
builder.add_transparent_p2sh_input(from_chain, outpoint, txout)?;
}
}
Ok(())
};
for utxo in proposal_step.transparent_inputs() {
add_transparent_input(
&mut builder,
&mut utxos_spent,
utxo.recipient_address(),
utxo.outpoint().clone(),
utxo.txout().clone(),
)?;
}
for input_ref in proposal_step.prior_step_inputs() {
let (address, outpoint) = unused_transparent_outputs
.remove(input_ref)
.ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))?;
let txout = &prior_step_results[input_ref.step_index()]
.1
.build_result
.transaction()
.transparent_bundle()
.ok_or(ProposalError::ReferenceError(*input_ref))?
.vout[outpoint.n() as usize];
add_transparent_input(
&mut builder,
&mut utxos_spent,
&address,
outpoint,
txout.clone(),
)?;
}
utxos_spent
};
let (external_ovk, internal_ovk) = match ovk_policy {
OvkPolicy::Sender => (
Some(
ufvk.select_ovk(zip32::Scope::External, &input_sources)
.ok_or(Error::KeyNotAvailable(input_sources.head))?,
),
None,
),
OvkPolicy::Custom {
external_ovk,
internal_ovk,
} => (Some(external_ovk), internal_ovk),
OvkPolicy::Discard => (None, None),
};
#[cfg(feature = "orchard")]
let mut orchard_output_meta: Vec<(ShieldedBuildRecipient<_>, Zatoshis, Option<MemoBytes>)> =
vec![];
#[cfg(feature = "orchard")]
let mut ironwood_output_meta: Vec<(
ShieldedBuildRecipient<_>,
Zatoshis,
Option<MemoBytes>,
)> = vec![];
let mut sapling_output_meta: Vec<(ShieldedBuildRecipient<_>, Zatoshis, Option<MemoBytes>)> =
vec![];
let mut transparent_output_meta: Vec<(
TransparentBuildRecipient<_>,
TransparentAddress,
Zatoshis,
StepOutputIndex,
)> = vec![];
for (&payment_index, output_pool) in proposal_step.payment_pools() {
let payment = proposal_step
.transaction_request()
.payments()
.get(&payment_index)
.expect(
"The mapping between payment index and payment is checked in step construction",
);
let payment_amount = payment
.amount()
.ok_or(ProposalError::PaymentAmountMissing(payment_index))?;
let recipient_address = payment.recipient_address();
let add_sapling_output =
|builder: &mut Builder<_, _>,
sapling_output_meta: &mut Vec<_>,
to: sapling::PaymentAddress|
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_sapling_output(
external_ovk.map(|k| k.into()),
to,
payment_amount,
memo.clone(),
)?;
sapling_output_meta.push((
ShieldedBuildRecipient::External {
recipient_address: recipient_address.clone(),
output_pool: PoolType::SAPLING,
},
payment_amount,
Some(memo),
));
Ok(())
};
#[cfg(feature = "orchard")]
let add_orchard_output =
|builder: &mut Builder<_, _>,
orchard_output_meta: &mut Vec<_>,
to: orchard::Address|
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_orchard_output(
external_ovk.map(|k| k.into()),
to,
payment_amount,
memo.clone(),
)?;
orchard_output_meta.push((
ShieldedBuildRecipient::External {
recipient_address: recipient_address.clone(),
output_pool: PoolType::ORCHARD,
},
payment_amount,
Some(memo),
));
Ok(())
};
#[cfg(feature = "orchard")]
let add_ironwood_output =
|builder: &mut Builder<_, _>,
ironwood_output_meta: &mut Vec<_>,
to: orchard::Address|
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_ironwood_output(
external_ovk.map(|k| k.into()),
to,
payment_amount,
memo.clone(),
)?;
ironwood_output_meta.push((
ShieldedBuildRecipient::External {
recipient_address: recipient_address.clone(),
output_pool: PoolType::IRONWOOD,
},
payment_amount,
Some(memo),
));
Ok(())
};
let add_transparent_output =
|builder: &mut Builder<_, _>,
transparent_output_meta: &mut Vec<_>,
to: TransparentAddress|
-> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
if payment.memo().is_some() {
return Err(Error::Payment(zip321::PaymentError::TransparentMemo));
}
builder.add_transparent_output(&to, payment_amount)?;
transparent_output_meta.push((
TransparentBuildRecipient::External {
recipient_address: recipient_address.clone(),
output_pool: PoolType::TRANSPARENT,
},
to,
payment_amount,
StepOutputIndex::Payment(payment_index),
));
Ok(())
};
match recipient_address
.clone()
.convert_if_network(params.network_type())?
{
Address::Unified(ua) => match output_pool {
#[cfg(not(feature = "orchard"))]
PoolType::Shielded(ShieldedPool::Orchard) => {
return Err(Error::ProposalNotSupported);
}
#[cfg(feature = "orchard")]
PoolType::Shielded(ShieldedPool::Orchard) => {
let to = *ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction");
add_orchard_output(&mut builder, &mut orchard_output_meta, to)?;
}
PoolType::Shielded(ShieldedPool::Sapling) => {
let to = *ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction");
add_sapling_output(&mut builder, &mut sapling_output_meta, to)?;
}
PoolType::Transparent => {
let to = *ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction");
add_transparent_output(&mut builder, &mut transparent_output_meta, to)?;
}
#[cfg(feature = "orchard")]
PoolType::Shielded(ShieldedPool::Ironwood) => {
let to = *ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction");
add_ironwood_output(&mut builder, &mut ironwood_output_meta, to)?;
}
#[cfg(not(feature = "orchard"))]
PoolType::Shielded(ShieldedPool::Ironwood) => {
return Err(Error::ProposalNotSupported);
}
},
Address::Sapling(to) => {
add_sapling_output(&mut builder, &mut sapling_output_meta, to)?;
}
Address::Transparent(to) => {
add_transparent_output(&mut builder, &mut transparent_output_meta, to)?;
}
#[cfg(not(feature = "transparent-inputs"))]
Address::Tex(_) => {
return Err(Error::ProposalNotSupported);
}
#[cfg(feature = "transparent-inputs")]
Address::Tex(data) => {
if has_shielded_inputs {
return Err(ProposalError::PaysTexFromShielded.into());
}
let to = TransparentAddress::PublicKeyHash(data);
add_transparent_output(&mut builder, &mut transparent_output_meta, to)?;
}
}
}
for change_value in proposal_step.balance().proposed_change() {
let memo = change_value
.memo()
.map_or_else(MemoBytes::empty, |m| m.clone());
let output_pool = change_value.output_pool();
match output_pool {
PoolType::Shielded(ShieldedPool::Sapling) => {
builder.add_sapling_output(
internal_ovk.map(|k| k.into()),
ufvk.sapling()
.ok_or(Error::KeyNotAvailable(PoolType::SAPLING))?
.change_address()
.1,
change_value.value(),
memo.clone(),
)?;
sapling_output_meta.push((
ShieldedBuildRecipient::InternalShielded {
receiving_account: account_id,
external_address: None,
},
change_value.value(),
Some(memo),
))
}
PoolType::Shielded(ShieldedPool::Orchard) => {
#[cfg(not(feature = "orchard"))]
return Err(Error::UnsupportedChangeType(output_pool));
#[cfg(feature = "orchard")]
{
let orchard_fvk = ufvk
.orchard()
.ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?;
let change_address =
orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal);
if ironwood_active_at(params, min_target_height) {
builder.add_orchard_change_output(
orchard_fvk.clone(),
internal_ovk.map(|k| k.into()),
change_address,
change_value.value(),
memo.clone(),
)?;
orchard_output_meta.push((
ShieldedBuildRecipient::InternalShielded {
receiving_account: account_id,
external_address: None,
},
change_value.value(),
Some(memo),
))
} else {
builder.add_orchard_output(
internal_ovk.map(|k| k.into()),
change_address,
change_value.value(),
memo.clone(),
)?;
orchard_output_meta.push((
ShieldedBuildRecipient::InternalShielded {
receiving_account: account_id,
external_address: None,
},
change_value.value(),
Some(memo),
))
}
}
}
PoolType::Transparent => {
#[cfg(not(feature = "transparent-inputs"))]
return Err(Error::UnsupportedChangeType(output_pool));
}
PoolType::Shielded(ShieldedPool::Ironwood) => {
#[cfg(not(feature = "orchard"))]
return Err(Error::UnsupportedChangeType(output_pool));
#[cfg(feature = "orchard")]
{
let orchard_fvk = ufvk
.orchard()
.ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?;
let change_address =
orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal);
builder.add_ironwood_output(
internal_ovk.map(|k| k.into()),
change_address,
change_value.value(),
memo.clone(),
)?;
ironwood_output_meta.push((
ShieldedBuildRecipient::InternalShielded {
receiving_account: account_id,
external_address: None,
},
change_value.value(),
Some(memo),
))
}
}
}
}
#[cfg(feature = "transparent-inputs")]
{
let ephemeral_outputs: Vec<(usize, &ChangeValue)> = proposal_step
.balance()
.proposed_change()
.iter()
.enumerate()
.filter(|(_, change_value)| {
change_value.is_ephemeral() && change_value.output_pool() == PoolType::Transparent
})
.collect();
let addresses_and_metadata = wallet_db
.reserve_next_n_ephemeral_addresses(account_id, ephemeral_outputs.len())
.map_err(Error::DataSource)?;
assert_eq!(addresses_and_metadata.len(), ephemeral_outputs.len());
for ((change_index, change_value), (ephemeral_address, _)) in
ephemeral_outputs.iter().zip(addresses_and_metadata)
{
builder.add_transparent_output(&ephemeral_address, change_value.value())?;
transparent_output_meta.push((
TransparentBuildRecipient::EphemeralTransparent {
receiving_account: account_id,
ephemeral_address,
},
ephemeral_address,
change_value.value(),
StepOutputIndex::Change(*change_index),
))
}
}
#[cfg(feature = "transparent-inputs")]
{
let transparent_change_outputs: Vec<(usize, &ChangeValue)> = proposal_step
.balance()
.proposed_change()
.iter()
.enumerate()
.filter(|(_, change_value)| {
!change_value.is_ephemeral() && change_value.output_pool() == PoolType::Transparent
})
.collect();
if !transparent_change_outputs.is_empty() {
let addresses_and_metadata = wallet_db
.reserve_next_n_internal_addresses(account_id, transparent_change_outputs.len())
.map_err(Error::DataSource)?;
assert_eq!(
addresses_and_metadata.len(),
transparent_change_outputs.len()
);
for ((change_index, change_value), (change_address, _)) in transparent_change_outputs
.iter()
.zip(addresses_and_metadata)
{
builder.add_transparent_output(&change_address, change_value.value())?;
transparent_output_meta.push((
TransparentBuildRecipient::InternalTransparent {
receiving_account: account_id,
recipient_address: change_address,
},
change_address,
change_value.value(),
StepOutputIndex::Change(*change_index),
))
}
}
}
Ok(BuildState {
#[cfg(feature = "transparent-inputs")]
step_index,
builder,
#[cfg(feature = "transparent-inputs")]
transparent_input_addresses,
#[cfg(feature = "orchard")]
orchard_output_meta,
#[cfg(feature = "orchard")]
ironwood_output_meta,
sapling_output_meta,
transparent_output_meta,
#[cfg(feature = "transparent-inputs")]
utxos_spent,
})
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
spending_keys: &SpendingKeys,
account_id: <DbT as WalletRead>::AccountId,
ovk_policy: OvkPolicy,
fee_rule: &FeeRuleT,
min_target_height: TargetHeight,
confirmations_policy: ConfirmationsPolicy,
prior_step_results: &[(&Step<N>, StepResult<<DbT as WalletRead>::AccountId>)],
proposal_step: &Step<N>,
#[cfg(feature = "transparent-inputs")] unused_transparent_outputs: &mut HashMap<
StepOutput,
(TransparentAddress, OutPoint),
>,
proposed_version: Option<TxVersion>,
expiry_height: Option<BlockHeight>,
) -> Result<
StepResult<<DbT as WalletRead>::AccountId>,
CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
>
where
DbT: WalletWrite + WalletCommitmentTrees,
ParamsT: consensus::Parameters + Clone,
FeeRuleT: FeeRule,
{
let build_state = build_proposed_transaction::<_, _, _, FeeRuleT, _, _>(
wallet_db,
params,
&spending_keys.usk.to_unified_full_viewing_key(),
account_id,
ovk_policy,
min_target_height,
confirmations_policy,
prior_step_results,
proposal_step,
#[cfg(feature = "transparent-inputs")]
unused_transparent_outputs,
proposed_version,
BundlePadding::DEFAULT,
expiry_height,
)?;
#[cfg_attr(not(feature = "transparent-inputs"), allow(unused_mut))]
let mut transparent_signing_set = TransparentSigningSet::new();
#[cfg(feature = "transparent-inputs")]
for (_address, address_metadata) in build_state.transparent_input_addresses {
match address_metadata.source() {
TransparentAddressSource::Derived {
scope,
address_index,
} => {
transparent_signing_set.add_key(
spending_keys
.usk
.transparent()
.derive_secret_key(*scope, *address_index)
.expect("spending key derivation should not fail"),
);
}
#[cfg(feature = "transparent-key-import")]
TransparentAddressSource::StandalonePubkey(_)
| TransparentAddressSource::StandaloneScript(_) => {
let keys = spending_keys
.standalone_transparent_keys
.get(&_address)
.ok_or(Error::AddressNotRecognized(_address))?;
for key in keys {
transparent_signing_set.add_key(*key);
}
}
}
}
let sapling_extsks = &[
spending_keys.usk.sapling().clone(),
spending_keys.usk.sapling().derive_internal(),
];
#[cfg(feature = "orchard")]
let orchard_saks = &[spending_keys.usk.orchard().into()];
#[cfg(not(feature = "orchard"))]
let orchard_saks = &[];
let build_result = build_state.builder.build(
&transparent_signing_set,
sapling_extsks,
orchard_saks,
UnwrapErr(SysRng),
spend_prover,
output_prover,
fee_rule,
)?;
#[cfg(feature = "orchard")]
let orchard_fvk: orchard::keys::FullViewingKey = spending_keys.usk.orchard().into();
#[cfg(feature = "orchard")]
let orchard_internal_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::Internal);
#[cfg(feature = "orchard")]
let orchard_external_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::External);
#[cfg(feature = "orchard")]
let orchard_outputs = build_state.orchard_output_meta.into_iter().enumerate().map(
|(i, (recipient, value, memo))| {
let output_index = build_result
.orchard_meta()
.output_action_index(i)
.expect("An action should exist in the transaction for each Orchard output.");
let recipient = recipient.into_recipient(|| {
build_result
.transaction()
.orchard_bundle()
.and_then(|bundle| {
bundle
.decrypt_output_with_key(output_index, &orchard_internal_ivk)
.or_else(|| {
bundle.decrypt_output_with_key(output_index, &orchard_external_ivk)
})
.map(|(note, _, _)| Note::Orchard {
note,
pool: orchard::ValuePool::Orchard,
})
})
.expect("Wallet-internal outputs must be decryptable with the wallet's IVK")
});
SentTransactionOutput::from_parts_in_tree(
Some(NoteCommitmentTree::Orchard),
output_index,
recipient,
value,
memo,
)
},
);
#[cfg(feature = "orchard")]
let ironwood_outputs = build_state
.ironwood_output_meta
.into_iter()
.enumerate()
.map(|(i, (recipient, value, memo))| {
let raw_output_index = build_result
.ironwood_meta()
.output_action_index(i)
.expect("An action should exist in the transaction for each Ironwood output.");
let recipient = recipient.into_recipient(|| {
build_result
.transaction()
.ironwood_bundle()
.and_then(|bundle| {
bundle
.decrypt_output_with_key(raw_output_index, &orchard_internal_ivk)
.map(|(note, _, _)| Note::Orchard { note, pool: orchard::ValuePool::Ironwood })
})
.expect(
"Wallet-internal Ironwood outputs must be decryptable with the wallet's IVK",
)
});
SentTransactionOutput::from_parts_in_tree(
Some(NoteCommitmentTree::Ironwood),
raw_output_index,
recipient,
value,
memo,
)
});
let sapling_dfvk = spending_keys
.usk
.sapling()
.to_diversifiable_full_viewing_key();
let sapling_internal_ivk =
PreparedIncomingViewingKey::new(&sapling_dfvk.to_ivk(Scope::Internal));
let sapling_outputs = build_state.sapling_output_meta.into_iter().enumerate().map(
|(i, (recipient, value, memo))| {
let output_index = build_result
.sapling_meta()
.output_index(i)
.expect("An output should exist in the transaction for each Sapling payment.");
let recipient = recipient.into_recipient(|| {
build_result
.transaction()
.sapling_bundle()
.and_then(|bundle| {
try_sapling_note_decryption(
&sapling_internal_ivk,
&bundle.shielded_outputs()[output_index],
zip212_enforcement(params, min_target_height.into()),
)
.map(|(note, _, _)| Note::Sapling(note))
})
.expect("Wallet-internal outputs must be decryptable with the wallet's IVK")
});
SentTransactionOutput::from_parts_in_tree(
Some(NoteCommitmentTree::Sapling),
output_index,
recipient,
value,
memo,
)
},
);
let txid: [u8; 32] = build_result.transaction().txid().into();
assert_eq!(
build_state.transparent_output_meta.len(),
build_result
.transaction()
.transparent_bundle()
.map_or(0, |b| b.vout.len()),
);
#[allow(unused_variables)]
let transparent_outputs = build_state
.transparent_output_meta
.into_iter()
.enumerate()
.map(|(n, (recipient, address, value, step_output_index))| {
let outpoint = OutPoint::new(txid, n as u32);
let recipient = recipient.into_recipient(
#[cfg(feature = "transparent-inputs")]
outpoint.clone(),
);
#[cfg(feature = "transparent-inputs")]
unused_transparent_outputs.insert(
StepOutput::new(build_state.step_index, step_output_index),
(address, outpoint),
);
SentTransactionOutput::from_parts(n, recipient, value, None)
});
let mut outputs: Vec<SentTransactionOutput<_>> = vec![];
#[cfg(feature = "orchard")]
outputs.extend(orchard_outputs);
#[cfg(feature = "orchard")]
outputs.extend(ironwood_outputs);
outputs.extend(sapling_outputs);
outputs.extend(transparent_outputs);
Ok(StepResult {
build_result,
outputs,
fee_amount: proposal_step.balance().fee_required(),
#[cfg(feature = "transparent-inputs")]
utxos_spent: build_state.utxos_spent,
})
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
#[cfg(feature = "pczt")]
pub fn create_pczt_from_proposal<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
wallet_db: &mut DbT,
params: &ParamsT,
account_id: <DbT as WalletRead>::AccountId,
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, N>,
expiry_height: Option<BlockHeight>,
orchard_pool_padding: BundlePadding,
) -> Result<pczt::Pczt, CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
where
DbT: WalletWrite + WalletCommitmentTrees,
ParamsT: consensus::Parameters + Clone,
FeeRuleT: FeeRule,
<DbT as WalletRead>::AccountId: serde::Serialize,
{
let account = wallet_db
.get_account(account_id)
.map_err(Error::DataSource)?
.ok_or(Error::AccountIdNotRecognized)?;
let ufvk = account.ufvk().ok_or(Error::AccountCannotSpend)?;
let account_derivation = account.source().key_derivation();
if proposal.steps().len() > 1 {
return Err(Error::ProposalNotSupported);
}
let fee_rule = proposal.fee_rule();
let min_target_height = proposal.min_target_height();
if let Some(expiry_height) = expiry_height {
let min_target_height = BlockHeight::from(min_target_height);
if expiry_height != consensus::H0 && expiry_height < min_target_height {
return Err(Error::ExpiryHeightBelowTargetHeight {
expiry_height,
min_target_height,
});
}
}
let prior_step_results = &[];
let proposal_step = proposal.steps().first();
let unused_transparent_outputs = &mut HashMap::new();
let proposed_version = proposal.proposed_version();
let build_state = build_proposed_transaction::<_, _, _, FeeRuleT, _, _>(
wallet_db,
params,
ufvk,
account_id,
ovk_policy,
min_target_height,
proposal.confirmations_policy(),
prior_step_results,
proposal_step,
#[cfg(feature = "transparent-inputs")]
unused_transparent_outputs,
proposed_version,
orchard_pool_padding,
None,
)?;
#[cfg(feature = "orchard")]
if let Some(requested) = expiry_height
&& step_is_canonical_crossing(wallet_db, params, proposal_step, min_target_height)
{
return Err(Error::ExpiryHeightConflictsWithCanonicalCrossing { requested });
}
let mut build_result = build_state
.builder
.build_for_pczt(UnwrapErr(SysRng), fee_rule)?;
if let Some(expiry_height) = expiry_height {
build_result.pczt_parts.expiry_height = expiry_height;
}
let created = Creator::build_from_parts(build_result.pczt_parts).ok_or(PcztError::Build)?;
let io_finalized = IoFinalizer::new(created).finalize_io()?;
#[cfg(feature = "orchard")]
let orchard_outputs = build_state
.orchard_output_meta
.into_iter()
.enumerate()
.map(|(i, (recipient, _, _))| {
let output_index = build_result
.orchard_meta
.output_action_index(i)
.expect("An action should exist in the transaction for each Orchard output.");
(
output_index,
PcztRecipient::from_shielded_recipient(recipient),
)
})
.collect::<HashMap<_, _>>();
#[cfg(feature = "orchard")]
let ironwood_outputs = build_state
.ironwood_output_meta
.into_iter()
.enumerate()
.map(|(i, (recipient, _, _))| {
let output_index = build_result
.ironwood_meta
.output_action_index(i)
.expect("An action should exist in the transaction for each Ironwood output.");
(
output_index,
PcztRecipient::from_shielded_recipient(recipient),
)
})
.collect::<HashMap<_, _>>();
let sapling_outputs = build_state
.sapling_output_meta
.into_iter()
.enumerate()
.map(|(i, (recipient, _, _))| {
let output_index = build_result
.sapling_meta
.output_index(i)
.expect("An output should exist in the transaction for each Sapling output.");
(
output_index,
PcztRecipient::from_shielded_recipient(recipient),
)
})
.collect::<HashMap<_, _>>();
let pczt = Updater::new(io_finalized)
.update_global_with(|mut updater| {
updater.set_proprietary(
PROPRIETARY_PROPOSAL_INFO.into(),
postcard::to_allocvec(&ProposalInfo::<<DbT as WalletRead>::AccountId> {
from_account: account_id,
target_height: proposal.min_target_height(),
})
.expect("postcard encoding of PCZT proposal metadata should not fail"),
)
})
.update_orchard_with(|mut updater| {
let spend_needs_derivation = updater
.bundle()
.actions()
.iter()
.map(|action| action.spend().spend_auth_sig().is_none())
.collect::<Vec<_>>();
for (index, needs_derivation) in spend_needs_derivation.iter().enumerate() {
updater.update_action_with(index, |mut action_updater| {
if let Some(derivation) = account_derivation
&& *needs_derivation
{
action_updater.set_spend_zip32_derivation(
orchard::pczt::Zip32Derivation::parse(
derivation.seed_fingerprint().to_bytes(),
vec![
zip32::ChildIndex::hardened(32).index(),
zip32::ChildIndex::hardened(params.network_type().coin_type())
.index(),
zip32::ChildIndex::hardened(u32::from(
derivation.account_index(),
))
.index(),
],
)
.expect("valid"),
);
}
if let Some((pczt_recipient, external_address)) = orchard_outputs.get(&index) {
if let Some(user_address) = external_address {
action_updater.set_output_user_address(user_address.encode());
}
action_updater.set_output_proprietary(
PROPRIETARY_OUTPUT_INFO.into(),
postcard::to_allocvec(pczt_recipient).expect(
"postcard encoding of PCZT recipient metadata should not fail",
),
);
}
Ok(())
})?;
}
Ok(())
})?
.update_ironwood_with(|mut updater| {
let spend_needs_derivation = updater
.bundle()
.actions()
.iter()
.map(|action| action.spend().spend_auth_sig().is_none())
.collect::<Vec<_>>();
for (index, needs_derivation) in spend_needs_derivation.iter().enumerate() {
updater.update_action_with(index, |mut action_updater| {
if let Some(derivation) = account_derivation
&& *needs_derivation
{
action_updater.set_spend_zip32_derivation(
orchard::pczt::Zip32Derivation::parse(
derivation.seed_fingerprint().to_bytes(),
vec![
zip32::ChildIndex::hardened(32).index(),
zip32::ChildIndex::hardened(params.network_type().coin_type())
.index(),
zip32::ChildIndex::hardened(u32::from(
derivation.account_index(),
))
.index(),
],
)
.expect("valid"),
);
}
if let Some((pczt_recipient, external_address)) = ironwood_outputs.get(&index) {
if let Some(user_address) = external_address {
action_updater.set_output_user_address(user_address.encode());
}
action_updater.set_output_proprietary(
PROPRIETARY_OUTPUT_INFO.into(),
postcard::to_allocvec(pczt_recipient).expect(
"postcard encoding of PCZT recipient metadata should not fail",
),
);
}
Ok(())
})?;
}
Ok(())
})?
.update_sapling_with(|mut updater| {
if let Some(derivation) = account_derivation {
let non_dummy_spends = updater
.bundle()
.spends()
.iter()
.enumerate()
.filter_map(|(index, spend)| {
spend.proof_generation_key().is_none().then_some(index)
})
.collect::<Vec<_>>();
for index in non_dummy_spends {
updater.update_spend_with(index, |mut spend_updater| {
spend_updater.set_zip32_derivation(
sapling::pczt::Zip32Derivation::parse(
derivation.seed_fingerprint().to_bytes(),
vec![
zip32::ChildIndex::hardened(32).index(),
zip32::ChildIndex::hardened(params.network_type().coin_type())
.index(),
zip32::ChildIndex::hardened(u32::from(
derivation.account_index(),
))
.index(),
],
)
.expect("valid"),
);
Ok(())
})?;
}
}
for index in 0..updater.bundle().outputs().len() {
if let Some((pczt_recipient, external_address)) = sapling_outputs.get(&index) {
updater.update_output_with(index, |mut output_updater| {
if let Some(user_address) = external_address {
output_updater.set_user_address(user_address.encode());
}
output_updater.set_proprietary(
PROPRIETARY_OUTPUT_INFO.into(),
postcard::to_allocvec(pczt_recipient).expect(
"postcard encoding of PCZT recipient metadata should not fail",
),
);
Ok(())
})?;
}
}
Ok(())
})?
.update_transparent_with(|mut updater| {
if let Some(derivation) = account_derivation {
let inputs_to_update = updater
.bundle()
.inputs()
.iter()
.enumerate()
.filter_map(|(index, input)| {
let address_metadata = build_state.transparent_input_addresses.get(
&TransparentAddress::from_script_from_chain(input.script_pubkey())
.expect("we created this with a supported transparent address"),
)?;
match address_metadata.source() {
TransparentAddressSource::Derived {
scope,
address_index,
} => Some((index, *scope, *address_index)),
#[cfg(feature = "transparent-key-import")]
TransparentAddressSource::StandalonePubkey(_)
| TransparentAddressSource::StandaloneScript(_) => None,
}
})
.collect::<Vec<_>>();
for (index, scope, address_index) in inputs_to_update {
updater.update_input_with(index, |mut input_updater| {
let pubkey = ufvk
.transparent()
.expect("we derived this successfully in build_proposed_transaction")
.derive_address_pubkey(scope, address_index)
.expect("spending key derivation should not fail");
input_updater.set_bip32_derivation(
pubkey.serialize(),
Bip32Derivation::parse(
derivation.seed_fingerprint().to_bytes(),
vec![
44 | ChildNumber::HARDENED_FLAG,
params.network_type().coin_type() | ChildNumber::HARDENED_FLAG,
u32::from(derivation.account_index())
| ChildNumber::HARDENED_FLAG,
ChildNumber::from(scope).into(),
ChildNumber::from(address_index).into(),
],
)
.expect("valid"),
);
Ok(())
})?;
}
}
assert_eq!(
build_state.transparent_output_meta.len(),
updater.bundle().outputs().len(),
);
for (index, (recipient, _, _, _)) in
build_state.transparent_output_meta.into_iter().enumerate()
{
updater.update_output_with(index, |mut output_updater| {
let (pczt_recipient, external_address) =
PcztRecipient::from_transparent_recipient(recipient);
if let Some(user_address) = external_address {
output_updater.set_user_address(user_address.encode());
}
output_updater.set_proprietary(
PROPRIETARY_OUTPUT_INFO.into(),
postcard::to_allocvec(&pczt_recipient)
.expect("postcard encoding of pczt recipient metadata should not fail"),
);
Ok(())
})?;
}
Ok(())
})?
.finish();
Ok(pczt)
}
#[cfg(feature = "pczt")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SignerView {
Full,
Compact,
}
#[cfg(feature = "pczt")]
pub fn redact_pczt_for_signer(pczt: &pczt::Pczt, view: SignerView) -> pczt::Pczt {
match view {
SignerView::Full => full_signer_view(pczt),
SignerView::Compact => compact_signer_view(pczt),
}
}
#[cfg(feature = "pczt")]
fn full_signer_view(pczt: &pczt::Pczt) -> pczt::Pczt {
fn redact_orchard_bundle(mut redactor: pczt::roles::redactor::orchard::OrchardRedactor<'_>) {
redactor.redact_actions(|mut action| {
action.clear_spend_witness();
action.clear_spend_dummy_sk();
action.redact_output_proprietary(PROPRIETARY_OUTPUT_INFO);
});
}
Redactor::new(pczt.clone())
.redact_global_with(|mut redactor| {
redactor.redact_proprietary(PROPRIETARY_PROPOSAL_INFO);
})
.redact_transparent_with(|mut redactor| {
redactor.redact_outputs(|mut output| {
output.redact_proprietary(PROPRIETARY_OUTPUT_INFO);
});
})
.redact_sapling_with(|mut redactor| {
redactor.redact_spends(|mut spend| {
spend.clear_witness();
spend.clear_dummy_ask();
});
redactor.redact_outputs(|mut output| {
output.redact_proprietary(PROPRIETARY_OUTPUT_INFO);
});
})
.redact_orchard_with(redact_orchard_bundle)
.redact_ironwood_with(redact_orchard_bundle)
.finish()
}
#[cfg(feature = "pczt")]
fn compact_signer_view(pczt: &pczt::Pczt) -> pczt::Pczt {
let redact_v6_anchors = *pczt.global().tx_version() == zcash_protocol::constants::V6_TX_VERSION;
fn redact_orchard_bundle(
mut redactor: pczt::roles::redactor::orchard::OrchardRedactor<'_>,
redact_v6_anchor: bool,
) {
redactor.clear_zkproof();
redactor.clear_bsk();
redactor.redact_actions(|mut action| {
action.clear_spend_witness();
action.clear_spend_dummy_sk();
action.redact_output_proprietary(PROPRIETARY_OUTPUT_INFO);
});
redactor.compact_resolvable_fields();
if redact_v6_anchor {
redactor.clear_anchor();
}
}
Redactor::new(pczt.clone())
.redact_global_with(|mut redactor| {
redactor.redact_proprietary(PROPRIETARY_PROPOSAL_INFO);
})
.redact_transparent_with(|mut redactor| {
redactor.redact_outputs(|mut output| {
output.redact_proprietary(PROPRIETARY_OUTPUT_INFO);
});
})
.redact_sapling_with(|mut redactor| {
redactor.clear_bsk();
redactor.redact_spends(|mut spend| {
spend.clear_zkproof();
spend.clear_dummy_ask();
});
redactor.redact_outputs(|mut output| {
output.clear_zkproof();
output.redact_proprietary(PROPRIETARY_OUTPUT_INFO);
});
if redact_v6_anchors {
redactor.clear_anchor();
}
})
.redact_orchard_with(|redactor| {
redact_orchard_bundle(redactor, redact_v6_anchors);
})
.redact_ironwood_with(|redactor| {
redact_orchard_bundle(redactor, redact_v6_anchors);
})
.finish()
}
#[cfg(feature = "pczt")]
pub fn redact_pczt_for_batch_signer(pczt: &pczt::Pczt) -> pczt::Pczt {
fn preauthorized_action_indices(bundle: &pczt::orchard::Bundle) -> Vec<usize> {
bundle
.actions()
.iter()
.enumerate()
.filter_map(|(index, action)| {
action.spend().spend_auth_sig().is_some().then_some(index)
})
.collect()
}
fn redact_orchard_bundle(
mut redactor: pczt::roles::redactor::orchard::OrchardRedactor<'_>,
preauthorized_action_indices: &[usize],
) {
redactor.redact_actions(|mut action| {
action.clear_spend_fvk();
action.clear_spend_auth_sig();
});
for &index in preauthorized_action_indices {
redactor.redact_action(index, |mut action| action.clear_spend_alpha());
}
}
let orchard_preauthorized = preauthorized_action_indices(pczt.orchard());
let ironwood_preauthorized = preauthorized_action_indices(pczt.ironwood());
Redactor::new(redact_pczt_for_signer(pczt, SignerView::Compact))
.redact_orchard_with(|redactor| {
redact_orchard_bundle(redactor, &orchard_preauthorized);
})
.redact_ironwood_with(|redactor| {
redact_orchard_bundle(redactor, &ironwood_preauthorized);
})
.finish()
}
#[cfg(feature = "pczt")]
pub fn extract_and_store_transaction_from_pczt<DbT, N>(
wallet_db: &mut DbT,
pczt: pczt::Pczt,
sapling_vk: Option<(
&sapling::circuit::SpendVerifyingKey,
&sapling::circuit::OutputVerifyingKey,
)>,
#[cfg(feature = "orchard")] orchard_vk: Option<&orchard::circuit::VerifyingKey>,
) -> Result<TxId, ExtractErrT<DbT, N>>
where
DbT: WalletWrite + WalletCommitmentTrees,
<DbT as WalletRead>::AccountId: serde::de::DeserializeOwned,
{
let finalized = SpendFinalizer::new(pczt).finalize_spends()?;
let proposal_info = finalized
.global()
.proprietary()
.get(PROPRIETARY_PROPOSAL_INFO)
.ok_or_else(|| PcztError::Invalid("PCZT missing proprietary proposal info field".into()))
.and_then(|v| {
postcard::from_bytes::<ProposalInfo<<DbT as WalletRead>::AccountId>>(v).map_err(|e| {
PcztError::Invalid(format!(
"Postcard decoding of proprietary proposal info failed: {e}"
))
})
})?;
#[allow(clippy::type_complexity)]
fn orchard_protocol_output_info<AccountId: serde::de::DeserializeOwned>(
actions: &[pczt::orchard::Action],
note_version: orchard::note::NoteVersion,
) -> Result<
Vec<
Option<(
(PcztRecipient<AccountId>, Option<ZcashAddress>),
orchard::Note,
)>,
>,
PcztError,
> {
actions
.iter()
.map(|act| {
let note = || {
let recipient = act.output().recipient().as_ref().and_then(|b| {
::orchard::Address::from_raw_address_bytes(b).into_option()
})?;
let value = act
.output()
.value()
.map(orchard::value::NoteValue::from_raw)?;
let rho =
orchard::note::Rho::from_bytes(act.spend().nullifier()).into_option()?;
let rseed = act.output().rseed().as_ref().and_then(|rseed| {
orchard::note::RandomSeed::from_bytes(*rseed, &rho).into_option()
})?;
orchard::Note::from_parts(recipient, value, rho, rseed, note_version)
.into_option()
};
let external_address = act
.output()
.user_address()
.as_deref()
.map(ZcashAddress::try_from_encoded)
.transpose()
.map_err(|e| PcztError::Invalid(format!("Invalid user_address: {e}")))?;
let pczt_recipient = act
.output()
.proprietary()
.get(PROPRIETARY_OUTPUT_INFO)
.map(|v| postcard::from_bytes::<PcztRecipient<AccountId>>(v))
.transpose()
.map_err(|e: postcard::Error| {
PcztError::Invalid(format!(
"Postcard decoding of proprietary output info failed: {e}"
))
})?
.map(|pczt_recipient| (pczt_recipient, external_address));
Ok(pczt_recipient.zip(note()))
})
.collect::<Result<Vec<_>, PcztError>>()
}
let orchard_output_info = orchard_protocol_output_info::<<DbT as WalletRead>::AccountId>(
finalized.orchard().actions(),
orchard::note::NoteVersion::V2,
)?;
let ironwood_output_info = orchard_protocol_output_info::<<DbT as WalletRead>::AccountId>(
finalized.ironwood().actions(),
orchard::note::NoteVersion::V3,
)?;
let sapling_output_info = finalized
.sapling()
.outputs()
.iter()
.map(|out| {
let note = || {
let recipient = out
.recipient()
.as_ref()
.and_then(::sapling::PaymentAddress::from_bytes)?;
let value = out.value().map(::sapling::value::NoteValue::from_raw)?;
let rseed = out
.rseed()
.as_ref()
.cloned()
.map(::sapling::note::Rseed::AfterZip212)?;
Some(::sapling::Note::from_parts(recipient, value, rseed))
};
let external_address = out
.user_address()
.as_deref()
.map(ZcashAddress::try_from_encoded)
.transpose()
.map_err(|e| PcztError::Invalid(format!("Invalid user_address: {e}")))?;
let pczt_recipient = out
.proprietary()
.get(PROPRIETARY_OUTPUT_INFO)
.map(|v| postcard::from_bytes::<PcztRecipient<<DbT as WalletRead>::AccountId>>(v))
.transpose()
.map_err(|e: postcard::Error| {
PcztError::Invalid(format!(
"Postcard decoding of proprietary output info failed: {e}"
))
})?
.map(|pczt_recipient| (pczt_recipient, external_address));
Ok(pczt_recipient.zip(note()))
})
.collect::<Result<Vec<_>, PcztError>>()?;
let transparent_output_info = finalized
.transparent()
.outputs()
.iter()
.map(|out| {
let external_address = out
.user_address()
.as_deref()
.map(ZcashAddress::try_from_encoded)
.transpose()
.map_err(|e| PcztError::Invalid(format!("Invalid user_address: {e}")))?;
let pczt_recipient = out
.proprietary()
.get(PROPRIETARY_OUTPUT_INFO)
.map(|v| postcard::from_bytes::<PcztRecipient<<DbT as WalletRead>::AccountId>>(v))
.transpose()
.map_err(|e: postcard::Error| {
PcztError::Invalid(format!(
"Postcard decoding of proprietary output info failed: {e}"
))
})?
.map(|pczt_recipient| (pczt_recipient, external_address));
Ok(pczt_recipient)
})
.collect::<Result<Vec<_>, PcztError>>()?;
let utxos_map = finalized
.transparent()
.inputs()
.iter()
.map(|input| {
Zatoshis::from_u64(*input.value()).map(|value| {
(
OutPoint::new(*input.prevout_txid(), *input.prevout_index()),
value,
)
})
})
.collect::<Result<BTreeMap<_, _>, _>>()?;
let mut tx_extractor = TransactionExtractor::new(finalized);
if let Some((spend_vk, output_vk)) = sapling_vk {
tx_extractor = tx_extractor.with_sapling(spend_vk, output_vk);
}
if let Some(orchard_vk) = orchard_vk {
tx_extractor = tx_extractor.with_orchard(orchard_vk);
}
let transaction = tx_extractor.extract()?;
let txid = transaction.txid();
#[allow(clippy::too_many_arguments)]
fn to_sent_transaction_output<
AccountId: Copy,
D: Domain,
O: ShieldedOutput<D, { ENC_CIPHERTEXT_SIZE }>,
DbT: WalletRead + WalletCommitmentTrees,
N,
>(
domain: D,
note: D::Note,
output: &O,
output_pool: ShieldedPool,
output_index: usize,
pczt_recipient: PcztRecipient<AccountId>,
external_address: Option<ZcashAddress>,
note_value: impl Fn(&D::Note) -> u64,
memo_bytes: impl Fn(&D::Memo) -> &[u8; 512],
wallet_note: impl Fn(D::Note) -> Note,
) -> Result<SentTransactionOutput<AccountId>, ExtractErrT<DbT, N>> {
let pk_d = D::get_pk_d(¬e);
let esk = D::derive_esk(¬e).expect("notes are post-ZIP 212");
let memo = try_output_recovery_with_pkd_esk(&domain, pk_d, esk, output).map(|(_, _, m)| {
MemoBytes::from_bytes(memo_bytes(&m)).expect("Memo is the correct length.")
});
let note_commitment_tree = match output_pool {
ShieldedPool::Sapling => NoteCommitmentTree::Sapling,
ShieldedPool::Orchard => NoteCommitmentTree::Orchard,
ShieldedPool::Ironwood => NoteCommitmentTree::Ironwood,
};
let note_value = Zatoshis::try_from(note_value(¬e))?;
let recipient = match (pczt_recipient, external_address) {
(PcztRecipient::External, Some(addr)) => Ok(Recipient::External {
recipient_address: addr,
output_pool: PoolType::Shielded(output_pool),
}),
(PcztRecipient::External, None) => Err(PcztError::Invalid(
"external recipient needs to have its user_address field set".into(),
)),
#[cfg(feature = "transparent-inputs")]
(PcztRecipient::EphemeralTransparent { .. }, _) => Err(PcztError::Invalid(
"shielded output cannot be EphemeralTransparent".into(),
)),
#[cfg(feature = "transparent-inputs")]
(PcztRecipient::InternalTransparent { .. }, _) => Err(PcztError::Invalid(
"shielded output cannot be InternalTransparent".into(),
)),
(PcztRecipient::InternalShielded { receiving_account }, external_address) => {
Ok(Recipient::InternalShielded {
receiving_account,
external_address,
note: Box::new(wallet_note(note)),
})
}
}?;
Ok(SentTransactionOutput::from_parts_in_tree(
Some(note_commitment_tree),
output_index,
recipient,
note_value,
memo,
))
}
#[cfg(feature = "orchard")]
let orchard_outputs = transaction
.orchard_bundle()
.map(|bundle| {
assert_eq!(bundle.actions().len(), orchard_output_info.len());
bundle
.actions()
.iter()
.zip(orchard_output_info)
.enumerate()
.filter_map(|(output_index, (action, output_info))| {
output_info.map(|((pczt_recipient, external_address), note)| {
let domain = OrchardDomain::for_action(action);
to_sent_transaction_output::<_, _, _, DbT, _>(
domain,
note,
action,
ShieldedPool::Orchard,
output_index,
pczt_recipient,
external_address,
|note| note.value().inner(),
|memo| memo,
|note| Note::Orchard {
note,
pool: orchard::ValuePool::Orchard,
},
)
})
})
.collect::<Result<Vec<_>, _>>()
})
.transpose()?;
#[cfg(feature = "orchard")]
let ironwood_outputs = transaction
.ironwood_bundle()
.map(|bundle| {
assert_eq!(bundle.actions().len(), ironwood_output_info.len());
bundle
.actions()
.iter()
.zip(ironwood_output_info)
.enumerate()
.filter_map(|(output_index, (action, output_info))| {
output_info.map(|((pczt_recipient, external_address), note)| {
let domain = IronwoodDomain::for_action(action);
to_sent_transaction_output::<_, _, _, DbT, _>(
domain,
note,
action,
ShieldedPool::Ironwood,
output_index,
pczt_recipient,
external_address,
|note| note.value().inner(),
|memo| memo,
|note| Note::Orchard {
note,
pool: orchard::ValuePool::Ironwood,
},
)
})
})
.collect::<Result<Vec<_>, _>>()
})
.transpose()?;
let sapling_outputs = transaction
.sapling_bundle()
.map(|bundle| {
assert_eq!(bundle.shielded_outputs().len(), sapling_output_info.len());
bundle
.shielded_outputs()
.iter()
.zip(sapling_output_info)
.enumerate()
.filter_map(|(output_index, (action, output_info))| {
output_info.map(|((pczt_recipient, external_address), note)| {
let domain =
SaplingDomain::new(sapling::note_encryption::Zip212Enforcement::On);
to_sent_transaction_output::<_, _, _, DbT, _>(
domain,
note,
action,
ShieldedPool::Sapling,
output_index,
pczt_recipient,
external_address,
|note| note.value().inner(),
|memo| memo,
Note::Sapling,
)
})
})
.collect::<Result<Vec<_>, _>>()
})
.transpose()?;
#[allow(unused_variables)]
let transparent_outputs = transaction
.transparent_bundle()
.map(|bundle| {
assert_eq!(bundle.vout.len(), transparent_output_info.len());
bundle
.vout
.iter()
.zip(transparent_output_info)
.enumerate()
.filter_map(|(output_index, (output, output_info))| {
output_info.map(|(pczt_recipient, external_address)| {
let outpoint = OutPoint::new(txid.into(), output_index as u32);
let recipient = match (pczt_recipient, external_address) {
(PcztRecipient::External, Some(addr)) => {
Ok(Recipient::External {
recipient_address: addr,
output_pool: PoolType::Transparent,
})
}
(PcztRecipient::External, None) => Err(PcztError::Invalid(
"external recipient needs to have its user_address field set".into(),
)),
#[cfg(feature = "transparent-inputs")]
(PcztRecipient::EphemeralTransparent { receiving_account }, _) => output
.recipient_address()
.ok_or(PcztError::Invalid(
"Ephemeral outputs cannot have a non-standard script_pubkey"
.into(),
))
.map(|ephemeral_address| Recipient::EphemeralTransparent {
receiving_account,
ephemeral_address,
outpoint,
}),
#[cfg(feature = "transparent-inputs")]
(PcztRecipient::InternalTransparent { receiving_account }, _) => output
.recipient_address()
.ok_or(PcztError::Invalid(
"Transparent change outputs cannot have a non-standard script_pubkey"
.into(),
))
.map(|recipient_address| Recipient::InternalTransparent {
receiving_account,
recipient_address,
}),
(
PcztRecipient::InternalShielded {
receiving_account,
},
_,
) => Err(PcztError::Invalid(
"Transparent output cannot be InternalShielded".into(),
)),
}?;
Ok(SentTransactionOutput::from_parts(
output_index,
recipient,
output.value(),
None,
))
})
})
.collect::<Result<Vec<_>, ExtractErrT<DbT, _>>>()
})
.transpose()?;
let mut outputs: Vec<SentTransactionOutput<_>> = vec![];
#[cfg(feature = "orchard")]
outputs.extend(orchard_outputs.into_iter().flatten());
#[cfg(feature = "orchard")]
outputs.extend(ironwood_outputs.into_iter().flatten());
outputs.extend(sapling_outputs.into_iter().flatten());
outputs.extend(transparent_outputs.into_iter().flatten());
let fee_amount = transaction
.fee_paid(|outpoint| Ok::<_, BalanceError>(utxos_map.get(outpoint).copied()))?
.expect("input map was constructed correctly");
let utxos_spent = utxos_map.into_keys().collect::<Vec<_>>();
let created = time::OffsetDateTime::now_utc();
let transactions = vec![SentTransaction::new(
&transaction,
created,
proposal_info.target_height,
proposal_info.from_account,
&outputs,
fee_amount,
#[cfg(feature = "transparent-inputs")]
&utxos_spent,
)];
wallet_db
.store_transactions_to_be_sent(&transactions)
.map_err(Error::DataSource)?;
Ok(txid)
}
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn shield_transparent_funds<DbT, ParamsT, InputsT, ChangeT>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: Zatoshis,
spending_keys: &SpendingKeys,
from_addrs: &[TransparentAddress],
to_account: <DbT as InputSource>::AccountId,
confirmations_policy: ConfirmationsPolicy,
) -> Result<NonEmpty<TxId>, ShieldErrT<DbT, InputsT, ChangeT>>
where
ParamsT: consensus::Parameters,
DbT: WalletWrite + WalletCommitmentTrees + InputSource<Error = <DbT as WalletRead>::Error>,
InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let proposal = propose_shielding(
wallet_db,
params,
input_selector,
change_strategy,
shielding_threshold,
from_addrs,
to_account,
confirmations_policy,
CoinbaseFilter::AllTransparentOutputs,
None,
)?;
create_proposed_transactions(
wallet_db,
params,
spend_prover,
output_prover,
spending_keys,
OvkPolicy::Sender,
&proposal,
None,
)
}
#[cfg(test)]
mod tests {
use super::*;
use zcash_protocol::zip318::AnchorBucketInterval;
fn policy(confirmations: u32) -> ConfirmationsPolicy {
ConfirmationsPolicy::new_symmetrical_unchecked(
confirmations,
#[cfg(feature = "transparent-inputs")]
false,
)
}
fn target(height: u32) -> TargetHeight {
TargetHeight::from(BlockHeight::from_u32(height))
}
#[test]
fn bucketed_policy_lands_the_anchor_on_a_boundary() {
let interval = AnchorBucketInterval::ZIP_318;
let policy = policy(10);
for height in [2_000_000u32, 2_000_143, 2_000_144, 1_000_000] {
let target = target(height);
let bucketed = policy
.bucketed(interval, target, BlockHeight::from_u32(1))
.expect("a boundary is reachable well above the grid's origin");
let anchor = bucketed.anchor_height(target);
assert!(
interval.is_boundary(anchor),
"anchor {anchor:?} off the grid"
);
let most_recent = interval.boundary_at_or_below(policy.anchor_height(target));
assert_eq!(
u32::from(anchor),
u32::from(most_recent) - interval.block_count().get(),
"the chosen boundary must be exactly one interval below the most recent"
);
assert!(anchor < most_recent, "an age of 0 is not admissible");
assert!(bucketed.trusted() >= policy.trusted());
assert!(bucketed.untrusted() >= bucketed.trusted());
assert!(
u32::from(bucketed.trusted()) - u32::from(policy.trusted())
< 2 * interval.block_count().get()
);
}
}
#[test]
fn bucketing_never_chooses_the_most_recent_boundary() {
let interval = AnchorBucketInterval::ZIP_318;
let policy = policy(10);
let aligned_anchor = interval.boundary_at_or_below(BlockHeight::from_u32(2_000_000));
let target = target(u32::from(aligned_anchor) + 10);
let bucketed = policy
.bucketed(interval, target, BlockHeight::from_u32(1))
.expect("reachable");
assert_eq!(
u32::from(bucketed.anchor_height(target)),
u32::from(aligned_anchor) - interval.block_count().get()
);
assert!(bucketed.trusted() > policy.trusted());
}
#[test]
fn bucketing_declines_below_the_first_boundary() {
let interval = AnchorBucketInterval::ZIP_318;
let policy = policy(10);
assert!(
policy
.bucketed(interval, target(100), BlockHeight::from_u32(1))
.is_none()
);
}
#[test]
fn bucketing_honours_a_custom_interval() {
let interval = AnchorBucketInterval::custom(NonZeroU32::new(12).expect("nonzero"));
let policy = policy(3);
let target = target(1_000);
let bucketed = policy
.bucketed(interval, target, BlockHeight::from_u32(1))
.expect("reachable");
let anchor = bucketed.anchor_height(target);
assert!(interval.is_boundary(anchor));
assert_eq!(u32::from(anchor), 984);
}
}