use std::ops::Range;
use rusqlite::{Connection, params};
use zcash_client_backend::data_api::{scanning::ScanPriority, transparent_ledger::ChainPoint};
use zcash_protocol::consensus::{BlockHeight, NetworkUpgrade, Parameters};
use super::recovery;
use crate::wallet::one_time_ivk::{account_key, corrupt, queue_rescan, restore_start};
use crate::{AccountUuid, error::SqliteClientError, wallet, wallet::scanning::priority_code};
pub(in crate::wallet::one_time_ivk) fn retain_spend_history<P: Parameters>(
conn: &Connection,
params: &P,
account: AccountUuid,
) -> Result<(), SqliteClientError> {
let (id, _) = account_key(conn, params, account)?;
conn.execute(
"INSERT OR IGNORE INTO ironwood_one_time_spend_retention
(account_id, nullifier_retention_height)
SELECT id, MAX(birthday_height, ?2) FROM accounts WHERE id = ?1",
params![
id.0,
params
.activation_height(NetworkUpgrade::Nu6_3)
.map(u32::from)
.unwrap_or(0)
],
)?;
Ok(())
}
pub(crate) fn finish_nullifier_recovery<P: Parameters>(
conn: &rusqlite::Transaction<'_>,
params: &P,
account: AccountUuid,
through: ChainPoint,
now: i64,
) -> Result<bool, SqliteClientError> {
let (id, _) = account_key(conn, params, account)?;
let enabled: bool = conn.query_row(
"SELECT EXISTS(SELECT 1 FROM ironwood_one_time_spend_retention WHERE account_id = ?1)",
[id.0],
|r| r.get(0),
)?;
if !enabled
|| wallet::fully_scanned_height(conn)? != Some(through.height)
|| wallet::chain_tip_height(conn)? != Some(through.height)
|| wallet::get_block_hash(conn, through.height)? != Some(through.hash)
{
return Ok(false);
}
recovery::maintain_restore_discovery(conn, params, account, now)?;
if recovery::refund_memos_pending(conn, account)? {
return Ok(false);
}
let pending: Option<u32> = conn.query_row(
"SELECT MIN(h) FROM (
SELECT k.scan_from AS h FROM ironwood_one_time_sweeps s
JOIN ironwood_one_time_keys k ON k.id = s.one_time_key_id
WHERE k.account_id = ?1 AND s.done_height IS NULL
UNION ALL SELECT p.height FROM ironwood_one_time_payment_recovery p
JOIN ironwood_one_time_keys k ON k.id = p.one_time_key_id
WHERE k.account_id = ?1)",
[id.0],
|r| r.get(0),
)?;
let next = pending.unwrap_or(u32::from(through.height).saturating_add(1));
conn.execute(
"UPDATE ironwood_one_time_spend_retention SET nullifier_retention_height = ?2,
replay_through = CASE WHEN ?2 > nullifier_retention_height
THEN NULL ELSE replay_through END
WHERE account_id = ?1",
params![id.0, next],
)?;
let prune_below = through.height.saturating_sub(crate::PRUNING_DEPTH);
wallet::prune_nullifier_map(conn, prune_below)?;
Ok(next > u32::from(through.height))
}
pub(in crate::wallet::one_time_ivk) fn queue_spend_history<P: Parameters>(
conn: &rusqlite::Transaction<'_>,
params: &P,
account: AccountUuid,
from: BlockHeight,
through: BlockHeight,
) -> Result<(), SqliteClientError> {
let (id, _) = account_key(conn, params, account)?;
let from = from.max(restore_start(conn, params, id)?);
let end = BlockHeight::from(
u32::from(through)
.checked_add(1)
.ok_or_else(|| corrupt("spend recovery height overflow"))?,
);
if from >= end {
return Err(corrupt("empty spend recovery interval"));
}
conn.execute(
"INSERT INTO ironwood_one_time_spend_retention
(account_id, nullifier_retention_height, replay_through)
VALUES (?1, ?2, ?3) ON CONFLICT (account_id) DO UPDATE SET
nullifier_retention_height =
MIN(nullifier_retention_height, excluded.nullifier_retention_height),
replay_through = MAX(COALESCE(replay_through, 0), excluded.replay_through)",
params![id.0, u32::from(from), u32::from(through)],
)?;
for range in unreplayed(conn, from..end)? {
queue_rescan(conn, range)?;
}
Ok(())
}
fn unreplayed(
conn: &Connection,
range: Range<BlockHeight>,
) -> Result<Vec<Range<BlockHeight>>, SqliteClientError> {
let (start, end) = (u32::from(range.start), u32::from(range.end));
let mut covered: Vec<(u32, u32)> = conn
.prepare(
"SELECT height, height + 1 FROM ironwood_nullifier_scan_blocks
WHERE height >= ?1 AND height < ?2
UNION ALL
SELECT MAX(block_range_start, ?1), MIN(block_range_end, ?2) FROM scan_queue
WHERE priority > ?3 AND block_range_end > ?1 AND block_range_start < ?2",
)?
.query_map(
params![start, end, priority_code(&ScanPriority::Scanned)],
|r| Ok((r.get(0)?, r.get(1)?)),
)?
.collect::<Result<_, _>>()?;
covered.sort_unstable();
let mut missing = Vec::new();
let mut next = start;
for (from, to) in covered {
if from > next {
missing.push(BlockHeight::from(next)..BlockHeight::from(from));
}
next = next.max(to);
}
if next < end {
missing.push(BlockHeight::from(next)..BlockHeight::from(end));
}
Ok(missing)
}