use std::ops::Range;
use rusqlite::{Connection, named_params};
use zcash_client_backend::data_api::ScannedBlock;
use zcash_protocol::consensus::BlockHeight;
use crate::{AccountUuid, error::SqliteClientError};
pub(crate) struct Entry {
height: BlockHeight,
tx_index: u16,
txid: [u8; 32],
nullifiers: Vec<[u8; 32]>,
}
pub(crate) struct Batch {
entries: Vec<Entry>,
coverage: Vec<(AccountUuid, Range<BlockHeight>)>,
}
pub(crate) fn collect(blocks: &[ScannedBlock<AccountUuid>]) -> Batch {
let entries = blocks
.iter()
.flat_map(|block| {
block.transactions().iter().filter_map(move |tx| {
let nullifiers = tx.ironwood_nullifiers().filter(|nfs| !nfs.is_empty())?;
Some(Entry {
height: block.height(),
tx_index: tx.block_index().into(),
txid: *tx.txid().as_ref(),
nullifiers: nullifiers.iter().map(|nf| nf.to_bytes()).collect(),
})
})
})
.collect();
let mut coverage: Vec<(AccountUuid, Range<BlockHeight>)> = vec![];
for block in blocks {
for account in block.scanned_accounts() {
match coverage
.iter_mut()
.rev()
.find(|(a, range)| a == account && range.end == block.height())
{
Some((_, range)) => range.end = block.height() + 1,
None => coverage.push((*account, block.height()..block.height() + 1)),
}
}
}
Batch { entries, coverage }
}
pub(crate) fn record(
conn: &rusqlite::Transaction<'_>,
tpir_session: &crate::wallet::transparent_ledger::session::SessionState,
batch: Batch,
) -> Result<(), SqliteClientError> {
let mut entry = conn.prepare_cached(
"INSERT OR REPLACE INTO ironwood_spend_journal (block_height, tx_index, txid)
VALUES (:height, :tx_index, :txid)",
)?;
let mut clear = conn.prepare_cached(
"DELETE FROM ironwood_spend_journal_nullifiers
WHERE block_height = :height AND tx_index = :tx_index",
)?;
let mut nullifier = conn.prepare_cached(
"INSERT INTO ironwood_spend_journal_nullifiers
(block_height, tx_index, action_index, nf)
VALUES (:height, :tx_index, :action_index, :nf)",
)?;
for e in batch.entries {
link_spends(conn, tpir_session, &e)?;
let height = u32::from(e.height);
entry.execute(named_params! {
":height": height,
":tx_index": e.tx_index,
":txid": e.txid,
})?;
clear.execute(named_params! {":height": height, ":tx_index": e.tx_index})?;
for (action_index, nf) in e.nullifiers.iter().enumerate() {
nullifier.execute(named_params! {
":height": height,
":tx_index": e.tx_index,
":action_index": action_index,
":nf": nf,
})?;
}
}
for (account, range) in batch.coverage {
cover(conn, account, range)?;
}
Ok(())
}
fn link_spends(
conn: &rusqlite::Transaction<'_>,
tpir_session: &crate::wallet::transparent_ledger::session::SessionState,
entry: &Entry,
) -> Result<(), SqliteClientError> {
let mut known =
conn.prepare_cached("SELECT EXISTS(SELECT 1 FROM ironwood_received_notes WHERE nf = ?1)")?;
let spends: Vec<orchard::note::Nullifier> = entry
.nullifiers
.iter()
.filter_map(|nf| match known.query_row([nf], |r| r.get::<_, bool>(0)) {
Ok(true) => Option::from(orchard::note::Nullifier::from_bytes(nf)).map(Ok),
Ok(false) => None,
Err(e) => Some(Err(e)),
})
.collect::<Result<_, _>>()?;
if spends.is_empty() {
return Ok(());
}
let tx_ref: crate::TxRef = conn.query_row(
"SELECT id_tx FROM transactions WHERE txid = ?1",
[entry.txid],
|r| r.get(0).map(crate::TxRef),
)?;
for nf in spends {
let inserted = crate::wallet::orchard::mark_ironwood_note_spent(conn, tx_ref, &nf)?;
crate::wallet::ironwood_hooks::put_received_note_spend(
conn,
tpir_session,
zcash_protocol::ShieldedPool::Ironwood,
tx_ref,
usize::from(inserted),
)?;
}
Ok(())
}
fn cover(
conn: &Connection,
account: AccountUuid,
range: Range<BlockHeight>,
) -> Result<(), SqliteClientError> {
let params = named_params! {
":uuid": account.0,
":start": u32::from(range.start),
":end": u32::from(range.end),
};
conn.execute(
"INSERT INTO ironwood_journal_coverage (account_id, start_height, end_height)
SELECT a.id,
MIN(:start, IFNULL(MIN(c.start_height), :start)),
MAX(:end, IFNULL(MAX(c.end_height), :end))
FROM accounts a
LEFT JOIN ironwood_journal_coverage c
ON c.account_id = a.id AND c.start_height <= :end AND c.end_height >= :start
WHERE a.uuid = :uuid
GROUP BY a.id
ON CONFLICT (account_id, start_height) DO UPDATE SET end_height = excluded.end_height",
params,
)?;
conn.execute(
"DELETE FROM ironwood_journal_coverage AS c
WHERE c.account_id = (SELECT id FROM accounts WHERE uuid = :uuid)
AND EXISTS (
SELECT 1 FROM ironwood_journal_coverage m
WHERE m.account_id = c.account_id
AND m.start_height <= c.start_height AND m.end_height >= c.end_height
AND (m.start_height, m.end_height) != (c.start_height, c.end_height)
)",
named_params! {":uuid": account.0},
)?;
Ok(())
}