use std::{
collections::{BTreeMap, HashSet},
convert::Infallible,
};
use assert_matches::assert_matches;
use sapling::zip32::ExtendedSpendingKey;
use transparent::{
address::{Script, TransparentAddress},
bundle::{Authorized, Bundle, OutPoint, TxIn, TxOut},
keys::{NonHardenedChildIndex, TransparentKeyScope},
};
use zcash_keys::{
address::Address,
keys::{UnifiedAddressRequest, transparent::gap_limits::GapLimits},
};
use zcash_primitives::{
block::BlockHash,
transaction::{Transaction, TransactionData, TxVersion, fees::zip317},
};
use zcash_protocol::{
consensus::{BlockHeight, BranchId, COINBASE_MATURITY_BLOCKS},
local_consensus::LocalNetwork,
value::Zatoshis,
};
use zip321::{Payment, TransactionRequest};
#[cfg(feature = "transparent-key-import")]
use {
crate::{
data_api::{
AccountBirthday,
chain::ChainState,
wallet::{self, SpendingKeys},
},
wallet::TransparentAddressSource,
},
secp256k1::{Secp256k1, SecretKey},
secrecy::Secret,
std::collections::HashMap,
zcash_protocol::consensus::{NetworkUpgrade, Parameters},
zcash_script::{descriptor::sh, pattern::check_multisig, script},
};
use super::TestAccount;
use crate::{
data_api::{
Account as _, AccountBalance, Balance, CoinbaseFilter, InputSource as _, MaxSpendMode,
TargetValue, WalletRead as _, WalletTest as _, WalletWrite,
testing::{
AddressType, DataStoreFactory, ShieldedPool, TestBuilder, TestCache, TestState,
single_output_change_strategy,
},
wallet::{
ConfirmationsPolicy, TargetHeight, decrypt_and_store_transaction,
input_selection::{
GreedyInputSelector, LockFilter, LockedInputPolicy, NoteSelection, SpendPolicy,
TransparentSpendPolicy,
},
},
},
fees::{ChangeValue, StandardFeeRule, TransparentChangePolicy},
wallet::{Exposure, OvkPolicy, WalletTransparentOutput},
};
pub use super::pool::locking::transparent_note_locking;
fn check_balance<DSF>(
st: &TestState<impl TestCache, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
account: &TestAccount<<DSF as DataStoreFactory>::Account>,
taddr: &TransparentAddress,
confirmations_policy: ConfirmationsPolicy,
expected: &Balance,
) where
DSF: DataStoreFactory,
{
let summary = st
.wallet()
.get_wallet_summary(confirmations_policy)
.unwrap()
.unwrap();
let balance = summary.account_balances().get(&account.id()).unwrap();
#[allow(deprecated)]
let old_unshielded_value = balance.unshielded();
assert_eq!(old_unshielded_value, expected.total());
assert_eq!(balance.unshielded_regular_balance(), expected);
assert_eq!(balance.unshielded_coinbase_balance(), &Balance::ZERO);
assert_eq!(balance.unshielded_balance(), *expected);
let target_height = TargetHeight::from(st.wallet().chain_height().unwrap().unwrap() + 1);
assert_eq!(
st.wallet()
.get_transparent_balances(account.id(), target_height, confirmations_policy)
.unwrap()
.get(taddr)
.cloned()
.map_or(Zatoshis::ZERO, |(_, b)| b.spendable_value()),
expected.total(),
);
assert_eq!(
st.wallet()
.get_spendable_transparent_outputs(
taddr,
target_height,
confirmations_policy,
CoinbaseFilter::AllTransparentOutputs,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.unwrap()
.into_iter()
.map(|utxo| utxo.value())
.sum::<Option<Zatoshis>>(),
Some(expected.spendable_value()),
);
}
pub fn put_received_transparent_utxo<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
<<DSF as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug + PartialEq,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let birthday = st.test_account().unwrap().birthday().height();
let account_id = st.test_account().unwrap().id();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account_id, UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = uaddr.transparent().unwrap();
let height_1 = birthday + 12345;
st.wallet_mut().update_chain_tip(height_1).unwrap();
let bal_absent = st
.wallet()
.get_transparent_balances(
account_id,
TargetHeight::from(height_1 + 1),
ConfirmationsPolicy::MIN,
)
.unwrap();
assert!(bal_absent.is_empty());
let value = Zatoshis::const_from_u64(100000);
let outpoint = OutPoint::fake();
let txout = TxOut::new(value, taddr.script().into());
let utxo = WalletTransparentOutput::from_parts(
outpoint.clone(),
txout.clone(),
Some(height_1),
Some(account_id),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo);
assert_matches!(res0, Ok(_));
let target_height = TargetHeight::from(height_1 + 1);
assert_matches!(
st.wallet().get_spendable_transparent_outputs(
taddr,
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
LockFilter::Policy(&LockedInputPolicy::Exclude),
).as_deref(),
Ok([ret])
if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo.outpoint(), utxo.txout(), Some(height_1))
);
assert_matches!(
st.wallet()
.get_unspent_transparent_output(utxo.outpoint(), target_height),
Ok(Some(ret))
if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo.outpoint(), utxo.txout(), Some(height_1))
);
let height_2 = birthday + 34567;
st.wallet_mut().update_chain_tip(height_2).unwrap();
let utxo2 = WalletTransparentOutput::from_parts(
outpoint,
txout,
Some(height_2),
Some(account_id),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
let res1 = st.wallet_mut().put_received_transparent_utxo(&utxo2);
assert_matches!(res1, Ok(id) if id == res0.unwrap());
assert_matches!(
st.wallet()
.get_spendable_transparent_outputs(
taddr,
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
LockFilter::Policy(&LockedInputPolicy::Exclude)
)
.as_deref(),
Ok(&[])
);
assert_matches!(
st.wallet()
.get_unspent_transparent_output(utxo2.outpoint(), target_height),
Ok(Some(ret))
if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo2.outpoint(), utxo2.txout(), Some(height_2))
);
assert_matches!(
st.wallet()
.get_spendable_transparent_outputs(taddr, TargetHeight::from(height_2 + 1), ConfirmationsPolicy::MIN, CoinbaseFilter::AllTransparentOutputs, LockFilter::Policy(&LockedInputPolicy::Exclude))
.as_deref(),
Ok([ret]) if (ret.outpoint(), ret.txout(), ret.mined_height()) == (utxo.outpoint(), utxo.txout(), Some(height_2))
);
assert_matches!(
st.wallet().get_transparent_balances(
account_id,
TargetHeight::from(height_2 + 1),
ConfirmationsPolicy::MIN
),
Ok(h) if h.get(taddr).map(|(_, b)| b.spendable_value()) == Some(value)
);
}
pub fn transparent_balance_across_shielding<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = uaddr.transparent().unwrap();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::MIN,
&Balance::ZERO,
);
let value = Zatoshis::from_u64(100000).unwrap();
let txout = TxOut::new(value, taddr.script().into());
let height = st.wallet().chain_height().unwrap().unwrap();
let utxo = WalletTransparentOutput::from_parts(
OutPoint::fake(),
txout,
Some(height),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
let mut zero_or_one_conf_value = Balance::ZERO;
zero_or_one_conf_value.add_spendable_value(value).unwrap();
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::MIN,
&zero_or_one_conf_value,
);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let txid = st
.shield_transparent_funds(
&input_selector,
&change_strategy,
value,
account.usk(),
&[*taddr],
account.id(),
ConfirmationsPolicy::MIN,
)
.unwrap()[0];
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::MIN,
&Balance::ZERO,
);
let (mined_height, _) = st.generate_next_block_including(txid);
st.scan_cached_blocks(mined_height, 1);
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::MIN,
&Balance::ZERO,
);
st.wallet_mut()
.truncate_to_height(mined_height - 1)
.unwrap();
assert_eq!(st.wallet().chain_height().unwrap(), Some(mined_height - 1));
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::MIN,
&Balance::ZERO,
);
let expiry_height = st
.wallet()
.get_transaction(txid)
.unwrap()
.expect("Transaction exists in the wallet.")
.expiry_height();
st.wallet_mut().update_chain_tip(expiry_height).unwrap();
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::MIN,
&zero_or_one_conf_value,
);
}
pub fn shielding_many_transparent_utxos<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
const NUM_UTXOS: usize = 160;
const PER_UTXO: u64 = 100_000;
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = uaddr.transparent().unwrap();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10_000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
let value = Zatoshis::const_from_u64(PER_UTXO);
let txout = TxOut::new(value, taddr.script().into());
let height = st.wallet().chain_height().unwrap().unwrap();
for i in 0..NUM_UTXOS {
let mut hash = [0u8; 32];
hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
let outpoint = OutPoint::new(hash, 0);
let utxo = WalletTransparentOutput::from_parts(
outpoint,
txout.clone(),
Some(height),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
}
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let txids = st
.shield_transparent_funds(
&input_selector,
&change_strategy,
value,
account.usk(),
&[*taddr],
account.id(),
ConfirmationsPolicy::MIN,
)
.expect("shielding many P2PKH UTXOs should succeed");
assert_eq!(txids.len(), 1);
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::MIN,
&Balance::ZERO,
);
}
pub fn get_spendable_transparent_outputs_for_addresses<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().id();
let birthday = st.test_account().unwrap().birthday().height();
let height_1 = birthday + 12345;
st.wallet_mut().update_chain_tip(height_1).unwrap();
let mut taddrs = Vec::new();
while taddrs.len() < 3 {
let (ua, _) = st
.wallet_mut()
.get_next_available_address(account_id, UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.expect("an address should be available within the gap limit");
if let Some(taddr) = ua.transparent() {
taddrs.push(*taddr);
}
}
let value = Zatoshis::const_from_u64(100_000);
for (i, taddr) in taddrs.iter().enumerate() {
let mut hash = [0u8; 32];
hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
let utxo = WalletTransparentOutput::from_parts(
OutPoint::new(hash, 0),
TxOut::new(value, taddr.script().into()),
Some(height_1),
Some(account_id),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
}
let target_height = TargetHeight::from(height_1 + 1);
let sorted = |mut v: Vec<TransparentAddress>| {
v.sort();
v
};
let all = st
.wallet()
.get_spendable_transparent_outputs_for_addresses(
&taddrs,
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.unwrap();
assert_eq!(all.len(), 3);
assert_eq!(
sorted(all.iter().map(|u| *u.recipient_address()).collect()),
sorted(taddrs.clone()),
);
let mut per_address = Vec::new();
for taddr in &taddrs {
per_address.extend(
st.wallet()
.get_spendable_transparent_outputs(
taddr,
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.unwrap(),
);
}
assert_eq!(
sorted(all.iter().map(|u| *u.recipient_address()).collect()),
sorted(per_address.iter().map(|u| *u.recipient_address()).collect()),
);
let subset = st
.wallet()
.get_spendable_transparent_outputs_for_addresses(
&taddrs[..1],
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.unwrap();
assert_eq!(subset.len(), 1);
assert_eq!(subset[0].recipient_address(), &taddrs[0]);
assert!(
st.wallet()
.get_spendable_transparent_outputs_for_addresses(
&[],
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.unwrap()
.is_empty()
);
}
pub fn shielding_transparent_input_cap<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
const BLOCK_SPACE_PERCENT: u32 = 1;
const CAP: usize = 133;
const NUM_UTXOS: usize = CAP + 7; const BASE: u64 = 100_000;
const STEP: u64 = 1_000;
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = uaddr.transparent().unwrap();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10_000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
let height = st.wallet().chain_height().unwrap().unwrap();
for i in 0..NUM_UTXOS {
let value = Zatoshis::const_from_u64(BASE + (i as u64) * STEP);
let mut hash = [0u8; 32];
hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
let utxo = WalletTransparentOutput::from_parts(
OutPoint::new(hash, 0),
TxOut::new(value, taddr.script().into()),
Some(height),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
}
let input_selector =
GreedyInputSelector::new().with_shielding_block_space_percent(BLOCK_SPACE_PERCENT);
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let proposal = st
.propose_shielding(
&input_selector,
&change_strategy,
Zatoshis::const_from_u64(BASE),
&[*taddr],
account.id(),
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
)
.expect("shielding proposal should succeed");
let inputs = proposal.steps().first().transparent_inputs();
assert_eq!(
inputs.len(),
CAP,
"the number of transparent inputs should be capped",
);
let min_selected = inputs.iter().map(|u| u.value()).min().unwrap();
assert_eq!(
min_selected,
Zatoshis::const_from_u64(BASE + ((NUM_UTXOS - CAP) as u64) * STEP),
"the lowest-value UTXOs should be the ones left unspent",
);
}
pub fn transparent_balance_spendability<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = uaddr.transparent().unwrap();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
check_balance::<DSF>(
&st as &TestState<_, DSF::DataStore, _>,
&account,
taddr,
ConfirmationsPolicy::MIN,
&Balance::ZERO,
);
let value = Zatoshis::from_u64(100000).unwrap();
let txout = TxOut::new(value, taddr.script().into());
let height = st.wallet().chain_height().unwrap().unwrap();
let utxo = WalletTransparentOutput::from_parts(
OutPoint::fake(),
txout,
Some(height),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
let mut zero_or_one_conf_value = Balance::ZERO;
zero_or_one_conf_value.add_spendable_value(value).unwrap();
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::MIN,
&zero_or_one_conf_value,
);
let mut not_confirmed_yet_value = Balance::ZERO;
not_confirmed_yet_value
.add_pending_spendable_value(value)
.unwrap();
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::new_symmetrical_unchecked(2, false),
¬_confirmed_yet_value,
);
st.generate_empty_block();
st.scan_cached_blocks(height, 1);
st.generate_empty_block();
st.scan_cached_blocks(height + 1, 1);
check_balance::<DSF>(
&st,
&account,
taddr,
ConfirmationsPolicy::new_symmetrical_unchecked(2, true),
&zero_or_one_conf_value,
);
}
fn fake_transparent_coinbase_tx(
lock_time: u32,
value: Zatoshis,
taddr: &TransparentAddress,
) -> Transaction {
let coinbase_bundle = Bundle {
vin: vec![TxIn::from_parts(
OutPoint::NULL,
Script::default(),
u32::MAX,
)],
vout: vec![TxOut::new(value, taddr.script().into())],
authorization: Authorized,
};
TransactionData::<zcash_primitives::transaction::Authorized>::from_parts(
TxVersion::V5,
BranchId::Nu5,
lock_time,
BlockHeight::from(0),
#[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
Zatoshis::ZERO,
Some(coinbase_bundle),
None,
None,
None,
)
.freeze()
.unwrap()
}
fn get_account_balance<DSF>(
st: &TestState<impl TestCache, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
account: &TestAccount<<DSF as DataStoreFactory>::Account>,
confirmations_policy: ConfirmationsPolicy,
) -> AccountBalance
where
DSF: DataStoreFactory,
{
let summary = st
.wallet()
.get_wallet_summary(confirmations_policy)
.unwrap()
.unwrap();
*summary.account_balances().get(&account.id()).unwrap()
}
pub fn transparent_coinbase_balance_split<DSF>(ds_factory: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(ds_factory)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = uaddr.transparent().unwrap();
let coinbase_value = Zatoshis::const_from_u64(625_000_000);
let coinbase_tx = fake_transparent_coinbase_tx(0, coinbase_value, taddr);
let (h, _) = st.generate_next_block_from_tx(0, &coinbase_tx);
st.scan_cached_blocks(h, 1);
let params = *st.network();
decrypt_and_store_transaction(¶ms, st.wallet_mut(), &coinbase_tx, Some(h)).unwrap();
let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
assert_eq!(
balance.unshielded_coinbase_balance().spendable_value(),
Zatoshis::ZERO
);
assert_eq!(
balance
.unshielded_coinbase_balance()
.value_pending_spendability(),
coinbase_value
);
assert_eq!(balance.unshielded_regular_balance(), &Balance::ZERO);
let balance = get_account_balance::<DSF>(
&st,
&account,
ConfirmationsPolicy::new_symmetrical_unchecked(2, false),
);
assert_eq!(
balance.unshielded_coinbase_balance().spendable_value(),
Zatoshis::ZERO
);
assert_eq!(
balance
.unshielded_coinbase_balance()
.value_pending_spendability(),
coinbase_value
);
assert_eq!(balance.unshielded_regular_balance(), &Balance::ZERO);
let regular_value = Zatoshis::const_from_u64(100_000);
let utxo = WalletTransparentOutput::from_parts(
OutPoint::fake(),
TxOut::new(regular_value, taddr.script().into()),
Some(h),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
assert_eq!(
balance.unshielded_regular_balance().spendable_value(),
regular_value
);
assert_eq!(
balance
.unshielded_coinbase_balance()
.value_pending_spendability(),
coinbase_value
);
assert_eq!(
balance.unshielded_balance(),
(*balance.unshielded_regular_balance() + *balance.unshielded_coinbase_balance()).unwrap()
);
#[allow(deprecated)]
let unshielded = balance.unshielded();
assert_eq!(unshielded, (regular_value + coinbase_value).unwrap());
assert_eq!(balance.total(), (regular_value + coinbase_value).unwrap());
for _ in 0..COINBASE_MATURITY_BLOCKS {
st.generate_empty_block();
}
st.scan_cached_blocks(h + 1, COINBASE_MATURITY_BLOCKS as usize);
let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
assert_eq!(
balance.unshielded_coinbase_balance().spendable_value(),
coinbase_value
);
assert_eq!(
balance
.unshielded_coinbase_balance()
.value_pending_spendability(),
Zatoshis::ZERO
);
assert_eq!(
balance.unshielded_regular_balance().spendable_value(),
regular_value
);
assert_eq!(balance.total(), (regular_value + coinbase_value).unwrap());
}
pub fn transparent_coinbase_balance_dust<DSF>(ds_factory: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let dust_value = Zatoshis::const_from_u64(1000);
assert!(dust_value <= zip317::MARGINAL_FEE);
let mut st = TestBuilder::new()
.with_data_store_factory(ds_factory)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = uaddr.transparent().unwrap();
let coinbase_tx = fake_transparent_coinbase_tx(0, dust_value, taddr);
let (h, _) = st.generate_next_block_from_tx(0, &coinbase_tx);
st.scan_cached_blocks(h, 1);
let params = *st.network();
decrypt_and_store_transaction(¶ms, st.wallet_mut(), &coinbase_tx, Some(h)).unwrap();
let utxo = WalletTransparentOutput::from_parts(
OutPoint::fake(),
TxOut::new(dust_value, taddr.script().into()),
Some(h),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
assert_eq!(
balance.unshielded_regular_balance().uneconomic_value(),
dust_value
);
assert_eq!(
balance.unshielded_coinbase_balance().uneconomic_value(),
dust_value
);
assert_eq!(
balance.uneconomic_value(),
(dust_value + dust_value).unwrap()
);
assert_eq!(
balance.unshielded_balance().spendable_value(),
Zatoshis::ZERO
);
assert_eq!(
balance.unshielded_balance().value_pending_spendability(),
Zatoshis::ZERO
);
assert_eq!(balance.total(), Zatoshis::ZERO);
for _ in 0..COINBASE_MATURITY_BLOCKS {
st.generate_empty_block();
}
st.scan_cached_blocks(h + 1, COINBASE_MATURITY_BLOCKS as usize);
let balance = get_account_balance::<DSF>(&st, &account, ConfirmationsPolicy::MIN);
assert_eq!(
balance.unshielded_coinbase_balance().uneconomic_value(),
dust_value
);
assert_eq!(
balance.unshielded_coinbase_balance().spendable_value(),
Zatoshis::ZERO
);
}
pub fn gap_limits<DSF>(ds_factory: DSF, cache: impl TestCache, gap_limits: GapLimits)
where
DSF: DataStoreFactory,
<DSF as DataStoreFactory>::AccountId: std::fmt::Debug,
{
let mut st = TestBuilder::new()
.with_data_store_factory(ds_factory)
.with_block_cache(cache)
.with_gap_limits(gap_limits)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let test_account = st.test_account().cloned().unwrap();
let account_uuid = test_account.account().id();
let ufvk = test_account.account().ufvk().unwrap().clone();
let external_taddrs = st
.wallet()
.get_transparent_receivers(account_uuid, false, true)
.unwrap();
assert_eq!(
u32::try_from(external_taddrs.len()).unwrap(),
gap_limits.external()
);
let internal_taddrs = st
.wallet()
.get_transparent_receivers(account_uuid, true, false)
.unwrap();
assert_eq!(
u32::try_from(internal_taddrs.len()).unwrap(),
gap_limits.external() + gap_limits.internal()
);
let ephemeral_taddrs = st
.wallet()
.get_known_ephemeral_addresses(account_uuid, None)
.unwrap();
assert_eq!(
u32::try_from(ephemeral_taddrs.len()).unwrap(),
gap_limits.ephemeral()
);
let (h0, _, _) = st.generate_next_block(
&ufvk.sapling().unwrap(),
AddressType::DefaultExternal,
Zatoshis::const_from_u64(1000000),
);
st.scan_cached_blocks(h0, 1);
let external_taddrs = st
.wallet()
.get_transparent_receivers(account_uuid, false, true)
.unwrap();
assert_eq!(
u32::try_from(external_taddrs.len()).unwrap(),
gap_limits.external()
+ (u32::try_from(ufvk.sapling().unwrap().default_address().0).unwrap() + 1)
);
let external_taddrs_sorted = external_taddrs
.into_iter()
.filter_map(|(addr, meta)| meta.address_index().map(|i| (i, addr)))
.collect::<BTreeMap<_, _>>();
let to = Address::from(
*external_taddrs_sorted
.get(&transparent::keys::NonHardenedChildIndex::from_index(4).unwrap())
.expect("An address exists at index 4."),
)
.to_zcash_address(st.network());
let txids = st
.create_standard_transaction(&test_account, to, Zatoshis::const_from_u64(20000))
.unwrap();
let (h1, _) = st.generate_next_block_including(txids.head);
let external_taddrs = st
.wallet()
.get_transparent_receivers(account_uuid, false, true)
.unwrap();
assert_eq!(
u32::try_from(external_taddrs.len()).unwrap(),
gap_limits.external()
+ (u32::try_from(ufvk.sapling().unwrap().default_address().0).unwrap() + 1)
);
st.scan_cached_blocks(h1, 1);
let tx = st.wallet().get_transaction(txids.head).unwrap().unwrap();
decrypt_and_store_transaction(&st.network().clone(), st.wallet_mut(), &tx, Some(h1)).unwrap();
let external_taddrs = st
.wallet()
.get_transparent_receivers(account_uuid, false, true)
.unwrap();
assert_eq!(
u32::try_from(external_taddrs.len()).unwrap(),
gap_limits.external() + 5
);
let query_height = st.wallet().utxo_query_height(account_uuid).unwrap();
assert_eq!(query_height, h0);
}
#[cfg(feature = "transparent-key-import")]
fn build_test_redeem_script() -> (script::Redeem, secp256k1::SecretKey) {
let secp = Secp256k1::new();
let secret_key = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key");
let pubkey = secret_key.public_key(&secp);
let redeem_script = script::Component(
check_multisig(1, &[&pubkey.serialize()], false)
.unwrap()
.into_iter()
.collect(),
);
(redeem_script, secret_key)
}
#[cfg(feature = "transparent-key-import")]
pub fn import_standalone_transparent_pubkey<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().id();
let secp = Secp256k1::new();
let secret_key = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key");
let pubkey = secret_key.public_key(&secp);
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_pubkey(account_id, pubkey),
Ok(_)
);
}
#[cfg(feature = "transparent-key-import")]
pub fn import_standalone_transparent_pubkey_idempotent<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().id();
let secp = Secp256k1::new();
let secret_key = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key");
let pubkey = secret_key.public_key(&secp);
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_pubkey(account_id, pubkey),
Ok(_)
);
let receivers_before = st
.wallet()
.get_transparent_receivers(account_id, false, true)
.unwrap();
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_pubkey(account_id, pubkey),
Ok(_)
);
let receivers_after = st
.wallet()
.get_transparent_receivers(account_id, false, true)
.unwrap();
assert_eq!(receivers_before.len(), receivers_after.len());
let taddr = TransparentAddress::from_pubkey(&pubkey);
let metadata = receivers_after
.get(&taddr)
.expect("address should be present");
assert!(matches!(
metadata.source(),
TransparentAddressSource::StandalonePubkey(_)
));
}
#[cfg(feature = "transparent-key-import")]
pub fn import_standalone_transparent_pubkey_conflict<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account1_id = st.test_account().unwrap().id();
let secp = Secp256k1::new();
let secret_key = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key");
let pubkey = secret_key.public_key(&secp);
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_pubkey(account1_id, pubkey),
Ok(_)
);
let birthday = AccountBirthday::from_parts(
ChainState::empty(
st.network()
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
- 1,
BlockHash([0; 32]),
),
None,
);
let seed2 = Secret::new(vec![42u8; 32]);
let (account2_id, _) = st
.wallet_mut()
.create_account("account2", &seed2, &birthday, None)
.unwrap();
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_pubkey(account2_id, pubkey),
Err(_)
);
}
#[cfg(feature = "transparent-key-import")]
pub fn import_standalone_transparent_pubkey_balance<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
<<DSF as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().id();
let birthday = st.test_account().unwrap().birthday().height();
let secp = Secp256k1::new();
let secret_key = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key");
let pubkey = secret_key.public_key(&secp);
st.wallet_mut()
.import_standalone_transparent_pubkey(account_id, pubkey)
.unwrap();
let taddr = TransparentAddress::from_pubkey(&pubkey);
let height = birthday + 1000;
st.wallet_mut().update_chain_tip(height).unwrap();
let value = Zatoshis::const_from_u64(50_000);
let outpoint = OutPoint::fake();
let txout = TxOut::new(value, taddr.script().into());
let utxo = WalletTransparentOutput::from_parts(
outpoint,
txout,
Some(height),
Some(account_id),
None,
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
let target_height = TargetHeight::from(height + 1);
let balances = st
.wallet()
.get_transparent_balances(account_id, target_height, ConfirmationsPolicy::MIN)
.unwrap();
assert_eq!(
balances.get(&taddr).map(|(_, b)| b.spendable_value()),
Some(value),
);
let utxos = st
.wallet()
.get_spendable_transparent_outputs(
&taddr,
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.unwrap();
assert_eq!(utxos.len(), 1);
assert_eq!(utxos[0].value(), value);
}
#[cfg(feature = "transparent-key-import")]
pub fn spend_from_standalone_pubkey<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let account_id = account.id();
let secp = Secp256k1::new();
let secret_key = SecretKey::from_slice(&[1u8; 32]).expect("valid secret key");
let pubkey = secret_key.public_key(&secp);
st.wallet_mut()
.import_standalone_transparent_pubkey(account_id, pubkey)
.unwrap();
let taddr = TransparentAddress::from_pubkey(&pubkey);
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
let value = Zatoshis::from_u64(100000).unwrap();
let height = st.wallet().chain_height().unwrap().unwrap();
let txout = TxOut::new(value, taddr.script().into());
let utxo = WalletTransparentOutput::from_parts(
OutPoint::fake(),
txout,
Some(height),
Some(account_id),
None,
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
let mut standalone_keys = HashMap::new();
standalone_keys.insert(taddr, vec![secret_key]);
let spending_keys = SpendingKeys::new(
account.usk().clone(),
#[cfg(feature = "transparent-key-import")]
standalone_keys,
);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let prover = ::zcash_proofs::prover::LocalTxProver::bundled();
let network = *st.network();
let txids = wallet::shield_transparent_funds(
st.wallet_mut(),
&network,
&prover,
&prover,
&input_selector,
&change_strategy,
value,
&spending_keys,
&[taddr],
account_id,
ConfirmationsPolicy::MIN,
)
.unwrap();
assert!(!txids.is_empty());
check_balance::<DSF>(
&st,
&account,
&taddr,
ConfirmationsPolicy::MIN,
&Balance::ZERO,
);
let fee = st
.get_tx_from_history(*txids.first())
.unwrap()
.unwrap()
.fee_paid()
.expect("fee should be known for wallet-created transactions");
let summary = st
.wallet()
.get_wallet_summary(ConfirmationsPolicy::MIN)
.unwrap()
.unwrap();
let account_balance = summary.account_balances().get(&account_id).unwrap();
assert_eq!(
account_balance
.sapling_balance()
.change_pending_confirmation(),
(value - fee).unwrap(),
);
}
#[cfg(feature = "transparent-key-import")]
pub fn import_standalone_transparent_p2sh<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().id();
let (redeem_script, _) = build_test_redeem_script();
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_script(account_id, redeem_script.clone()),
Ok(_)
);
let receivers = st
.wallet()
.get_transparent_receivers(account_id, false, true)
.unwrap();
let script_pubkey = sh(&redeem_script);
let expected_addr =
TransparentAddress::from_script_pubkey(&script_pubkey).expect("valid P2SH address");
let metadata = receivers
.get(&expected_addr)
.expect("address should be present");
assert!(matches!(
metadata.source(),
TransparentAddressSource::StandaloneScript(_)
));
}
#[cfg(feature = "transparent-key-import")]
pub fn import_standalone_transparent_p2sh_idempotent<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().id();
let (redeem_script, _) = build_test_redeem_script();
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_script(account_id, redeem_script.clone()),
Ok(_)
);
let receivers_before = st
.wallet()
.get_transparent_receivers(account_id, false, true)
.unwrap();
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_script(account_id, redeem_script.clone()),
Ok(_)
);
let receivers_after = st
.wallet()
.get_transparent_receivers(account_id, false, true)
.unwrap();
assert_eq!(receivers_before.len(), receivers_after.len());
let script_pubkey = sh(&redeem_script);
let expected_addr =
TransparentAddress::from_script_pubkey(&script_pubkey).expect("valid P2SH address");
let metadata = receivers_after
.get(&expected_addr)
.expect("address should be present");
assert!(matches!(
metadata.source(),
TransparentAddressSource::StandaloneScript(_)
));
}
#[cfg(feature = "transparent-key-import")]
pub fn import_standalone_transparent_p2sh_conflict<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account1_id = st.test_account().unwrap().id();
let (redeem_script, _) = build_test_redeem_script();
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_script(account1_id, redeem_script.clone()),
Ok(_)
);
let birthday = AccountBirthday::from_parts(
ChainState::empty(
st.network()
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
- 1,
BlockHash([0; 32]),
),
None,
);
let seed2 = Secret::new(vec![42u8; 32]);
let (account2_id, _) = st
.wallet_mut()
.create_account("account2", &seed2, &birthday, None)
.unwrap();
assert_matches!(
st.wallet_mut()
.import_standalone_transparent_script(account2_id, redeem_script),
Err(_)
);
}
#[cfg(feature = "transparent-key-import")]
pub fn import_standalone_transparent_p2sh_balance<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
<<DSF as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().id();
let birthday = st.test_account().unwrap().birthday().height();
let (redeem_script, _) = build_test_redeem_script();
st.wallet_mut()
.import_standalone_transparent_script(account_id, redeem_script.clone())
.unwrap();
let script_pubkey = sh(&redeem_script);
let taddr = TransparentAddress::from_script_pubkey(&script_pubkey).expect("valid P2SH address");
let height = birthday + 1000;
st.wallet_mut().update_chain_tip(height).unwrap();
let value = Zatoshis::const_from_u64(50_000);
let outpoint = OutPoint::fake();
let txout = TxOut::new(value, taddr.script().into());
let utxo = WalletTransparentOutput::from_parts(
outpoint,
txout,
Some(height),
Some(account_id),
None,
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
let target_height = TargetHeight::from(height + 1);
let balances = st
.wallet()
.get_transparent_balances(account_id, target_height, ConfirmationsPolicy::MIN)
.unwrap();
assert_eq!(
balances.get(&taddr).map(|(_, b)| b.spendable_value()),
Some(value),
);
let utxos = st
.wallet()
.get_spendable_transparent_outputs(
&taddr,
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.unwrap();
assert_eq!(utxos.len(), 1);
assert_eq!(utxos[0].value(), value);
}
#[cfg(feature = "transparent-key-import")]
pub fn spend_from_standalone_p2sh<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let account_id = account.id();
let (redeem_script, secret_key) = build_test_redeem_script();
st.wallet_mut()
.import_standalone_transparent_script(account_id, redeem_script.clone())
.unwrap();
let script_pubkey = sh(&redeem_script);
let taddr = TransparentAddress::from_script_pubkey(&script_pubkey).expect("valid P2SH address");
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
let value = Zatoshis::from_u64(100000).unwrap();
let height = st.wallet().chain_height().unwrap().unwrap();
let txout = TxOut::new(value, taddr.script().into());
let utxo = WalletTransparentOutput::from_parts(
OutPoint::fake(),
txout,
Some(height),
Some(account_id),
None,
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
let mut standalone_keys = HashMap::new();
standalone_keys.insert(taddr, vec![secret_key]);
let spending_keys = SpendingKeys::new(
account.usk().clone(),
#[cfg(feature = "transparent-key-import")]
standalone_keys,
);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let prover = ::zcash_proofs::prover::LocalTxProver::bundled();
let network = *st.network();
let txids = wallet::shield_transparent_funds(
st.wallet_mut(),
&network,
&prover,
&prover,
&input_selector,
&change_strategy,
value,
&spending_keys,
&[taddr],
account_id,
ConfirmationsPolicy::MIN,
)
.unwrap();
assert!(!txids.is_empty());
check_balance::<DSF>(
&st,
&account,
&taddr,
ConfirmationsPolicy::MIN,
&Balance::ZERO,
);
let fee = st
.get_tx_from_history(*txids.first())
.unwrap()
.unwrap()
.fee_paid()
.expect("fee should be known for wallet-created transactions");
let summary = st
.wallet()
.get_wallet_summary(ConfirmationsPolicy::MIN)
.unwrap()
.unwrap();
let account_balance = summary.account_balances().get(&account_id).unwrap();
assert_eq!(
account_balance
.sapling_balance()
.change_pending_confirmation(),
(value - fee).unwrap(),
);
}
pub fn mark_transparent_addresses_exposed<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().id();
let taddr = *st
.wallet()
.get_last_generated_address_matching(account_id, UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap()
.transparent()
.unwrap();
let exposure_of = |st: &TestState<_, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
addr: &TransparentAddress|
-> Exposure {
st.wallet()
.get_transparent_address_metadata(account_id, addr)
.unwrap()
.unwrap()
.exposure()
};
let initial = exposure_of(&st, &taddr);
let very_high = BlockHeight::from(u32::MAX);
st.wallet_mut()
.mark_transparent_addresses_exposed(&[(taddr, very_high)])
.unwrap();
match initial {
Exposure::Exposed { at_height, .. } => assert_matches!(
exposure_of(&st, &taddr),
Exposure::Exposed { at_height: h, .. } if h == at_height
),
Exposure::Unknown | Exposure::CannotKnow => assert_matches!(
exposure_of(&st, &taddr),
Exposure::Exposed { at_height: h, .. } if h == very_high
),
}
st.wallet_mut()
.mark_transparent_addresses_exposed(&[(taddr, BlockHeight::from(0))])
.unwrap();
assert_matches!(
exposure_of(&st, &taddr),
Exposure::Exposed { at_height, .. } if at_height == BlockHeight::from(0)
);
st.wallet_mut()
.mark_transparent_addresses_exposed(&[(taddr, BlockHeight::from(100))])
.unwrap();
assert_matches!(
exposure_of(&st, &taddr),
Exposure::Exposed { at_height, .. } if at_height == BlockHeight::from(0)
);
let unknown = TransparentAddress::PublicKeyHash([0u8; 20]);
assert!(
st.wallet_mut()
.mark_transparent_addresses_exposed(&[(unknown, BlockHeight::from(1))])
.is_err()
);
st.wallet_mut()
.mark_transparent_addresses_exposed(&[])
.unwrap();
}
pub fn mark_transparent_addresses_exposed_bulk<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let gap_limits = GapLimits::new(5, 2, 2);
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_gap_limits(gap_limits)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().id();
let mut receivers = st
.wallet()
.get_transparent_receivers(account_id, false, true)
.unwrap()
.into_iter()
.filter_map(|(addr, meta)| {
let exposure = meta.exposure();
meta.address_index().map(|i| (i.index(), addr, exposure))
})
.collect::<Vec<_>>();
receivers.sort_by_key(|(i, _, _)| *i);
let unexposed = receivers
.iter()
.copied()
.filter(|(_, _, exposure)| matches!(exposure, Exposure::Unknown))
.collect::<Vec<_>>();
assert!(
unexposed.len() >= 2,
"account should have at least 2 unexposed derived receivers"
);
let (_idx_a, addr_a, _) = unexposed[0];
let (_idx_b, addr_b, _) = unexposed[1];
let height_a = BlockHeight::from(10);
let height_b = BlockHeight::from(20);
st.wallet_mut()
.mark_transparent_addresses_exposed(&[(addr_a, height_a), (addr_b, height_b)])
.unwrap();
let exposure_of = |st: &TestState<_, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
addr: &TransparentAddress|
-> Exposure {
st.wallet()
.get_transparent_address_metadata(account_id, addr)
.unwrap()
.unwrap()
.exposure()
};
assert_matches!(
exposure_of(&st, &addr_a),
Exposure::Exposed { at_height, .. } if at_height == height_a
);
assert_matches!(
exposure_of(&st, &addr_b),
Exposure::Exposed { at_height, .. } if at_height == height_b
);
let (idx_c, addr_c, _) = *receivers
.iter()
.find(|(_, addr, _)| *addr != addr_a && *addr != addr_b)
.expect("account should have a third derived receiver");
let before = exposure_of(&st, &addr_c);
let unknown = TransparentAddress::PublicKeyHash([0x7u8; 20]);
assert!(
st.wallet_mut()
.mark_transparent_addresses_exposed(&[
(addr_c, BlockHeight::from(5)),
(unknown, BlockHeight::from(5)),
])
.is_err()
);
assert_eq!(
exposure_of(&st, &addr_c),
before,
"exposure at index {idx_c} must not change when bulk call fails atomically"
);
}
pub fn mark_transparent_addresses_exposed_unknown_address<DSF>(dsf: DSF)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let unknown = TransparentAddress::PublicKeyHash([0u8; 20]);
assert!(
st.wallet_mut()
.mark_transparent_addresses_exposed(&[(unknown, BlockHeight::from(1))])
.is_err()
);
}
#[allow(clippy::type_complexity)]
fn setup_transparent_only_account<DSF>(
dsf: DSF,
cache: impl TestCache,
utxo_value: Zatoshis,
) -> (
TestState<impl TestCache, <DSF as DataStoreFactory>::DataStore, LocalNetwork>,
TestAccount<<DSF as DataStoreFactory>::Account>,
OutPoint,
)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = uaddr.transparent().unwrap();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
let txout = TxOut::new(utxo_value, taddr.script().into());
let height = st.wallet().chain_height().unwrap().unwrap();
let outpoint = OutPoint::fake();
let utxo = WalletTransparentOutput::from_parts(
outpoint.clone(),
txout,
Some(height),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
(st, account, outpoint)
}
fn t2t_request(network: &LocalNetwork, amount: Zatoshis) -> TransactionRequest {
let recipient = TransparentAddress::PublicKeyHash([7u8; 20]);
TransactionRequest::new(vec![Payment::without_memo(
Address::Transparent(recipient).to_zcash_address(network),
amount,
)])
.unwrap()
}
pub fn propose_t2t_shielded_only_is_insufficient<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let utxo_value = Zatoshis::const_from_u64(100_000);
let (mut st, account, _outpoint) = setup_transparent_only_account(dsf, cache, utxo_value);
let network = *st.network();
let request = t2t_request(&network, Zatoshis::const_from_u64(40_000));
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let result = st.propose_transfer_with_policy(
account.id(),
&input_selector,
&change_strategy,
request,
ConfirmationsPolicy::MIN,
&SpendPolicy::default(),
);
assert_matches!(
result,
Err(crate::data_api::error::Error::InsufficientFunds { .. }),
"shielded-only policy must not spend transparent UTXOs as a fallback",
);
}
pub fn propose_t2t_any_account_taddr<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let utxo_value = Zatoshis::const_from_u64(100_000);
let transfer_amount = Zatoshis::const_from_u64(40_000);
let (mut st, account, outpoint) = setup_transparent_only_account(dsf, cache, utxo_value);
let network = *st.network();
let request = t2t_request(&network, transfer_amount);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let proposal = st
.propose_transfer_with_policy(
account.id(),
&input_selector,
&change_strategy,
request,
ConfirmationsPolicy::MIN,
&SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
)
.expect("transparent spend must succeed under any-account-address transparent spending");
assert_eq!(proposal.steps().len(), 1);
let step = &proposal.steps().head;
assert_eq!(
step.transparent_inputs().len(),
1,
"expected exactly one transparent input selected from the account's UTXOs",
);
assert_eq!(step.transparent_inputs()[0].outpoint(), &outpoint);
assert_eq!(step.transparent_inputs()[0].txout().value(), utxo_value);
assert_eq!(
step.balance().total(),
(utxo_value - transfer_amount).unwrap(),
);
assert!(step.balance().fee_required() > Zatoshis::ZERO);
assert!(!step.balance().proposed_change().is_empty());
}
pub fn propose_t2shielded_requires_transparent_regather<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = *uaddr.transparent().unwrap();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10_000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
let dust_value = Zatoshis::const_from_u64(10_000);
let n_dust = 30;
let height = st.wallet().chain_height().unwrap().unwrap();
for i in 0..n_dust {
let mut hash = [0u8; 32];
hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
let utxo = WalletTransparentOutput::from_parts(
OutPoint::new(hash, 0),
TxOut::new(dust_value, taddr.script().into()),
Some(height),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
}
let network = *st.network();
let recipient = ExtendedSpendingKey::master(&[1u8; 32])
.to_diversifiable_full_viewing_key()
.default_address()
.1;
let payment_amount = Zatoshis::const_from_u64(50_000);
let request = TransactionRequest::new(vec![Payment::without_memo(
Address::Sapling(recipient).to_zcash_address(&network),
payment_amount,
)])
.unwrap();
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let initial_gather = st
.wallet()
.select_spendable_transparent_outputs(
account.id(),
TargetHeight::from(height + 1),
ConfirmationsPolicy::MIN,
CoinbaseFilter::NonCoinbaseOnly,
None,
TargetValue::AtLeast(payment_amount),
usize::MAX,
&StandardFeeRule::Zip317,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.expect("initial gather should succeed");
let initial_gather_value: Zatoshis = initial_gather
.iter()
.map(|u| u.value())
.fold(Zatoshis::ZERO, |acc, v| (acc + v).unwrap());
let proposal = st
.propose_transfer_with_policy(
account.id(),
&input_selector,
&change_strategy,
request,
ConfirmationsPolicy::MIN,
&SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
)
.expect(
"transparent spend should succeed via the re-gather fallback despite the \
initial gather's fee estimate being insufficient",
);
let step = &proposal.steps().head;
let gathered_value: Zatoshis = step
.transparent_inputs()
.iter()
.map(|i| i.txout().value())
.fold(Zatoshis::ZERO, |acc, v| (acc + v).unwrap());
assert!(
gathered_value > initial_gather_value,
"the successful proposal should have gathered more transparent value ({}) than \
the insufficient initial gather ({})",
u64::from(gathered_value),
u64::from(initial_gather_value),
);
assert!(step.balance().fee_required() > Zatoshis::ZERO);
}
pub fn prefer_consolidation_accounts_for_selected_transparent_value<DSF>(
dsf: DSF,
cache: impl TestCache,
) where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let ufvk = account.account().ufvk().unwrap();
let sapling_fvk = ufvk.sapling().unwrap();
let (start_height, _, _) = st.generate_next_block(
&sapling_fvk,
AddressType::DefaultExternal,
Zatoshis::const_from_u64(600_000),
);
st.generate_next_block(
&sapling_fvk,
AddressType::DefaultExternal,
Zatoshis::const_from_u64(500_000),
);
st.scan_cached_blocks(start_height, 2);
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = *uaddr.transparent().unwrap();
let utxo = WalletTransparentOutput::from_parts(
OutPoint::fake(),
TxOut::new(Zatoshis::const_from_u64(600_000), taddr.script().into()),
st.wallet().chain_height().unwrap(),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
let recipient = ExtendedSpendingKey::master(&[1u8; 32])
.to_diversifiable_full_viewing_key()
.default_address()
.1;
let request = TransactionRequest::new(vec![Payment::without_memo(
Address::Sapling(recipient).to_zcash_address(st.network()),
Zatoshis::const_from_u64(1_000_000),
)])
.unwrap();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let spend_policy = SpendPolicy::default()
.with_transparent(TransparentSpendPolicy::any_account_addr())
.with_note_selection(NoteSelection::PreferConsolidation);
let proposal = st
.propose_transfer_with_policy(
account.id(),
&GreedyInputSelector::new(),
&change_strategy,
request,
ConfirmationsPolicy::MIN,
&spend_policy,
)
.expect("the mixed transparent and shielded inputs cover the payment and fee");
let step = &proposal.steps().head;
assert_eq!(step.transparent_inputs().len(), 1);
assert_eq!(
step.shielded_inputs()
.expect("the proposal spends a shielded note")
.notes()
.len(),
1,
);
}
pub fn propose_transfer_transparent_input_cap<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
const BLOCK_SPACE_PERCENT: u32 = 1;
const CAP: usize = 133;
const NUM_UTXOS: usize = CAP + 7; const DUST_VALUE: u64 = 10_000;
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = *uaddr.transparent().unwrap();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10_000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
let height = st.wallet().chain_height().unwrap().unwrap();
for i in 0..NUM_UTXOS {
let mut hash = [0u8; 32];
hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
let utxo = WalletTransparentOutput::from_parts(
OutPoint::new(hash, 0),
TxOut::new(Zatoshis::const_from_u64(DUST_VALUE), taddr.script().into()),
Some(height),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
}
let payment_amount = Zatoshis::const_from_u64(5_000 * (CAP as u64) + DUST_VALUE);
let network = *st.network();
let request = t2t_request(&network, payment_amount);
let input_selector =
GreedyInputSelector::new().with_shielding_block_space_percent(BLOCK_SPACE_PERCENT);
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let result = st.propose_transfer_with_policy(
account.id(),
&input_selector,
&change_strategy,
request,
ConfirmationsPolicy::MIN,
&SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
);
assert_matches!(
result,
Err(crate::data_api::error::Error::InsufficientFunds { .. }),
"the transparent gather should stop at the input cap rather than consuming every \
eligible dust UTXO, so the request should fail rather than succeed with an \
uncapped number of inputs",
);
}
pub fn propose_t2t_from_addresses<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
let external_taddrs = st
.wallet()
.get_transparent_receivers(account.id(), false, true)
.unwrap();
let mut taddrs_by_index = external_taddrs
.into_iter()
.filter_map(|(addr, meta)| meta.address_index().map(|i| (i, addr)))
.collect::<BTreeMap<_, _>>()
.into_values();
let addr_named = taddrs_by_index.next().expect("at least one external taddr");
let addr_other = taddrs_by_index
.next()
.expect("at least two external taddrs");
let named_value = Zatoshis::const_from_u64(100_000);
let other_value = Zatoshis::const_from_u64(150_000);
let height = st.wallet().chain_height().unwrap().unwrap();
let named_outpoint = OutPoint::new([1u8; 32], 0);
let other_outpoint = OutPoint::new([2u8; 32], 0);
for (addr, outpoint, value) in [
(addr_other, other_outpoint.clone(), other_value),
(addr_named, named_outpoint.clone(), named_value),
] {
let utxo = WalletTransparentOutput::from_parts(
outpoint,
TxOut::new(value, addr.script().into()),
Some(height),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
}
let network = *st.network();
let request = t2t_request(&network, Zatoshis::const_from_u64(40_000));
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling);
let proposal = st
.propose_transfer_with_policy(
account.id(),
&input_selector,
&change_strategy,
request,
ConfirmationsPolicy::MIN,
&SpendPolicy::default()
.with_transparent(TransparentSpendPolicy::from_one_address(addr_named)),
)
.expect("transparent spend from named address must succeed");
let step = &proposal.steps().head;
let selected: Vec<&OutPoint> = step
.transparent_inputs()
.iter()
.map(|i| i.outpoint())
.collect();
assert!(
selected.contains(&&named_outpoint),
"the named address's UTXO must be selected",
);
assert!(
!selected.contains(&&other_outpoint),
"an unnamed address's UTXO must not be selected",
);
}
pub fn value_bounded_transparent_gather<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
.unwrap()
.unwrap();
let taddr = *uaddr.transparent().unwrap();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10_000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
for _ in 1..10 {
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
}
st.scan_cached_blocks(start_height, 10);
let dust_value = Zatoshis::const_from_u64(10_000);
let n_dust = 50;
let height = st.wallet().chain_height().unwrap().unwrap();
for i in 0..n_dust {
let mut hash = [0u8; 32];
hash[..4].copy_from_slice(&(i as u32).to_le_bytes());
let utxo = WalletTransparentOutput::from_parts(
OutPoint::new(hash, 0),
TxOut::new(dust_value, taddr.script().into()),
Some(height),
Some(account.id()),
Some(TransparentKeyScope::EXTERNAL),
None,
)
.unwrap();
st.wallet_mut()
.put_received_transparent_utxo(&utxo)
.unwrap();
}
let target = Zatoshis::const_from_u64(30_000);
let target_height = TargetHeight::from(height + 1);
let bound = st
.wallet()
.select_spendable_transparent_outputs(
account.id(),
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
None,
TargetValue::AtLeast(target),
usize::MAX,
&StandardFeeRule::Zip317,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.expect("value-bounded gather should succeed");
assert!(
!bound.is_empty(),
"value-bounded gather should return at least one UTXO",
);
assert_eq!(
bound.len(),
6,
"value-bounded gather should return exactly 6 UTXOs (10_000 zats each) to cover \
30_000 zats net of the ZIP 317 marginal fee for 6 P2PKH inputs",
);
assert!(
bound.len() < n_dust,
"value-bounded gather should not return all {n_dust} UTXOs (returned {})",
bound.len(),
);
let total: Zatoshis = bound
.iter()
.map(|u| u.value())
.fold(Zatoshis::ZERO, |acc, v| (acc + v).unwrap());
assert!(
total >= target,
"value-bounded gather should cover the target (got {}, want >= {})",
u64::from(total),
u64::from(target),
);
let all = st
.wallet()
.select_spendable_transparent_outputs(
account.id(),
target_height,
ConfirmationsPolicy::MIN,
CoinbaseFilter::AllTransparentOutputs,
None,
TargetValue::AllFunds(MaxSpendMode::MaxSpendable),
usize::MAX,
&StandardFeeRule::Zip317,
LockFilter::Policy(&LockedInputPolicy::Exclude),
)
.expect("AllFunds gather should succeed");
assert_eq!(all.len(), n_dust);
}
pub fn reserve_next_n_internal_addresses_gap_limit<DSF>(
dsf: DSF,
cache: impl TestCache,
is_reached_gap_limit: impl Fn(
&<DSF::DataStore as crate::data_api::WalletRead>::Error,
DSF::AccountId,
u32,
) -> bool,
) where
DSF: DataStoreFactory,
{
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().cloned().unwrap().id();
let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
let not_our_value = Zatoshis::const_from_u64(10000);
let (start_height, _, _) =
st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
st.scan_cached_blocks(start_height, 1);
let reserved = st
.wallet_mut()
.reserve_next_n_internal_addresses(account_id, 3)
.unwrap();
assert_eq!(reserved.len(), 3);
for (i, (_, meta)) in reserved.iter().enumerate() {
assert_eq!(meta.scope(), Some(TransparentKeyScope::INTERNAL));
assert_eq!(
meta.address_index(),
Some(NonHardenedChildIndex::const_from_index(
u32::try_from(i).unwrap()
)),
);
}
let more = st
.wallet_mut()
.reserve_next_n_internal_addresses(account_id, 2)
.unwrap();
assert_eq!(more.len(), 2);
for (i, (_, meta)) in more.iter().enumerate() {
assert_eq!(meta.scope(), Some(TransparentKeyScope::INTERNAL));
assert_eq!(
meta.address_index(),
Some(NonHardenedChildIndex::const_from_index(
u32::try_from(reserved.len() + i).unwrap()
)),
);
}
let unique_addrs = reserved
.iter()
.chain(more.iter())
.map(|(a, _)| *a)
.collect::<HashSet<_>>();
assert_eq!(unique_addrs.len(), reserved.len() + more.len());
assert_matches!(
st.wallet_mut().reserve_next_n_internal_addresses(account_id, 1),
Err(e) if is_reached_gap_limit(&e, account_id, 5)
);
let ephemeral = st
.wallet_mut()
.reserve_next_n_ephemeral_addresses(account_id, 1)
.unwrap();
assert_eq!(ephemeral[0].1.scope(), Some(TransparentKeyScope::EPHEMERAL),);
assert_eq!(
ephemeral[0].1.address_index(),
Some(NonHardenedChildIndex::const_from_index(0)),
);
}
pub fn propose_t2t_with_transparent_change<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let utxo_value = Zatoshis::const_from_u64(100_000);
let transfer_amount = Zatoshis::const_from_u64(40_000);
let (mut st, account, outpoint) = setup_transparent_only_account(dsf, cache, utxo_value);
let network = *st.network();
let request = t2t_request(&network, transfer_amount);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling)
.with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
let proposal = st
.propose_transfer_with_policy(
account.id(),
&input_selector,
&change_strategy,
request,
ConfirmationsPolicy::MIN,
&SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
)
.expect("t->t proposal with transparent change must succeed");
assert_eq!(proposal.steps().len(), 1);
let step = &proposal.steps().head;
assert_eq!(step.transparent_inputs().len(), 1);
assert_eq!(step.transparent_inputs()[0].outpoint(), &outpoint);
assert!(step.shielded_inputs().is_none());
let expected_fee = Zatoshis::const_from_u64(10_000);
let expected_change = ((utxo_value - transfer_amount).unwrap() - expected_fee).unwrap();
assert_eq!(step.balance().fee_required(), expected_fee);
assert_eq!(
step.balance().proposed_change(),
[ChangeValue::transparent(expected_change)],
);
assert!(!step.balance().proposed_change()[0].is_ephemeral());
super::check_proposal_serialization_roundtrip(&network, st.wallet(), &proposal);
let txids = st
.create_proposed_transactions::<Infallible, _, Infallible, _>(
account.usk(),
OvkPolicy::Sender,
&proposal,
)
.expect("transaction creation must succeed");
assert_eq!(txids.len(), 1);
let txid = txids.head;
let tx = st
.wallet()
.get_transaction(txid)
.unwrap()
.expect("the created transaction is retrievable");
assert!(tx.sapling_bundle().is_none());
#[cfg(feature = "orchard")]
assert!(tx.orchard_bundle().is_none());
let bundle = tx
.transparent_bundle()
.expect("the transaction has a transparent bundle");
assert_eq!(bundle.vin.len(), 1);
assert_eq!(bundle.vout.len(), 2);
let payment_recipient = TransparentAddress::PublicKeyHash([7u8; 20]);
let change_outputs: Vec<_> = bundle
.vout
.iter()
.filter(|out| out.recipient_address() != Some(payment_recipient))
.collect();
assert_eq!(change_outputs.len(), 1);
let change_output = change_outputs[0];
assert_eq!(change_output.value(), expected_change);
let change_address = change_output
.recipient_address()
.expect("the change output pays a standard P2PKH address");
let receivers = st
.wallet()
.get_transparent_receivers(account.id(), true, false)
.unwrap();
let change_meta = receivers
.get(&change_address)
.expect("the change address belongs to the spending account");
assert_eq!(change_meta.scope(), Some(TransparentKeyScope::INTERNAL));
let cur_height = st.wallet().chain_height().unwrap().unwrap();
assert_matches!(
change_meta.exposure(),
Exposure::Exposed { at_height, .. } if at_height == cur_height
);
let (h, _) = st.generate_next_block_including(txid);
st.scan_cached_blocks(h, 1);
let mut expected_balance = Balance::ZERO;
expected_balance
.add_spendable_value(expected_change)
.unwrap();
check_balance::<DSF>(
&st,
&account,
&change_address,
ConfirmationsPolicy::MIN,
&expected_balance,
);
}
pub fn propose_t2t_transparent_change_exact_match<DSF>(dsf: DSF, cache: impl TestCache)
where
DSF: DataStoreFactory,
{
let utxo_value = Zatoshis::const_from_u64(50_000);
let transfer_amount = Zatoshis::const_from_u64(40_000);
let (mut st, account, _outpoint) = setup_transparent_only_account(dsf, cache, utxo_value);
let network = *st.network();
let request = t2t_request(&network, transfer_amount);
let input_selector = GreedyInputSelector::new();
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Sapling)
.with_transparent_change_policy(TransparentChangePolicy::TransparentChangeAllowed);
let proposal = st
.propose_transfer_with_policy(
account.id(),
&input_selector,
&change_strategy,
request,
ConfirmationsPolicy::MIN,
&SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
)
.expect("exactly-balanced t->t proposal must succeed");
assert_eq!(proposal.steps().len(), 1);
let step = &proposal.steps().head;
assert_eq!(
step.balance().fee_required(),
Zatoshis::const_from_u64(10_000),
);
assert_eq!(step.balance().proposed_change(), []);
}