use rusqlite::{Connection, params};
use zakura_one_time_ivk::swap::{REFUND_MEMO_MAGIC, RefundMemo};
use zcash_protocol::consensus::{BlockHeight, Parameters};
use crate::wallet::one_time_ivk::{Discovery, KeyId, Purpose, account_key, invalid, register};
use crate::{AccountUuid, error::SqliteClientError, wallet};
pub(in crate::wallet::one_time_ivk) fn recheck<P: Parameters>(
conn: &rusqlite::Transaction<'_>,
params: &P,
account: AccountUuid,
) -> Result<(), SqliteClientError> {
let (owner, _) = account_key(conn, params, account)?;
conn.execute(
"DELETE FROM ironwood_one_time_discovery WHERE account_id = ?1",
[owner.0],
)?;
conn.execute(
"DELETE FROM ironwood_one_time_fresh_accounts WHERE account_id = ?1",
[owner.0],
)?;
conn.execute(
"UPDATE ironwood_one_time_lookups SET cutoff_height = NULL, done_height = NULL
WHERE done_height IS NOT NULL AND one_time_key_id IN (
SELECT k.id FROM ironwood_one_time_keys k
WHERE k.account_id = ?1 AND (k.paid_before_birthday = 1
OR EXISTS(SELECT 1 FROM ironwood_received_notes n
WHERE n.one_time_key_id = k.id)))",
[owner.0],
)?;
Ok(())
}
pub(in crate::wallet::one_time_ivk) fn mark_fresh<P: Parameters>(
conn: &rusqlite::Transaction<'_>,
params: &P,
account: AccountUuid,
) -> Result<(), SqliteClientError> {
let (owner, _) = account_key(conn, params, account)?;
let (fresh, keyed): (bool, bool) = conn.query_row(
"SELECT EXISTS(SELECT 1 FROM ironwood_one_time_fresh_accounts WHERE account_id = ?1),
EXISTS(SELECT 1 FROM ironwood_one_time_keys WHERE account_id = ?1)",
[owner.0],
|r| Ok((r.get(0)?, r.get(1)?)),
)?;
if !fresh && keyed {
return Err(invalid("account already has one-time keys"));
}
conn.execute(
"INSERT OR IGNORE INTO ironwood_one_time_fresh_accounts (account_id) VALUES (?1)",
[owner.0],
)?;
Ok(())
}
pub(in crate::wallet::one_time_ivk) fn refund_memos_pending(
conn: &Connection,
account: AccountUuid,
) -> Result<bool, SqliteClientError> {
let scanned = wallet::fully_scanned_height(conn)?.map(u32::from);
Ok(conn.query_row(
"SELECT EXISTS(SELECT 1 FROM ironwood_received_notes n
JOIN transactions t ON t.id_tx = n.transaction_id
JOIN accounts a ON a.id = n.account_id
WHERE a.uuid = ?1 AND n.recipient_key_scope = 1
AND n.one_time_key_id IS NULL AND t.mined_height IS NOT NULL
AND (n.memo IS NULL OR (substr(n.memo, 1, 5) = ?3
AND NOT EXISTS(SELECT 1 FROM v_received_output_spends s
WHERE s.transaction_id = n.transaction_id
AND s.account_id = n.account_id)
AND (?2 IS NULL OR t.mined_height > ?2))))",
params![account.0, scanned, REFUND_MEMO_MAGIC],
|r| r.get(0),
)?)
}
pub(in crate::wallet::one_time_ivk) fn recover_refund_memos<P: Parameters>(
conn: &rusqlite::Transaction<'_>,
params: &P,
account: AccountUuid,
now: i64,
) -> Result<(), SqliteClientError> {
let (account_ref, _) = account_key(conn, params, account)?;
let scanned = wallet::fully_scanned_height(conn)?.map(u32::from);
let records = conn
.prepare_cached(
"SELECT n.memo, t.mined_height, EXISTS (SELECT 1 FROM v_received_output_spends s
WHERE s.transaction_id = n.transaction_id AND s.account_id = n.account_id)
FROM ironwood_received_notes n
JOIN transactions t ON t.id_tx = n.transaction_id
WHERE n.account_id = ?1 AND n.recipient_key_scope = 1
AND n.one_time_key_id IS NULL AND t.mined_height IS NOT NULL
AND substr(n.memo, 1, 5) = ?2",
)?
.query_map(params![account_ref.0, REFUND_MEMO_MAGIC], |row| {
Ok((
row.get::<_, Vec<u8>>(0)?,
row.get::<_, u32>(1)?,
row.get::<_, bool>(2)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
for (bytes, height, authenticated) in records {
let memo = zcash_protocol::memo::MemoBytes::from_bytes(&bytes)
.ok()
.and_then(|bytes| RefundMemo::decode(bytes.as_array()));
let (memo, authenticated) = match memo {
Some(memo) if authenticated => (memo, true),
Some(memo) if scanned.is_some_and(|scanned| height <= scanned) => (memo, false),
_ => continue,
};
let key_id = KeyId::new(Purpose::Refund, memo.index());
let covered: bool = conn.query_row(
"SELECT EXISTS(SELECT 1 FROM ironwood_one_time_keys k
WHERE k.account_id = ?1 AND k.purpose = 0 AND k.key_index = ?2
AND k.scan_from <= ?3 AND (k.active_from <= ?3 OR EXISTS(
SELECT 1 FROM ironwood_one_time_lookups l WHERE l.one_time_key_id = k.id)))",
params![account_ref.0, key_id.index().to_be_bytes(), height],
|row| row.get(0),
)?;
if !covered {
let from = BlockHeight::from(height);
register(
conn,
params,
account,
key_id,
from,
authenticated,
Discovery::Lookup,
now,
)?;
}
}
Ok(())
}