use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use tokio::sync::mpsc;
use zcash_keys::keys::UnifiedFullViewingKey;
use zcash_primitives::transaction::TxId;
use zcash_protocol::{
ShieldedProtocol,
consensus::{self, BlockHeight},
};
use zip32::AccountId;
use crate::{
client::{self, FetchRequest},
error::SyncError,
scan::{DecryptedNoteData, transactions::scan_transactions},
wallet::{
NullifierMap, OutputId, ScanTarget, WalletBlock, WalletTransaction,
traits::{SyncBlocks, SyncNullifiers, SyncOutPoints, SyncShardTrees, SyncTransactions},
},
};
use super::state;
pub(super) async fn update_shielded_spends<P, W>(
consensus_parameters: &P,
wallet: &mut W,
fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
scanned_blocks: &BTreeMap<BlockHeight, WalletBlock>,
additional_nullifier_map: Option<&mut NullifierMap>,
) -> Result<(), SyncError<W::Error>>
where
P: consensus::Parameters,
W: SyncBlocks + SyncTransactions + SyncNullifiers + SyncShardTrees,
{
let (sapling_derived_nullifiers, orchard_derived_nullifiers) = collect_derived_nullifiers(
wallet
.get_wallet_transactions()
.map_err(SyncError::WalletError)?,
);
let (mut sapling_spend_scan_targets, mut orchard_spend_scan_targets) = detect_shielded_spends(
wallet
.get_nullifiers_mut()
.map_err(SyncError::WalletError)?,
sapling_derived_nullifiers.clone(),
orchard_derived_nullifiers.clone(),
);
if let Some(nullifier_map) = additional_nullifier_map {
let (mut additional_sapling_spend_scan_targets, mut additional_orchard_spend_scan_targets) =
detect_shielded_spends(
nullifier_map,
sapling_derived_nullifiers,
orchard_derived_nullifiers,
);
sapling_spend_scan_targets.append(&mut additional_sapling_spend_scan_targets);
orchard_spend_scan_targets.append(&mut additional_orchard_spend_scan_targets);
}
let sync_state = wallet
.get_sync_state_mut()
.map_err(SyncError::WalletError)?;
state::set_found_note_scan_ranges(
consensus_parameters,
sync_state,
ShieldedProtocol::Sapling,
sapling_spend_scan_targets.values().copied(),
);
state::set_found_note_scan_ranges(
consensus_parameters,
sync_state,
ShieldedProtocol::Orchard,
orchard_spend_scan_targets.values().copied(),
);
scan_spending_transactions(
fetch_request_sender,
consensus_parameters,
wallet,
ufvks,
sapling_spend_scan_targets
.values()
.chain(orchard_spend_scan_targets.values())
.copied(),
scanned_blocks,
)
.await?;
update_spent_notes(
wallet,
sapling_spend_scan_targets,
orchard_spend_scan_targets,
true,
)
.map_err(SyncError::WalletError)?;
Ok(())
}
async fn scan_spending_transactions<L, P, W>(
fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
consensus_parameters: &P,
wallet: &mut W,
ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
scan_targets: L,
scanned_blocks: &BTreeMap<BlockHeight, WalletBlock>,
) -> Result<(), SyncError<W::Error>>
where
L: Iterator<Item = ScanTarget>,
P: consensus::Parameters,
W: SyncBlocks + SyncTransactions + SyncNullifiers,
{
let wallet_transactions = wallet
.get_wallet_transactions()
.map_err(SyncError::WalletError)?;
let wallet_txids = wallet_transactions.keys().copied().collect::<HashSet<_>>();
let mut spending_scan_targets = BTreeSet::new();
let mut wallet_blocks = BTreeMap::new();
for scan_target in scan_targets {
let block_height = scan_target.block_height;
let txid = scan_target.txid;
if wallet_txids.contains(&txid) {
continue;
}
spending_scan_targets.insert(scan_target);
let wallet_block = match wallet.get_wallet_block(block_height) {
Ok(block) => block,
Err(_) => match scanned_blocks.get(&block_height) {
Some(block) => block.clone(),
None => {
WalletBlock::from_compact_block(
consensus_parameters,
fetch_request_sender.clone(),
&client::get_compact_block(fetch_request_sender.clone(), block_height)
.await?,
)
.await?
}
},
};
wallet_blocks.insert(block_height, wallet_block);
}
let mut outpoint_map = BTreeMap::new(); let spending_transactions = scan_transactions(
fetch_request_sender,
consensus_parameters,
ufvks,
spending_scan_targets,
DecryptedNoteData::new(),
&wallet_blocks,
&mut outpoint_map,
HashMap::new(), )
.await?;
wallet
.extend_wallet_transactions(spending_transactions)
.map_err(SyncError::WalletError)
}
pub(super) fn collect_derived_nullifiers(
wallet_transactions: &HashMap<TxId, WalletTransaction>,
) -> (
Vec<sapling_crypto::Nullifier>,
Vec<orchard::note::Nullifier>,
) {
let sapling_nullifiers = wallet_transactions
.values()
.flat_map(super::super::wallet::WalletTransaction::sapling_notes)
.filter_map(|note| note.nullifier)
.collect::<Vec<_>>();
let orchard_nullifiers = wallet_transactions
.values()
.flat_map(super::super::wallet::WalletTransaction::orchard_notes)
.filter_map(|note| note.nullifier)
.collect::<Vec<_>>();
(sapling_nullifiers, orchard_nullifiers)
}
pub(super) fn detect_shielded_spends(
nullifier_map: &mut NullifierMap,
sapling_derived_nullifiers: Vec<sapling_crypto::Nullifier>,
orchard_derived_nullifiers: Vec<orchard::note::Nullifier>,
) -> (
BTreeMap<sapling_crypto::Nullifier, ScanTarget>,
BTreeMap<orchard::note::Nullifier, ScanTarget>,
) {
let sapling_spend_scan_targets = sapling_derived_nullifiers
.iter()
.filter_map(|nf| nullifier_map.sapling.remove_entry(nf))
.collect();
let orchard_spend_scan_targets = orchard_derived_nullifiers
.iter()
.filter_map(|nf| nullifier_map.orchard.remove_entry(nf))
.collect();
(sapling_spend_scan_targets, orchard_spend_scan_targets)
}
pub(super) fn update_spent_notes<W>(
wallet: &mut W,
sapling_spend_scan_targets: BTreeMap<sapling_crypto::Nullifier, ScanTarget>,
orchard_spend_scan_targets: BTreeMap<orchard::note::Nullifier, ScanTarget>,
remove_marks: bool,
) -> Result<(), W::Error>
where
W: SyncTransactions + SyncShardTrees,
{
let mut shard_trees = std::mem::take(wallet.get_shard_trees_mut()?);
let wallet_transactions = wallet.get_wallet_transactions_mut()?;
for transaction in wallet_transactions.values_mut() {
let transaction_height = transaction.status.get_confirmed_height();
transaction
.sapling_notes_mut()
.into_iter()
.for_each(|note| {
if let Some(scan_target) = note
.nullifier
.and_then(|nf| sapling_spend_scan_targets.get(&nf))
{
note.spending_transaction = Some(scan_target.txid);
if remove_marks
&& let Some(height) = transaction_height
&& let Some(position) = note.position
{
shard_trees
.sapling
.remove_mark(position, Some(&height))
.expect("infallible");
}
}
});
}
for transaction in wallet_transactions.values_mut() {
let transaction_height = transaction.status.get_confirmed_height();
transaction
.orchard_notes_mut()
.into_iter()
.for_each(|note| {
if let Some(scan_target) = note
.nullifier
.and_then(|nf| orchard_spend_scan_targets.get(&nf))
{
note.spending_transaction = Some(scan_target.txid);
if remove_marks
&& let Some(height) = transaction_height
&& let Some(position) = note.position
{
shard_trees
.orchard
.remove_mark(position, Some(&height))
.expect("infallible");
}
}
});
}
*wallet.get_shard_trees_mut()? = shard_trees;
Ok(())
}
pub(super) fn update_transparent_spends<W>(
wallet: &mut W,
additional_outpoint_map: Option<&mut BTreeMap<OutputId, ScanTarget>>,
) -> Result<(), W::Error>
where
W: SyncBlocks + SyncTransactions + SyncOutPoints,
{
let transparent_output_ids = collect_transparent_output_ids(wallet.get_wallet_transactions()?);
let mut transparent_spend_scan_targets =
detect_transparent_spends(wallet.get_outpoints_mut()?, transparent_output_ids.clone());
if let Some(outpoint_map) = additional_outpoint_map {
let mut additional_transparent_spend_scan_targets =
detect_transparent_spends(outpoint_map, transparent_output_ids);
transparent_spend_scan_targets.append(&mut additional_transparent_spend_scan_targets);
}
update_spent_coins(
wallet.get_wallet_transactions_mut()?,
transparent_spend_scan_targets,
);
Ok(())
}
pub(super) fn collect_transparent_output_ids(
wallet_transactions: &HashMap<TxId, WalletTransaction>,
) -> Vec<OutputId> {
wallet_transactions
.values()
.flat_map(super::super::wallet::WalletTransaction::transparent_coins)
.map(|coin| coin.output_id)
.collect()
}
pub(super) fn detect_transparent_spends(
outpoint_map: &mut BTreeMap<OutputId, ScanTarget>,
transparent_output_ids: Vec<OutputId>,
) -> BTreeMap<OutputId, ScanTarget> {
transparent_output_ids
.iter()
.filter_map(|output_id| outpoint_map.remove_entry(output_id))
.collect()
}
pub(super) fn update_spent_coins(
wallet_transactions: &mut HashMap<TxId, WalletTransaction>,
transparent_spend_scan_targets: BTreeMap<OutputId, ScanTarget>,
) {
wallet_transactions
.values_mut()
.flat_map(|tx| tx.transparent_coins_mut())
.for_each(|coin| {
if let Some(scan_target) = transparent_spend_scan_targets.get(&coin.output_id) {
coin.spending_transaction = Some(scan_target.txid);
}
});
}