pub(super) mod funding;
pub(super) mod lifecycle;
pub(super) mod reservations;
pub(crate) use funding::start_funded_refund_keys;
pub use funding::verify_refund_funding_proposal;
pub use reservations::{
OperationOutcome, ProviderSeen, RECEIVE_GAP_LIMIT, RECEIVE_RECLAIM_SECONDS, ReceiveDeposit,
SEEN_SLACK_SECONDS,
};
use std::borrow::{Borrow, BorrowMut};
use rand_core::Rng;
use rusqlite::Connection;
use zakura_one_time_ivk::swap::Observation;
use zcash_protocol::{
consensus::{BlockHeight, Parameters},
memo::MemoBytes,
};
use super::{OneTimeKey, account_key, restore::recovery, unix_now};
use crate::{AccountUuid, WalletDb, error::SqliteClientError, util::Clock};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReservationPolicy {
Gap,
Limit,
Stale,
Coverage,
}
impl<C: Borrow<Connection>, P: Parameters, CL, R> WalletDb<C, P, CL, R> {
pub fn refund_funding_memo(
&self,
account: AccountUuid,
index: u64,
deposit: &str,
) -> Result<MemoBytes, SqliteClientError> {
funding::funding_memo(self.conn.borrow(), &self.params, account, index, deposit)
}
}
impl<C: BorrowMut<Connection>, P: Parameters, CL: Clock, R: Rng> WalletDb<C, P, CL, R> {
pub fn reserve_refund_key(
&mut self,
account: AccountUuid,
tip: BlockHeight,
seen: Option<&ProviderSeen<'_>>,
) -> Result<Result<OneTimeKey, ReservationPolicy>, SqliteClientError> {
let now = unix_now(&self.clock);
self.transactionally(|db| funding::reserve(db.conn.0, db.params, account, tip, seen, now))
}
pub fn record_refund_operation(
&mut self,
account: AccountUuid,
index: u64,
deposit: &str,
deadline: i64,
now: i64,
) -> Result<(), SqliteClientError> {
self.transactionally(|db| {
funding::record_operation(db.conn.0, db.params, account, index, deposit, deadline, now)
})
}
pub fn prepare_receive_reservation(
&mut self,
account: AccountUuid,
now: i64,
tip: BlockHeight,
seen: Option<&ProviderSeen<'_>>,
) -> Result<Result<OneTimeKey, ReservationPolicy>, SqliteClientError> {
let registered_at = unix_now(&self.clock);
self.transactionally(|db| {
let (conn, params) = (db.conn.0, db.params);
if let Some(seen) = seen {
let (account_ref, _) = account_key(conn, params, account)?;
reservations::record_seen(conn, account_ref.0, seen)?;
}
recovery::maintain_receive_lookahead(conn, params, account, registered_at)?;
reservations::reap(conn, params, account, now)?;
let scan_from = match issuance_start(conn, tip)? {
Ok(from) => from,
Err(policy) => return Ok(Err(policy)),
};
reservations::prepare(conn, params, account, now, scan_from, seen)
})
}
pub fn begin_receive_operation(
&mut self,
account: AccountUuid,
index: u64,
deadline: i64,
now: i64,
) -> Result<Result<String, ReservationPolicy>, SqliteClientError> {
let mut id = [0; 16];
self.rng.fill_bytes(&mut id);
let request = hex::encode(id);
self.transactionally(|db| {
reservations::begin(db.conn.0, account, index, &request, deadline, now)
.map(|begun| begun.map(|()| request))
})
}
pub fn finish_receive_operation(
&mut self,
account: AccountUuid,
request: &str,
outcome: &OperationOutcome,
) -> Result<(), SqliteClientError> {
self.transactionally(|db| reservations::finish(db.conn.0, account, request, outcome))
}
pub fn start_receive_operation(
&mut self,
account: AccountUuid,
request: &str,
now: i64,
) -> Result<Result<ReceiveDeposit, ReservationPolicy>, SqliteClientError> {
self.transactionally(|db| reservations::start(db.conn.0, account, request, now))
}
pub fn record_operation_status(
&mut self,
account: AccountUuid,
deposit: &str,
memo: Option<&str>,
observation: Observation,
now: i64,
) -> Result<bool, SqliteClientError> {
self.transactionally(|db| {
reservations::record_status(
db.conn.0,
db.params,
account,
(deposit, memo),
observation,
now,
)
})
}
pub fn close_finished_one_time_keys(
&mut self,
account: AccountUuid,
now: i64,
tip: BlockHeight,
) -> Result<usize, SqliteClientError> {
self.transactionally(|db| {
lifecycle::close_finished(db.conn.0, db.params, account, now, tip)
})
}
}
pub const ISSUANCE_TIP_LAG: u32 = 10;
const RESERVED: &str = "(k.reserved_at IS NOT NULL AND k.released_at IS NULL)";
fn issuance_start(
conn: &Connection,
tip: BlockHeight,
) -> Result<Result<BlockHeight, ReservationPolicy>, SqliteClientError> {
let Some(scanned) = crate::wallet::fully_scanned_height(conn)? else {
return Ok(Err(ReservationPolicy::Coverage));
};
let tip = tip.max(crate::wallet::chain_tip_height(conn)?.unwrap_or(scanned));
Ok(
if u32::from(tip).saturating_sub(u32::from(scanned)) > ISSUANCE_TIP_LAG {
Err(ReservationPolicy::Coverage)
} else {
Ok(scanned + 1)
},
)
}
#[cfg(test)]
mod tests;