use zcash_client_backend::data_api::transparent_ledger::TransparentLedgerMode;
use crate::{
error::{AuthorityRefusal, RefusalReason, SqliteClientError},
wallet::chain_tip_height,
};
use zcash_client_backend::data_api::transparent_ledger::AppliedTransparentPolicy;
use super::policy::{durable_policy, resolve_mode};
#[cfg(feature = "transparent-inputs")]
use {
super::recovery,
crate::{AccountRef, AccountUuid},
zcash_client_backend::data_api::{
transparent_ledger::{AccountLifecycle, CandidateBlocker, RecoveryBlocker},
wallet::TargetHeight,
},
zcash_keys::keys::transparent::gap_limits::GapLimits,
zcash_protocol::consensus::{self, BlockHeight},
};
pub(crate) fn grants_private_authority(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<bool, SqliteClientError> {
let configured = configured.ok_or(SqliteClientError::TransparentLedgerModeNotConfigured)?;
Ok(configured == TransparentLedgerMode::PrivateRequired
&& durable_policy(conn)?.map(|policy| policy.mode)
== Some(TransparentLedgerMode::PrivateRequired))
}
pub(crate) fn refused(reason: RefusalReason) -> SqliteClientError {
SqliteClientError::TransparentAuthorityUnavailable(AuthorityRefusal::Refused(reason))
}
pub(crate) fn check_transparent_authority(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<(), SqliteClientError> {
match resolve_mode(conn, configured)? {
TransparentLedgerMode::PrivateRequired => Err(refused(RefusalReason::NoAuthority)),
TransparentLedgerMode::Public => {
if cfg!(not(feature = "transparent-inputs")) {
Err(refused(RefusalReason::NoAuthority))
} else if chain_tip_height(conn)?.is_none() {
Err(refused(RefusalReason::ChainUnknown))
} else {
Ok(())
}
}
}
}
#[cfg(feature = "transparent-inputs")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum InputAuthority {
Public,
Private(Vec<AccountRef>),
}
#[cfg(feature = "transparent-inputs")]
impl InputAuthority {
pub(crate) fn sql_params(&self) -> (bool, std::rc::Rc<Vec<rusqlite::types::Value>>) {
match self {
Self::Public => (false, std::rc::Rc::new(vec![])),
Self::Private(accounts) => (
true,
std::rc::Rc::new(
accounts
.iter()
.map(|a| rusqlite::types::Value::Integer(a.0))
.collect(),
),
),
}
}
pub(crate) fn admits(&self, account: AccountRef) -> bool {
match self {
Self::Public => true,
Self::Private(accounts) => accounts.contains(&account),
}
}
pub(crate) fn admits_output(&self, account: AccountRef, placed: bool) -> bool {
match self {
Self::Public => true,
Self::Private(accounts) => placed && accounts.contains(&account),
}
}
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn input_authority<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
gap_limits: &GapLimits,
configured: Option<TransparentLedgerMode>,
target: TargetHeight,
) -> Result<InputAuthority, SqliteClientError> {
if resolve_mode(conn, configured)?.retains_public_authority() {
check_transparent_authority(conn, configured)?;
return Ok(InputAuthority::Public);
}
let Some(tip) = chain_tip_height(conn)? else {
return Err(refused(RefusalReason::ChainUnknown));
};
if BlockHeight::from(target) != tip + 1 {
return Ok(InputAuthority::Private(vec![]));
}
let mut stmt = conn.prepare_cached(
"SELECT a.uuid FROM tpir_active_accounts t JOIN accounts a ON a.id = t.account_id",
)?;
let active = stmt
.query_map([], |row| row.get(0).map(AccountUuid))?
.collect::<Result<Vec<_>, _>>()?;
let mut eligible = vec![];
for account in active {
let ledger = recovery::account_ledger(conn, params, gap_limits, account)?;
if ledger.authorizes_after(tip) {
eligible.push(ledger.account_ref);
}
}
Ok(InputAuthority::Private(eligible))
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn private_exclusion<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
gap_limits: &GapLimits,
account: AccountUuid,
target: TargetHeight,
) -> Result<Option<RefusalReason>, SqliteClientError> {
let Some(tip) = chain_tip_height(conn)? else {
return Ok(Some(RefusalReason::ChainUnknown));
};
let target = BlockHeight::from(target);
if target <= tip {
return Ok(Some(RefusalReason::StaleTarget));
}
let ledger = recovery::account_ledger(conn, params, gap_limits, account)?;
if ledger.lifecycle != AccountLifecycle::Active || ledger.recovery_suspended {
return Ok(Some(RefusalReason::NotAdmitted));
}
let blockers = recovery::ledger_blockers(conn, &ledger, Some(tip))?;
let catching_up = blockers.iter().all(|blocker| {
matches!(
blocker,
RecoveryBlocker::Recovery(CandidateBlocker::IncompleteCoverage)
| RecoveryBlocker::ChainBehindTip
)
});
if !catching_up || (target == tip + 1 && blockers.is_empty()) {
return Ok(Some(RefusalReason::NotAdmitted));
}
Ok(None)
}
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn refuse_unadmitted_input<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
gap_limits: &GapLimits,
authority: &InputAuthority,
account: AccountUuid,
account_ref: AccountRef,
target: TargetHeight,
pending: &mut Vec<AccountUuid>,
) -> Result<(), SqliteClientError> {
if authority.admits(account_ref) {
return Err(refused(RefusalReason::NotInLedger));
}
exclude_or_defer(conn, params, gap_limits, account, target, pending)
}
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn refuse_unplaced_input<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
gap_limits: &GapLimits,
authority: &InputAuthority,
account: AccountUuid,
account_ref: AccountRef,
target: TargetHeight,
pending: &mut Vec<AccountUuid>,
) -> Result<(), SqliteClientError> {
if authority.admits(account_ref) {
return Err(refused(RefusalReason::Withdrawn));
}
exclude_or_defer(conn, params, gap_limits, account, target, pending)
}
#[cfg(feature = "transparent-inputs")]
fn exclude_or_defer<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
gap_limits: &GapLimits,
account: AccountUuid,
target: TargetHeight,
pending: &mut Vec<AccountUuid>,
) -> Result<(), SqliteClientError> {
match private_exclusion(conn, params, gap_limits, account, target)? {
Some(reason) => Err(refused(reason)),
None => {
if !pending.contains(&account) {
pending.push(account);
}
Ok(())
}
}
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn pending_result(pending: Vec<AccountUuid>) -> Result<(), SqliteClientError> {
if pending.is_empty() {
Ok(())
} else {
Err(SqliteClientError::TransparentAuthorityUnavailable(
AuthorityRefusal::Pending { accounts: pending },
))
}
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn check_address_owners<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
gap_limits: &GapLimits,
authority: &InputAuthority,
addresses: &[transparent::address::TransparentAddress],
target: TargetHeight,
) -> Result<(), SqliteClientError> {
let InputAuthority::Private(_) = authority else {
return Ok(());
};
let mut owners = vec![];
for address in addresses {
if let Some((account, _)) =
crate::wallet::transparent::find_account_uuid_for_transparent_address(
conn, params, address,
)?
{
owners.push((account, crate::wallet::get_account_ref(conn, account)?));
}
}
if owners.is_empty() || owners.iter().any(|(_, owner)| authority.admits(*owner)) {
return Ok(());
}
let mut pending = vec![];
let mut reason = None;
for (account, _) in owners {
match private_exclusion(conn, params, gap_limits, account, target)? {
None if !pending.contains(&account) => pending.push(account),
None => {}
Some(refusal) => {
reason.get_or_insert(refusal);
}
}
}
match reason {
Some(reason) if pending.is_empty() => Err(refused(reason)),
_ => pending_result(pending),
}
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn public_discovery_permitted(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<bool, SqliteClientError> {
Ok(resolve_mode(conn, configured)?.retains_public_authority())
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn check_public_discovery(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<(), SqliteClientError> {
if public_discovery_permitted(conn, configured)? {
Ok(())
} else {
Err(SqliteClientError::PublicTransparentDiscoveryForbidden)
}
}
pub(crate) fn transparent_funds_current(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<bool, SqliteClientError> {
Ok(transparent_funds_refusal(conn, configured)?.is_none())
}
pub(crate) fn transparent_funds_refusal(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<Option<RefusalReason>, SqliteClientError> {
let durable_private = matches!(
durable_policy(conn)?,
Some(AppliedTransparentPolicy {
mode: TransparentLedgerMode::PrivateRequired,
..
})
);
Ok(
if cfg!(not(feature = "transparent-inputs"))
|| durable_private
|| configured == Some(TransparentLedgerMode::PrivateRequired)
{
Some(RefusalReason::NoAuthority)
} else if chain_tip_height(conn)?.is_none() {
Some(RefusalReason::ChainUnknown)
} else {
None
},
)
}
pub(crate) fn has_transparent_inputs(tx: &zcash_primitives::transaction::Transaction) -> bool {
tx.transparent_bundle()
.is_some_and(|bundle| !bundle.vin.is_empty())
}