use std::num::NonZeroU32;
use zcash_primitives::consensus::BlockHeight;
use zcash_primitives::transaction::TxId;
use zcash_primitives::transaction::fees::zip317::MARGINAL_FEE;
use zcash_protocol::PoolType;
use zcash_protocol::value::Zatoshis;
use super::LightWallet;
use super::error::WalletError;
use super::transaction::transaction_unspent_outputs;
use pepper_sync::wallet::NoteInterface;
use pepper_sync::wallet::OutputId;
use pepper_sync::wallet::OutputInterface;
use pepper_sync::wallet::TransparentCoin;
use pepper_sync::wallet::WalletTransaction;
use query::OutputQuery;
use query::OutputSpendStatusQuery;
use zingo_status::confirmation_status::ConfirmationStatus;
pub mod query;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct OutputRef {
output_id: OutputId,
pool_type: PoolType,
}
impl OutputRef {
pub fn new(output_id: OutputId, pool_type: PoolType) -> Self {
OutputRef {
output_id,
pool_type,
}
}
pub fn output_id(&self) -> OutputId {
self.output_id
}
pub fn txid(&self) -> TxId {
self.output_id.txid()
}
pub fn output_index(&self) -> u16 {
self.output_id.output_index()
}
pub fn pool_type(&self) -> PoolType {
self.pool_type
}
}
impl std::fmt::Display for OutputRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{{
output id: {}
pool type: {}
}}",
self.output_id, self.pool_type
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpendStatus {
Unspent,
CalculatedSpent(TxId),
TransmittedSpent(TxId),
MempoolSpent(TxId),
Spent(TxId),
}
impl SpendStatus {
pub fn is_unspent(&self) -> bool {
matches!(self, Self::Unspent)
}
pub fn is_pending_spent(&self) -> bool {
matches!(self, Self::CalculatedSpent(_))
|| matches!(self, Self::TransmittedSpent(_))
|| matches!(self, Self::MempoolSpent(_))
}
pub fn is_confirmed_spent(&self) -> bool {
matches!(self, Self::Spent(_))
}
}
impl std::fmt::Display for SpendStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpendStatus::Unspent => write!(f, "unspent"),
SpendStatus::CalculatedSpent(txid) => write!(f, "calculated spent in {}", txid),
SpendStatus::TransmittedSpent(txid) => write!(f, "transmitted spent in {}", txid),
SpendStatus::MempoolSpent(txid) => write!(f, "mempool spent in {}", txid),
SpendStatus::Spent(txid) => write!(f, "confirmed spent in {}", txid),
}
}
}
impl LightWallet {
pub fn output_transaction(&self, output: &impl OutputInterface) -> &WalletTransaction {
self.wallet_transactions
.get(&output.output_id().txid())
.expect("transaction should exist in the wallet")
}
pub fn output_spend_status(&self, output: &impl OutputInterface) -> SpendStatus {
if let Some(txid) = output.spending_transaction() {
match self
.wallet_transactions
.get(&txid)
.expect("transaction should exist in the wallet")
.status()
{
ConfirmationStatus::Calculated(_) => SpendStatus::CalculatedSpent(txid),
ConfirmationStatus::Transmitted(_) => SpendStatus::TransmittedSpent(txid),
ConfirmationStatus::Mempool(_) => SpendStatus::MempoolSpent(txid),
ConfirmationStatus::Confirmed(_) => SpendStatus::Spent(txid),
}
} else {
SpendStatus::Unspent
}
}
pub fn wallet_outputs<Op: OutputInterface>(&self) -> Vec<&Op> {
self.wallet_transactions
.values()
.flat_map(|transaction| Op::transaction_outputs(transaction))
.collect()
}
pub fn sum_queried_output_values(&self, query: OutputQuery) -> u64 {
self.wallet_transactions
.values()
.fold(0, |acc, transaction| {
acc + self.sum_queried_transaction_output_values(transaction, query)
})
}
pub fn sum_queried_transaction_output_values(
&self,
transaction: &WalletTransaction,
query: OutputQuery,
) -> u64 {
let mut sum = 0;
if query.transparent() {
for output in transaction.transparent_coins().iter() {
if self.query_output_spend_status(query.spend_status, output) {
sum += output.value();
}
}
}
if query.sapling() {
for output in transaction.sapling_notes().iter() {
if self.query_output_spend_status(query.spend_status, output) {
sum += output.value();
}
}
}
if query.orchard() {
for output in transaction.orchard_notes().iter() {
if self.query_output_spend_status(query.spend_status, output) {
sum += output.value();
}
}
}
sum
}
fn query_output_spend_status(
&self,
query: OutputSpendStatusQuery,
output: &impl OutputInterface,
) -> bool {
if let Some(txid) = output.spending_transaction() {
match self
.wallet_transactions
.get(&txid)
.expect("transaction should exist in the wallet")
.status()
{
ConfirmationStatus::Confirmed(_) => query.spent,
_confirmation_pending if query.pending_spent => true,
_ => false,
}
} else {
query.unspent
}
}
pub(crate) fn spendable_notes<'a, N: NoteInterface>(
&'a self,
anchor_height: BlockHeight,
exclude: &'a [OutputId],
) -> Vec<&'a N> {
self.wallet_transactions
.values()
.flat_map(|transaction| {
if transaction
.status()
.is_confirmed_before_or_at(&anchor_height)
{
transaction_unspent_outputs::<N>(transaction, exclude).collect()
} else {
Vec::new()
}
})
.filter(|¬e| note.nullifier().is_some() && note.position().is_some())
.collect()
}
pub(crate) fn spendable_transparent_coins<'a>(
&'a self,
target_height: BlockHeight,
exclude: &'a [OutputId],
min_confirmations: NonZeroU32,
) -> Vec<&'a TransparentCoin> {
self.wallet_transactions
.values()
.filter(|&transaction| transaction.status().is_confirmed())
.flat_map(|transaction| {
if transaction
.status()
.get_confirmed_height()
.expect("transaction must be confirmed in this scope")
> self.sync_state.wallet_height().unwrap_or(self.birthday)
- min_confirmations.get()
+ 1
{
return Vec::new();
}
let additional_confirmations = transaction
.transaction()
.transparent_bundle()
.map_or(0, |bundle| if bundle.is_coinbase() { 100 } else { 0 });
if transaction
.status()
.is_confirmed_before_or_at(&(target_height - additional_confirmations))
{
transaction_unspent_outputs::<TransparentCoin>(transaction, exclude).collect()
} else {
Vec::new()
}
})
.collect()
}
pub(crate) fn select_spendable_notes_by_pool<'a, N: NoteInterface>(
&'a self,
remaining_value_needed: &mut RemainingNeeded,
anchor_height: BlockHeight,
exclude: &'a [OutputId],
) -> Result<Vec<&'a N>, WalletError> {
let target_value = match remaining_value_needed {
RemainingNeeded::Positive(value) => *value,
RemainingNeeded::GracelessChangeAmount(_) => return Ok(Vec::new()),
};
let mut selected_notes: Vec<&'a N> = Vec::new();
let mut unselected_notes = self.spendable_notes::<N>(anchor_height, exclude);
unselected_notes.sort_by_key(|&output| output.value());
let dust_index =
unselected_notes.partition_point(|output| output.value() <= MARGINAL_FEE.into_u64());
let _dust_notes = unselected_notes.drain(..dust_index).collect::<Vec<_>>();
let mut unselected_note_index = 0;
let mut total_selected_note_value: Zatoshis;
loop {
if unselected_notes.is_empty() {
break;
}
total_selected_note_value = Zatoshis::from_u64(
selected_notes
.iter()
.fold(0, |acc, output: &&N| acc + output.value()),
)?;
*remaining_value_needed =
calculate_remaining_needed(target_value, total_selected_note_value);
let updated_target_value = match remaining_value_needed {
RemainingNeeded::Positive(updated_target_value) => updated_target_value.into_u64(),
RemainingNeeded::GracelessChangeAmount(_change) => {
break;
}
};
match unselected_notes.get(unselected_note_index) {
Some(&smallest_unselected) => {
if smallest_unselected.value() >= updated_target_value {
selected_notes.push(smallest_unselected);
unselected_notes.remove(unselected_note_index);
} else {
unselected_note_index += 1;
}
}
None => {
selected_notes.push(unselected_notes.pop().expect("should be nonempty"));
unselected_note_index = 0;
}
}
}
Ok(selected_notes)
}
}
pub(crate) enum RemainingNeeded {
Positive(Zatoshis),
GracelessChangeAmount(Zatoshis),
}
fn calculate_remaining_needed(target_value: Zatoshis, selected_value: Zatoshis) -> RemainingNeeded {
if let Some(amount) = target_value - selected_value {
if amount == Zatoshis::ZERO {
RemainingNeeded::GracelessChangeAmount(Zatoshis::ZERO)
} else {
RemainingNeeded::Positive(amount)
}
} else {
RemainingNeeded::GracelessChangeAmount(
(selected_value - target_value).expect("This is guaranteed positive"),
)
}
}