use rusqlite::named_params;
use zcash_client_backend::data_api::transparent_ledger::{
AggregatePayment, EffectCompleteness, FeeState, HistoryClassification, PoolEffect,
TransactionMetadataEvidence, WholeTransactionFee,
};
use zcash_primitives::transaction::TxId;
use zcash_protocol::{PoolType, value::Zatoshis};
use crate::error::SqliteClientError;
use super::evidence::TransactionFacts;
pub(super) fn whole_fee(
stored: Option<Zatoshis>,
metadata: Option<&TransactionMetadataEvidence>,
) -> Option<Zatoshis> {
match (stored, metadata.map(|e| e.metadata.fee)) {
(Some(stored), Some(WholeTransactionFee::Exact(fee))) => (stored == fee).then_some(stored),
(Some(_), Some(WholeTransactionFee::NotApplicable)) => None,
(Some(stored), _) => Some(stored),
(None, Some(WholeTransactionFee::Exact(fee))) => Some(fee),
(None, _) => None,
}
}
fn has_other_outgoing_evidence(
conn: &rusqlite::Connection,
account_id: i64,
transaction_id: i64,
) -> Result<bool, SqliteClientError> {
Ok(conn.query_row(
"SELECT EXISTS (
SELECT 1 FROM sent_notes s
WHERE s.from_account_id = :account_id AND s.transaction_id = :transaction_id
AND NOT EXISTS (
SELECT 1 FROM v_received_outputs ro
WHERE ro.account_id = :account_id AND ro.transaction_id = s.transaction_id
AND ro.pool = s.output_pool AND ro.output_index = s.output_index
)
) OR EXISTS (
SELECT 1 FROM v_received_outputs ro
JOIN v_received_output_spends ros
ON ros.pool = ro.pool AND ros.received_output_id = ro.id_within_pool_table
WHERE ros.transaction_id = :transaction_id AND ro.account_id != :account_id
)",
named_params![":account_id": account_id, ":transaction_id": transaction_id],
|row| row.get(0),
)?)
}
#[cfg(feature = "transparent-inputs")]
fn has_ledger_spend(conn: &rusqlite::Connection, txid: &TxId) -> Result<bool, SqliteClientError> {
Ok(conn.query_row(
"SELECT EXISTS (
SELECT 1 FROM tpir_spend_events s
JOIN tpir_active_accounts a ON a.account_id = s.account_id
JOIN transactions t ON t.txid = s.spending_txid AND t.mined_height = s.mined_height
WHERE s.spending_txid = :txid
AND NOT EXISTS (SELECT 1 FROM tpir_quarantined_accounts qa
WHERE qa.account_id = s.account_id)
AND EXISTS (
SELECT 1 FROM tpir_spend_observations o
JOIN tpir_qualified_revisions q ON q.revision_id = o.revision_id
JOIN tpir_revisions r ON r.id = o.revision_id
WHERE o.spend_id = s.id
AND NOT EXISTS (SELECT 1 FROM tpir_quarantined_sources qs
WHERE qs.source = r.source)
)
)",
named_params![":txid": txid.as_ref()],
|row| row.get(0),
)?)
}
pub(super) fn inferred_outgoing(
conn: &rusqlite::Connection,
account_id: i64,
transaction_id: i64,
#[cfg_attr(not(feature = "transparent-inputs"), allow(unused_variables))] txid: &TxId,
effects: &[PoolEffect],
fee: Zatoshis,
metadata: Option<&TransactionMetadataEvidence>,
) -> Result<Option<Zatoshis>, SqliteClientError> {
if metadata.is_some_and(|e| e.metadata.transparent_input_count != 0) {
return Ok(None);
}
let mut spent = Zatoshis::ZERO;
let mut received = Zatoshis::ZERO;
for effect in effects {
match effect.pool {
PoolType::Transparent if effect.spent != Zatoshis::ZERO => return Ok(None),
PoolType::Transparent => {}
PoolType::Shielded(_) if effect.completeness != EffectCompleteness::Complete => {
return Ok(None);
}
PoolType::Shielded(_) => {
let (Some(s), Some(r)) = (spent + effect.spent, received + effect.received) else {
return Ok(None);
};
spent = s;
received = r;
}
}
}
#[cfg(feature = "transparent-inputs")]
if has_ledger_spend(conn, txid)? {
return Ok(None);
}
if has_other_outgoing_evidence(conn, account_id, transaction_id)? {
return Ok(None);
}
Ok((spent - received)
.and_then(|net| net - fee)
.filter(|outgoing| *outgoing > Zatoshis::ZERO))
}
pub(super) fn is_private_shielding(
tx_facts: &TransactionFacts,
effects: &[PoolEffect],
metadata: Option<&TransactionMetadataEvidence>,
owned_inputs: u32,
sent_elsewhere: u64,
) -> bool {
let Some(metadata) = metadata.map(|e| e.metadata) else {
return false;
};
let WholeTransactionFee::Exact(fee) = metadata.fee else {
return false;
};
let fees_agree = tx_facts.fee.is_none_or(|known| known == fee);
let spent: u64 = effects.iter().map(|e| e.spent.into_u64()).sum();
let received: u64 = effects.iter().map(|e| e.received.into_u64()).sum();
let balanced = Some(spent) == received.checked_add(fee.into_u64());
let shape = effects.iter().all(|e| {
e.completeness == EffectCompleteness::Complete
&& match e.pool {
PoolType::Transparent => e.received == Zatoshis::ZERO && e.spent > Zatoshis::ZERO,
PoolType::Shielded(_) => e.spent == Zatoshis::ZERO,
}
}) && effects
.iter()
.any(|e| matches!(e.pool, PoolType::Shielded(_)) && e.received > Zatoshis::ZERO);
metadata.has_shielded_components
&& owned_inputs > 0
&& owned_inputs == metadata.transparent_input_count
&& fees_agree
&& balanced
&& tx_facts.has_transparent_outputs == Some(false)
&& shape
&& sent_elsewhere == 0
}
pub(super) fn inferred_payment(
transaction_metadata: Option<&TransactionMetadataEvidence>,
transparent_effect: Option<&PoolEffect>,
sole_transparent_funding: bool,
) -> Option<Zatoshis> {
transaction_metadata.and_then(|e| {
let WholeTransactionFee::Exact(fee) = e.metadata.fee else {
return None;
};
let effect = transparent_effect?;
if !sole_transparent_funding || effect.completeness != EffectCompleteness::Complete {
return None;
}
effect
.spent
.into_u64()
.checked_sub(effect.received.into_u64())?
.checked_sub(fee.into_u64())
.and_then(|v| Zatoshis::from_u64(v).ok())
})
}
pub(super) fn aggregate_payment(
tx_facts: &TransactionFacts,
inferred_payment: Option<Zatoshis>,
recorded_sent: u64,
) -> Result<AggregatePayment, SqliteClientError> {
Ok(if tx_facts.constructed {
AggregatePayment::Exact(
Zatoshis::from_u64(recorded_sent)
.map_err(|e| SqliteClientError::CorruptedData(e.to_string()))?,
)
} else if let Some(amount) = inferred_payment {
AggregatePayment::Exact(amount)
} else if recorded_sent > 0 {
AggregatePayment::Partial(
Zatoshis::from_u64(recorded_sent)
.map_err(|e| SqliteClientError::CorruptedData(e.to_string()))?,
)
} else {
AggregatePayment::Unknown
})
}
pub(super) fn account_fee(
tx_facts: &TransactionFacts,
spent: u64,
transaction_metadata: Option<&TransactionMetadataEvidence>,
sole_transparent_funding: bool,
inferred_payment: Option<Zatoshis>,
received_only: bool,
) -> FeeState {
match tx_facts.fee {
Some(fee)
if spent > 0
&& (tx_facts.constructed
|| (!tx_facts.mixed_without_full_data
&& (transaction_metadata.is_none() || sole_transparent_funding))) =>
{
FeeState::Known(fee)
}
_ if sole_transparent_funding && inferred_payment.is_some() => {
match transaction_metadata.unwrap().metadata.fee {
WholeTransactionFee::Exact(fee) => FeeState::Known(fee),
_ => FeeState::Unknown,
}
}
_ if received_only => FeeState::NotApplicable,
_ => FeeState::Unknown,
}
}
pub(super) fn classification(
tx_facts: &TransactionFacts,
received_only: bool,
payments_accounted: bool,
inferred_payment: Option<Zatoshis>,
net_shielding: bool,
) -> HistoryClassification {
if tx_facts.created_locally {
HistoryClassification::LocalIntent
} else if received_only || payments_accounted || inferred_payment.is_some() {
HistoryClassification::Reconstructed
} else if net_shielding {
HistoryClassification::NetReconstructed
} else {
HistoryClassification::Provisional
}
}