use std::{collections::HashMap, convert::Infallible};
use zcash_client_backend::{
TransferType,
data_api::{
MaxSpendMode,
error::Error,
wallet::{
ConfirmationsPolicy,
input_selection::{GreedyInputSelector, LockedInputPolicy},
propose_send_max_transfer,
},
},
decrypt_transaction,
fees::{
ChangeValue, DustAction, DustOutputPolicy, StandardFeeRule, TransactionBalance,
standard::SingleOutputChangeStrategy,
},
wallet::OvkPolicy,
};
use zcash_primitives::transaction::TxId;
use super::*;
fn one_time_funded(value: u64) -> State {
let mut st = ironwood_wallet();
let account = st.test_account().unwrap().id();
let start = st.sapling_activation_height();
let refund = reserve_key(st.wallet_mut().db_mut(), account, Purpose::Refund, start);
pay(&mut st, refund.full_viewing_key(), value);
st.generate_and_scan_empty_blocks_with_one_time_ivks(2);
st
}
fn markers(st: &mut State, txid: TxId) -> Vec<u64> {
let (height, _) = st.generate_next_block_including(txid);
st.scan_cached_blocks_with_one_time_ivks(height, 1);
let account = st.test_account().cloned().unwrap();
let tx = st.wallet().get_transaction(txid).unwrap().unwrap();
let ufvks = HashMap::from([(account.id(), account.usk().to_unified_full_viewing_key())]);
let decrypted = decrypt_transaction(st.network(), Some(height), None, &tx, &ufvks);
decrypted
.ironwood_outputs()
.iter()
.filter(|output| output.transfer_type() == TransferType::AccountInternal)
.map(|output| output.note().0.value().inner())
.collect()
}
#[test]
fn exact_one_time_spend_carries_a_zero_value_marker() {
let mut st = one_time_funded(2_000_000);
let account = st.test_account().cloned().unwrap();
let to = payment(&unified(foreign_receiver()), 1_990_000, None);
let change = (None, zcash_protocol::ShieldedPool::Orchard);
let (proposal, txid) = transfer(&mut st, (account.id(), account.usk()), vec![to], change);
let step = &proposal.steps()[0];
assert_eq!(
step.balance().proposed_change(),
[ChangeValue::ironwood(Zatoshis::ZERO, None)]
);
let unmarked = Proposal::single_step(
step.transaction_request().clone(),
step.payment_pools().clone(),
vec![],
step.shielded_inputs().cloned(),
step.anchor_height().unwrap(),
TransactionBalance::new(vec![], step.balance().fee_required()).unwrap(),
*proposal.fee_rule(),
proposal.min_target_height(),
proposal.confirmations_policy(),
false,
true,
)
.unwrap();
let created = st.create_proposed_transactions::<Infallible, _, Infallible, _>(
account.usk(),
OvkPolicy::Sender,
&unmarked,
);
assert!(matches!(created, Err(Error::OneTimeSpendWithoutMarker)));
assert_eq!(step.payment_pools()[&0], zcash_protocol::PoolType::IRONWOOD);
let internal = FullViewingKey::from(account.usk().orchard()).address_at(0u32, Scope::Internal);
let to_self = payment(&unified(internal), 1_990_000, None);
let to_self = Proposal::single_step(
zip321::TransactionRequest::new(vec![to_self]).unwrap(),
step.payment_pools().clone(),
vec![],
step.shielded_inputs().cloned(),
step.anchor_height().unwrap(),
TransactionBalance::new(vec![], step.balance().fee_required()).unwrap(),
*proposal.fee_rule(),
proposal.min_target_height(),
proposal.confirmations_policy(),
false,
true,
)
.unwrap();
let created = st.create_proposed_transactions::<Infallible, _, Infallible, _>(
account.usk(),
OvkPolicy::Sender,
&to_self,
);
assert!(matches!(created, Err(Error::OneTimeSpendWithoutMarker)));
assert_eq!(markers(&mut st, txid.unwrap()), [0]);
}
#[test]
fn sending_a_one_time_note_max_carries_a_zero_value_marker() {
let mut st = one_time_funded(2_000_000);
let account = st.test_account().cloned().unwrap();
let to = unified(foreign_receiver()).to_zcash_address(&ironwood_network());
let proposal = propose_send_max_transfer::<_, _, _, Infallible>(
st.wallet_mut(),
&ironwood_network(),
account.id(),
&[zcash_protocol::ShieldedPool::Ironwood],
&StandardFeeRule::Zip317,
to,
None,
MaxSpendMode::MaxSpendable,
ConfirmationsPolicy::MIN,
&LockedInputPolicy::Exclude,
None,
)
.unwrap();
let balance = proposal.steps()[0].balance();
assert_eq!(
balance.proposed_change(),
[ChangeValue::ironwood(Zatoshis::ZERO, None)]
);
assert_eq!(balance.fee_required(), Zatoshis::const_from_u64(10_000));
let txid = st
.create_proposed_transactions::<Infallible, _, Infallible, _>(
account.usk(),
OvkPolicy::Sender,
&proposal,
)
.unwrap()[0];
assert_eq!(markers(&mut st, txid), [0]);
}
#[test]
fn a_one_time_spend_adding_dust_to_the_fee_keeps_its_marker() {
let mut st = one_time_funded(2_000_000);
let account = st.test_account().cloned().unwrap();
let strategy = SingleOutputChangeStrategy::<TestDb>::new(
StandardFeeRule::Zip317,
None,
zcash_protocol::ShieldedPool::Orchard,
DustOutputPolicy::new(DustAction::AddDustToFee, None),
);
let to = payment(&unified(foreign_receiver()), 1_989_900, None);
let request = zip321::TransactionRequest::new(vec![to]).unwrap();
let proposal = st
.propose_transfer(
account.id(),
&GreedyInputSelector::new(),
&strategy,
request,
ConfirmationsPolicy::MIN,
)
.unwrap();
let balance = proposal.steps()[0].balance();
assert_eq!(
balance.proposed_change(),
[ChangeValue::ironwood(Zatoshis::ZERO, None)]
);
assert_eq!(balance.fee_required(), Zatoshis::const_from_u64(10_100));
}