use rusqlite::{Connection, named_params, params};
use zakura_one_time_ivk::swap::{
COMPLETION_LIMIT_SECS, Observation, OperationStatus, RESTORE_WATCH_SECS, ReceiptExpectation,
};
use zcash_client_backend::data_api::wallet::ConfirmationsPolicy;
use zcash_protocol::consensus::{BlockHeight, Parameters};
use super::{
RESERVED,
reservations::{STARTED, reap},
};
use crate::wallet::one_time_ivk::{RESTORED, account_key, invalid};
use crate::{
AccountUuid, error::SqliteClientError, wallet, wallet::common::tx_unexpired_condition,
};
pub(in crate::wallet::one_time_ivk) fn close_finished<P: Parameters>(
conn: &rusqlite::Transaction<'_>,
params: &P,
account: AccountUuid,
now: i64,
tip: BlockHeight,
) -> Result<usize, SqliteClientError> {
let (owner, _) = account_key(conn, params, account)?;
if wallet::chain_tip_height(conn)? != Some(tip)
|| wallet::fully_scanned_height(conn)? != Some(tip)
{
return Ok(0);
}
reap(conn, params, account, now)?;
let tip_time: i64 = conn.query_row(
"SELECT time FROM blocks WHERE height = ?1",
[u32::from(tip)],
|r| r.get(0),
)?;
let now = now.min(tip_time);
let confirmed_below = u32::from(tip)
.saturating_add(1)
.saturating_sub(ConfirmationsPolicy::default().untrusted().get());
let mut stmt = conn.prepare(&format!(
"SELECT k.id, k.registered_at, k.purpose = 1 AND {RESERVED},
EXISTS(SELECT 1 FROM ironwood_received_notes n
JOIN transactions t ON t.id_tx = n.transaction_id
WHERE n.one_time_key_id = k.id
AND (t.mined_height > :confirmed_below
OR (t.mined_height IS NULL AND ({unexpired})))),
({RESTORED}),
(SELECT COUNT(*) FROM ironwood_one_time_operations o
WHERE o.one_time_key_id = k.id),
(SELECT COUNT(*) FROM ironwood_one_time_operations o
WHERE o.one_time_key_id = k.id AND o.expectation IN (0, 3, 4)),
(SELECT MAX(o.deadline) FROM ironwood_one_time_operations o
WHERE o.one_time_key_id = k.id),
(SELECT COALESCE(SUM(COALESCE(o.expected_value, 1)), 0)
FROM ironwood_one_time_operations o
WHERE o.one_time_key_id = k.id AND o.expectation = 2),
(SELECT COALESCE(SUM(n.value), 0) FROM ironwood_received_notes n
JOIN transactions t ON t.id_tx = n.transaction_id
WHERE n.one_time_key_id = k.id AND t.mined_height <= :confirmed_below),
(SELECT b.time FROM ironwood_one_time_sweeps s
JOIN blocks b ON b.height = s.done_height
WHERE s.one_time_key_id = k.id),
{STARTED}
FROM ironwood_one_time_keys k
WHERE k.account_id = :account AND k.active_from IS NOT NULL AND k.closed_at IS NULL",
unexpired = tx_unexpired_condition("t"),
))?;
let bounds = named_params![
":account": owner.0,
":confirmed_below": confirmed_below,
":target_height": u32::from(tip + 1),
];
let finished = stmt
.query_map(bounds, |row| {
let id: i64 = row.get(0)?;
let registered_at: i64 = row.get(1)?;
let reserved: bool = row.get(2)?;
let pending_receipt: bool = row.get(3)?;
let restored: bool = row.get(4)?;
let operations: u32 = row.get(5)?;
let unresolved: u32 = row.get(6)?;
let deadline: Option<i64> = row.get(7)?;
let expected: i64 = row.get(8)?;
let received: i64 = row.get(9)?;
let swept_at: Option<i64> = row.get(10)?;
let started: bool = row.get(11)?;
let limit = if restored {
registered_at
.max(swept_at.unwrap_or(registered_at))
.saturating_add(RESTORE_WATCH_SECS)
} else {
deadline
.unwrap_or(registered_at)
.saturating_add(COMPLETION_LIMIT_SECS)
};
let settled = operations > 0 && unresolved == 0 && received >= expected;
let finished = if reserved {
started && now >= limit
} else {
settled || now >= limit
};
Ok((id, !pending_receipt && finished))
})?
.collect::<Result<Vec<_>, _>>()?;
let mut closed = 0;
for (id, finished) in finished {
if finished {
conn.execute(
"UPDATE ironwood_one_time_keys SET closed_at = ?2 WHERE id = ?1",
params![id, now],
)?;
closed += 1;
}
}
Ok(closed)
}
pub(in crate::wallet::one_time_ivk) fn record(
conn: &Connection,
id: i64,
observation: Observation,
now: i64,
) -> Result<bool, SqliteClientError> {
if now < 0 {
return Err(invalid("invalid observation time"));
}
let (expectation, amount) = match observation.status {
OperationStatus::Active => (0, None),
OperationStatus::Terminal(ReceiptExpectation::None) => (1, None),
OperationStatus::Terminal(ReceiptExpectation::Positive(value)) => (2, value),
OperationStatus::Terminal(ReceiptExpectation::Unknown) => (3, None),
OperationStatus::AwaitingDeposit => (4, None),
};
let amount = amount
.map(|v| i64::try_from(u64::from(v)).ok().filter(|v| *v > 0))
.map(|v| v.ok_or_else(|| invalid("expected receipt must be positive")))
.transpose()?;
conn.execute(
"UPDATE ironwood_one_time_operations SET funded = MAX(funded, ?2) WHERE id = ?1",
params![id, observation.funded],
)?;
let current = conn.execute(
"UPDATE ironwood_one_time_operations SET
observed_at = ?2, expectation = ?3, expected_value = ?4
WHERE id = ?1 AND (observed_at < ?2 OR (observed_at = ?2 AND NOT (
(expectation = 2 AND (?3 != 2 OR COALESCE(?4, 1) < COALESCE(expected_value, 1)))
OR (expectation = 3 AND ?3 = 1))))",
params![id, now, expectation, amount],
)?;
Ok(current > 0)
}