use std::{
collections::{BTreeMap, BTreeSet},
convert::Infallible,
fmt::Debug,
marker::PhantomData,
ops::Range,
sync::atomic::{self, AtomicU8},
};
use incrementalmerkletree::Position;
use orchard::tree::MerkleHashOrchard;
use shardtree::{ShardTree, store::memory::MemoryShardStore};
use tokio::sync::mpsc;
use zcash_address::unified::ParseError;
use zcash_keys::{address::UnifiedAddress, encoding::encode_payment_address};
use zcash_primitives::{block::BlockHash, transaction::TxId};
use zcash_protocol::{
PoolType, ShieldedPool,
consensus::{self, BlockHeight},
memo::Memo,
value::Zatoshis,
};
use zcash_transparent::address::Script;
use zcash_transparent::bundle::OutPoint;
use zingo_netutils::lightwallet_protocol::CompactBlock;
use zingo_status::confirmation_status::ConfirmationStatus;
use crate::{
client::FetchRequest,
error::{ServerError, SyncModeError},
keys::{self, KeyId, transparent::TransparentAddressId},
scan::compact_blocks::calculate_block_tree_bounds,
shardtree_ext::{CheckpointAppendOutcome, ShardTreeExt as _},
sync::{SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE, ScanPriority, ScanRange},
utils::{block, transaction},
witness,
};
pub mod traits;
#[cfg(feature = "wallet_essentials")]
pub mod serialization;
#[derive(Debug, Clone, Copy)]
pub struct ScanTarget {
block_height: BlockHeight,
txid: Option<TxId>,
pools: ScanTargetPools,
origin: ScanTargetOrigin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScanTargetOrigin {
Internal,
External,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct ScanTargetPools(u8);
impl ScanTargetPools {
const ALL_POOLS: [ShieldedPool; 3] = [
ShieldedPool::Sapling,
ShieldedPool::Orchard,
ShieldedPool::Ironwood,
];
const fn bit(pool: ShieldedPool) -> u8 {
match pool {
ShieldedPool::Sapling => 1,
ShieldedPool::Orchard => 1 << 1,
ShieldedPool::Ironwood => 1 << 2,
}
}
#[cfg(feature = "wallet_essentials")]
pub(crate) fn from_bits(bits: u8) -> Option<Self> {
let known = Self::ALL_POOLS
.into_iter()
.fold(0, |known, pool| known | Self::bit(pool));
(bits & !known == 0).then_some(Self(bits))
}
#[cfg(feature = "wallet_essentials")]
pub(crate) fn bits(self) -> u8 {
self.0
}
pub(crate) fn with(self, pool: ShieldedPool) -> Self {
Self(self.0 | Self::bit(pool))
}
pub(crate) fn is_empty(self) -> bool {
self.0 == 0
}
pub(crate) fn iter(self) -> impl Iterator<Item = ShieldedPool> {
Self::ALL_POOLS
.into_iter()
.filter(move |&pool| self.0 & Self::bit(pool) != 0)
}
}
impl ScanTarget {
#[must_use]
pub fn new(block_height: BlockHeight, txid: Option<TxId>) -> Self {
Self {
block_height,
txid,
pools: ScanTargetPools::default(),
origin: ScanTargetOrigin::External,
}
}
pub(crate) fn internal(block_height: BlockHeight, txid: TxId) -> Self {
Self {
block_height,
txid: Some(txid),
pools: ScanTargetPools::default(),
origin: ScanTargetOrigin::Internal,
}
}
#[must_use]
pub fn block_height(&self) -> BlockHeight {
self.block_height
}
#[must_use]
pub fn txid(&self) -> Option<TxId> {
self.txid
}
pub(crate) fn pools(&self) -> ScanTargetPools {
self.pools
}
pub(crate) fn origin(&self) -> ScanTargetOrigin {
self.origin
}
pub(crate) fn with_pool(self, pool: ShieldedPool) -> Self {
Self {
pools: self.pools.with(pool),
..self
}
}
fn merge(mut self, other: Self) -> Self {
self.pools.0 |= other.pools.0;
if other.origin == ScanTargetOrigin::Internal {
self.origin = ScanTargetOrigin::Internal;
}
self
}
fn key(&self) -> (BlockHeight, Option<TxId>) {
(self.block_height, self.txid)
}
}
impl PartialEq for ScanTarget {
fn eq(&self, other: &Self) -> bool {
self.key() == other.key()
}
}
impl Eq for ScanTarget {}
impl PartialOrd for ScanTarget {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ScanTarget {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.key().cmp(&other.key())
}
}
#[derive(Debug, Clone)]
pub struct InitialSyncState {
pub(crate) sync_start_height: BlockHeight,
pub(crate) wallet_tree_bounds: TreeBounds,
pub(crate) previously_scanned_blocks: u32,
pub(crate) previously_scanned_sapling_outputs: u32,
pub(crate) previously_scanned_orchard_outputs: u32,
pub(crate) previously_scanned_ironwood_outputs: u32,
}
impl InitialSyncState {
#[must_use]
pub fn new() -> Self {
InitialSyncState {
sync_start_height: 0.into(),
wallet_tree_bounds: TreeBounds {
sapling_initial_tree_size: 0,
sapling_final_tree_size: 0,
orchard_initial_tree_size: 0,
orchard_final_tree_size: 0,
ironwood_initial_tree_size: 0,
ironwood_final_tree_size: 0,
},
previously_scanned_blocks: 0,
previously_scanned_sapling_outputs: 0,
previously_scanned_orchard_outputs: 0,
previously_scanned_ironwood_outputs: 0,
}
}
}
impl Default for InitialSyncState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct SyncState {
pub(crate) scan_ranges: Vec<ScanRange>,
pub(crate) sapling_shard_ranges: Vec<Range<BlockHeight>>,
pub(crate) orchard_shard_ranges: Vec<Range<BlockHeight>>,
pub(crate) ironwood_shard_ranges: Vec<Range<BlockHeight>>,
pub(crate) scan_targets: BTreeSet<ScanTarget>,
pub(crate) initial_sync_state: InitialSyncState,
pub(crate) transparent_scan_floor: Option<BlockHeight>,
}
impl SyncState {
#[must_use]
pub fn new() -> Self {
SyncState {
scan_ranges: Vec::new(),
sapling_shard_ranges: Vec::new(),
orchard_shard_ranges: Vec::new(),
ironwood_shard_ranges: Vec::new(),
scan_targets: BTreeSet::new(),
initial_sync_state: InitialSyncState::new(),
transparent_scan_floor: None,
}
}
#[must_use]
pub fn scan_ranges(&self) -> &[ScanRange] {
&self.scan_ranges
}
#[must_use]
pub fn sapling_shard_ranges(&self) -> &[Range<BlockHeight>] {
&self.sapling_shard_ranges
}
#[must_use]
pub fn orchard_shard_ranges(&self) -> &[Range<BlockHeight>] {
&self.orchard_shard_ranges
}
#[must_use]
pub fn ironwood_shard_ranges(&self) -> &[Range<BlockHeight>] {
&self.ironwood_shard_ranges
}
#[must_use]
pub fn shard_ranges(&self, pool: ShieldedPool) -> &[Range<BlockHeight>] {
match pool {
ShieldedPool::Sapling => &self.sapling_shard_ranges,
ShieldedPool::Orchard => &self.orchard_shard_ranges,
ShieldedPool::Ironwood => &self.ironwood_shard_ranges,
}
}
pub(crate) fn scan_complete(&self) -> bool {
self.scan_ranges
.iter()
.all(|scan_range| scan_range.priority() == ScanPriority::Scanned)
}
#[must_use]
pub fn fully_scanned_height(&self) -> Option<BlockHeight> {
if let Some(scan_range) = self
.scan_ranges
.iter()
.find(|scan_range| scan_range.priority() != ScanPriority::Scanned)
{
Some(scan_range.block_range().start - 1)
} else {
self.scan_ranges
.last()
.map(|range| range.block_range().end - 1)
}
}
#[must_use]
pub fn highest_scanned_height(&self) -> Option<BlockHeight> {
if let Some(last_scanned_range) = self.scan_ranges.iter().rfind(|scan_range| {
scan_range.priority() == ScanPriority::Scanned
|| scan_range.priority() == ScanPriority::ScannedWithoutMapping
|| scan_range.priority() == ScanPriority::RefetchingNullifiers
}) {
Some(last_scanned_range.block_range().end - 1)
} else {
self.wallet_birthday().map(|start| start - 1)
}
}
#[must_use]
pub fn wallet_birthday(&self) -> Option<BlockHeight> {
self.scan_ranges
.first()
.map(|range| range.block_range().start)
}
pub(crate) fn insert_scan_target(&mut self, scan_target: ScanTarget) {
let merged = self
.scan_targets
.take(&scan_target)
.map_or(scan_target, |held| held.merge(scan_target));
self.scan_targets.insert(merged);
}
#[must_use]
pub fn last_known_chain_height(&self) -> Option<BlockHeight> {
self.scan_ranges
.last()
.map(|range| range.block_range().end - 1)
}
}
impl Default for SyncState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncMode {
NotRunning,
Paused,
Running,
Shutdown,
}
impl SyncMode {
pub fn from_u8(mode: u8) -> Result<Self, SyncModeError> {
match mode {
0 => Ok(Self::NotRunning),
1 => Ok(Self::Paused),
2 => Ok(Self::Running),
3 => Ok(Self::Shutdown),
_ => Err(SyncModeError::InvalidSyncMode(mode)),
}
}
pub fn from_atomic_u8(atomic_sync_mode: &AtomicU8) -> Result<SyncMode, SyncModeError> {
SyncMode::from_u8(atomic_sync_mode.load(atomic::Ordering::Acquire))
}
pub fn on_completion(self) -> Self {
match self {
Self::Running => Self::Shutdown,
other => other,
}
}
pub fn apply(
atomic_sync_mode: &AtomicU8,
step: impl Fn(Self) -> Self,
) -> Result<Self, SyncModeError> {
atomic_sync_mode
.try_update(
atomic::Ordering::AcqRel,
atomic::Ordering::Acquire,
|mode| Self::from_u8(mode).ok().map(|mode| step(mode) as u8),
)
.map_err(SyncModeError::InvalidSyncMode)
.and_then(Self::from_u8)
}
pub fn transition(
atomic_sync_mode: &AtomicU8,
from: Self,
to: Self,
) -> Result<Self, SyncModeError> {
Self::apply(
atomic_sync_mode,
|mode| if mode == from { to } else { mode },
)
}
}
#[derive(Debug, Clone, Copy)]
#[allow(missing_docs)]
pub struct TreeBounds {
pub sapling_initial_tree_size: u32,
pub sapling_final_tree_size: u32,
pub orchard_initial_tree_size: u32,
pub orchard_final_tree_size: u32,
pub ironwood_initial_tree_size: u32,
pub ironwood_final_tree_size: u32,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct OutputId {
txid: TxId,
output_index: u32,
}
impl OutputId {
#[must_use]
pub fn new(txid: TxId, output_index: u32) -> Self {
OutputId { txid, output_index }
}
#[must_use]
pub fn txid(&self) -> TxId {
self.txid
}
#[must_use]
pub fn output_index(&self) -> u32 {
self.output_index
}
}
impl std::fmt::Display for OutputId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{{
txid: {}
output index: {}
}}",
self.txid, self.output_index
)
}
}
impl From<&OutPoint> for OutputId {
fn from(value: &OutPoint) -> Self {
OutputId::new(*value.txid(), value.n())
}
}
impl From<OutputId> for OutPoint {
fn from(value: OutputId) -> Self {
OutPoint::new(value.txid.into(), value.output_index)
}
}
#[derive(Debug)]
pub struct NullifierMap {
pub sapling: BTreeMap<sapling_crypto::Nullifier, ScanTarget>,
pub orchard: BTreeMap<orchard::note::Nullifier, ScanTarget>,
pub ironwood: BTreeMap<orchard::note::Nullifier, ScanTarget>,
}
impl NullifierMap {
#[must_use]
pub fn new() -> Self {
Self {
sapling: BTreeMap::new(),
orchard: BTreeMap::new(),
ironwood: BTreeMap::new(),
}
}
pub fn clear(&mut self) {
self.sapling.clear();
self.orchard.clear();
self.ironwood.clear();
}
}
impl Default for NullifierMap {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct WalletBlock {
pub(crate) block_height: BlockHeight,
pub(crate) block_hash: BlockHash,
pub(crate) prev_hash: BlockHash,
pub(crate) time: u32,
pub(crate) txids: Vec<TxId>,
pub(crate) tree_bounds: TreeBounds,
}
impl WalletBlock {
pub(crate) async fn from_compact_block(
consensus_parameters: &impl consensus::Parameters,
fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
block: &CompactBlock,
) -> Result<Self, ServerError> {
let tree_bounds =
calculate_block_tree_bounds(consensus_parameters, fetch_request_sender, block).await?;
Ok(Self {
block_height: block::get_compact_height(block),
block_hash: block::get_compact_hash(block),
prev_hash: block::get_compact_prev_hash(block),
time: block.time,
txids: block
.vtx
.iter()
.map(transaction::get_compact_txid)
.collect(),
tree_bounds,
})
}
#[must_use]
pub fn block_height(&self) -> BlockHeight {
self.block_height
}
#[must_use]
pub fn block_hash(&self) -> BlockHash {
self.block_hash
}
#[must_use]
pub fn prev_hash(&self) -> BlockHash {
self.prev_hash
}
#[must_use]
pub fn time(&self) -> u32 {
self.time
}
#[must_use]
pub fn txids(&self) -> &[TxId] {
&self.txids
}
#[must_use]
pub fn tree_bounds(&self) -> TreeBounds {
self.tree_bounds
}
}
pub struct WalletTransaction {
pub(crate) txid: TxId,
pub(crate) status: ConfirmationStatus,
pub(crate) transaction: zcash_primitives::transaction::Transaction,
pub(crate) datetime: u32,
pub(crate) transparent_coins: Vec<TransparentCoin>,
pub(crate) sapling_notes: Vec<SaplingNote>,
pub(crate) orchard_notes: Vec<OrchardNote>,
pub(crate) ironwood_notes: Vec<IronwoodNote>,
pub(crate) outgoing_sapling_notes: Vec<OutgoingSaplingNote>,
pub(crate) outgoing_orchard_notes: Vec<OutgoingOrchardNote>,
pub(crate) outgoing_ironwood_notes: Vec<OutgoingIronwoodNote>,
}
impl WalletTransaction {
#[must_use]
pub fn txid(&self) -> TxId {
self.txid
}
#[must_use]
pub fn status(&self) -> ConfirmationStatus {
self.status
}
#[must_use]
pub fn transaction(&self) -> &zcash_primitives::transaction::Transaction {
&self.transaction
}
#[must_use]
pub fn datetime(&self) -> u32 {
self.datetime
}
#[must_use]
pub fn transparent_coins(&self) -> &[TransparentCoin] {
&self.transparent_coins
}
pub fn transparent_coins_mut(&mut self) -> Vec<&mut TransparentCoin> {
self.transparent_coins.iter_mut().collect()
}
#[must_use]
pub fn sapling_notes(&self) -> &[SaplingNote] {
&self.sapling_notes
}
pub fn sapling_notes_mut(&mut self) -> Vec<&mut SaplingNote> {
self.sapling_notes.iter_mut().collect()
}
#[must_use]
pub fn orchard_notes(&self) -> &[OrchardNote] {
&self.orchard_notes
}
pub fn orchard_notes_mut(&mut self) -> Vec<&mut OrchardNote> {
self.orchard_notes.iter_mut().collect()
}
#[must_use]
pub fn ironwood_notes(&self) -> &[IronwoodNote] {
&self.ironwood_notes
}
pub fn ironwood_notes_mut(&mut self) -> Vec<&mut IronwoodNote> {
self.ironwood_notes.iter_mut().collect()
}
#[must_use]
pub fn outgoing_sapling_notes(&self) -> &[OutgoingSaplingNote] {
&self.outgoing_sapling_notes
}
#[must_use]
pub fn outgoing_orchard_notes(&self) -> &[OutgoingOrchardNote] {
&self.outgoing_orchard_notes
}
#[must_use]
pub fn outgoing_ironwood_notes(&self) -> &[OutgoingIronwoodNote] {
&self.outgoing_ironwood_notes
}
pub fn sapling_nullifiers(&self) -> Vec<&sapling_crypto::Nullifier> {
self.transaction
.sapling_bundle()
.map_or_else(Vec::new, |bundle| {
bundle
.shielded_spends()
.iter()
.map(|spend| spend.nullifier())
.collect::<Vec<_>>()
})
}
pub fn orchard_nullifiers(&self) -> Vec<&orchard::note::Nullifier> {
self.transaction
.orchard_bundle()
.map_or_else(Vec::new, |bundle| {
bundle
.actions()
.iter()
.map(orchard::Action::nullifier)
.collect::<Vec<_>>()
})
}
pub fn ironwood_nullifiers(&self) -> Vec<&orchard::note::Nullifier> {
self.transaction
.ironwood_bundle()
.map_or_else(Vec::new, |bundle| {
bundle
.actions()
.iter()
.map(orchard::Action::nullifier)
.collect::<Vec<_>>()
})
}
pub fn outpoints(&self) -> Vec<&OutPoint> {
self.transaction
.transparent_bundle()
.map_or_else(Vec::new, |bundle| {
bundle
.vin
.iter()
.map(zcash_transparent::bundle::TxIn::prevout)
.collect::<Vec<_>>()
})
}
pub fn update_status(
&mut self,
status: ConfirmationStatus,
datetime: u32,
fail_confirmed: bool,
) {
match status {
ConfirmationStatus::Transmitted(_)
if matches!(self.status(), ConfirmationStatus::Calculated(_)) =>
{
self.status = status;
self.datetime = datetime;
}
ConfirmationStatus::Mempool(_)
if matches!(
self.status(),
ConfirmationStatus::Calculated(_)
| ConfirmationStatus::Transmitted(_)
| ConfirmationStatus::Failed(_)
) =>
{
self.status = status;
self.datetime = datetime;
}
ConfirmationStatus::Confirmed(_)
if matches!(
self.status(),
ConfirmationStatus::Calculated(_)
| ConfirmationStatus::Transmitted(_)
| ConfirmationStatus::Mempool(_)
) =>
{
self.status = status;
self.datetime = datetime;
}
ConfirmationStatus::Failed(_)
if (fail_confirmed && !matches!(self.status(), ConfirmationStatus::Failed(_)))
|| self.status().is_pending() =>
{
self.status = status;
self.datetime = datetime;
}
_ => (),
}
}
}
#[cfg(any(test, feature = "test-features"))]
impl WalletTransaction {
pub fn new_for_test(txid: TxId, status: ConfirmationStatus) -> Self {
Self::new_for_test_with_expiry(txid, status, BlockHeight::from_u32(0))
}
pub fn new_for_test_with_expiry(
txid: TxId,
status: ConfirmationStatus,
expiry_height: BlockHeight,
) -> Self {
use zcash_primitives::transaction::{TransactionData, TxVersion};
use zcash_protocol::consensus::BranchId;
let transaction = TransactionData::from_parts(
TxVersion::V6,
BranchId::Nu6_3,
0,
expiry_height,
None,
None,
None,
None,
)
.freeze()
.expect("empty v5 transaction should always be valid");
Self {
txid,
status,
transaction,
datetime: 0,
transparent_coins: Vec::new(),
sapling_notes: Vec::new(),
orchard_notes: Vec::new(),
ironwood_notes: Vec::new(),
outgoing_sapling_notes: Vec::new(),
outgoing_orchard_notes: Vec::new(),
outgoing_ironwood_notes: Vec::new(),
}
}
#[must_use]
pub fn with_sapling_notes_for_test(mut self, sapling_notes: Vec<SaplingNote>) -> Self {
self.sapling_notes = sapling_notes;
self
}
#[must_use]
pub fn with_outgoing_sapling_notes_for_test(
mut self,
outgoing_sapling_notes: Vec<OutgoingSaplingNote>,
) -> Self {
self.outgoing_sapling_notes = outgoing_sapling_notes;
self
}
#[must_use]
pub fn with_transparent_coins_for_test(
mut self,
transparent_coins: Vec<TransparentCoin>,
) -> Self {
self.transparent_coins = transparent_coins;
self
}
pub fn new_for_test_with_orchard_notes(
txid: TxId,
status: ConfirmationStatus,
orchard_notes: Vec<OrchardNote>,
outgoing_orchard_notes: Vec<OutgoingOrchardNote>,
) -> Self {
let mut transaction = Self::new_for_test(txid, status);
transaction.orchard_notes = orchard_notes;
transaction.outgoing_orchard_notes = outgoing_orchard_notes;
transaction
}
pub fn new_for_test_with_ironwood_notes(
txid: TxId,
status: ConfirmationStatus,
ironwood_notes: Vec<IronwoodNote>,
outgoing_ironwood_notes: Vec<OutgoingIronwoodNote>,
) -> Self {
let mut transaction = Self::new_for_test(txid, status);
transaction.ironwood_notes = ironwood_notes;
transaction.outgoing_ironwood_notes = outgoing_ironwood_notes;
transaction
}
}
#[cfg(any(test, feature = "test-features"))]
impl SyncState {
pub fn new_for_test(scan_ranges: Vec<ScanRange>) -> Self {
let mut sync_state = Self::new();
sync_state.scan_ranges = scan_ranges;
sync_state
}
}
#[cfg(any(test, feature = "test-features"))]
impl<N, Nf: Copy, P> WalletNote<N, Nf, P> {
pub fn new_for_test(
output_id: OutputId,
account_id: zip32::AccountId,
scope: zip32::Scope,
note: N,
memo: Memo,
position: Option<Position>,
) -> Self {
Self {
output_id,
key_id: KeyId::from_parts(account_id, scope),
note,
nullifier: None,
position,
memo,
spending_transaction: None,
refetch_nullifier_ranges: Vec::new(),
marker: std::marker::PhantomData,
}
}
#[must_use]
pub fn with_nullifier_for_test(mut self, nullifier: Nf) -> Self {
self.nullifier = Some(nullifier);
self
}
}
#[cfg(feature = "test-features")]
impl<N, P> OutgoingNote<N, P> {
pub fn new_for_test(
output_id: OutputId,
account_id: zip32::AccountId,
scope: zip32::Scope,
note: N,
memo: Memo,
recipient_full_unified_address: Option<UnifiedAddress>,
) -> Self {
Self {
output_id,
key_id: KeyId::from_parts(account_id, scope),
note,
memo,
recipient_full_unified_address,
marker: std::marker::PhantomData,
}
}
}
#[cfg(feature = "wallet_essentials")]
impl WalletTransaction {
#[must_use]
pub fn total_value_sent(&self) -> u64 {
let transparent_value_sent = self
.transaction
.transparent_bundle()
.map_or(0, |bundle| {
bundle
.vout
.iter()
.map(|output| output.value().into_u64())
.sum()
})
.saturating_sub(self.total_output_value::<TransparentCoin>());
let sapling_value_sent =
self.total_external_outgoing_note_value::<OutgoingSaplingNote, SaplingNote>();
let orchard_value_sent =
self.total_external_outgoing_note_value::<OutgoingOrchardNote, OrchardNote>();
let ironwood_value_sent =
self.total_external_outgoing_note_value::<OutgoingIronwoodNote, IronwoodNote>();
transparent_value_sent + sapling_value_sent + orchard_value_sent + ironwood_value_sent
}
#[must_use]
pub fn total_value_received(&self) -> u64 {
self.total_output_value::<TransparentCoin>()
+ self.total_output_value::<SaplingNote>()
+ self.total_output_value::<OrchardNote>()
+ self.total_output_value::<IronwoodNote>()
}
#[must_use]
pub fn total_output_value<Op: OutputInterface>(&self) -> u64 {
Op::transaction_outputs(self)
.iter()
.map(OutputInterface::value)
.sum()
}
#[must_use]
pub fn total_outgoing_note_value<Op: OutgoingNoteInterface>(&self) -> u64 {
Op::transaction_outgoing_notes(self)
.iter()
.map(OutgoingNoteInterface::value)
.sum()
}
#[must_use]
pub fn total_external_outgoing_note_value<Outgoing, Incoming>(&self) -> u64
where
Outgoing: OutgoingNoteInterface,
Incoming: OutputInterface,
{
Outgoing::transaction_outgoing_notes(self)
.iter()
.filter(|outgoing_note| {
!Incoming::transaction_outputs(self)
.iter()
.any(|wallet_note| wallet_note.output_id() == outgoing_note.output_id())
})
.map(OutgoingNoteInterface::value)
.sum()
}
}
impl std::fmt::Debug for WalletTransaction {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("WalletTransaction")
.field("txid", &self.txid)
.field("confirmation_status", &self.status)
.field("datetime", &self.datetime)
.field("transparent_coins", &self.transparent_coins)
.field("sapling_notes", &self.sapling_notes)
.field("orchard_notes", &self.orchard_notes)
.field("ironwood_notes", &self.ironwood_notes)
.field("outgoing_sapling_notes", &self.outgoing_sapling_notes)
.field("outgoing_orchard_notes", &self.outgoing_orchard_notes)
.field("outgoing_ironwood_notes", &self.outgoing_ironwood_notes)
.finish()
}
}
pub trait KeyIdInterface {
fn account_id(&self) -> zip32::AccountId;
}
pub trait OutputInterface: Sized {
type KeyId: KeyIdInterface;
type Input: Clone + Debug + PartialEq + Eq + PartialOrd + Ord;
const POOL_TYPE: PoolType;
fn output_id(&self) -> OutputId;
fn key_id(&self) -> Self::KeyId;
fn spending_transaction(&self) -> Option<TxId>;
fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>);
fn value(&self) -> u64;
fn spend_link(&self) -> Option<Self::Input>;
fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input>;
fn transaction_outputs(transaction: &WalletTransaction) -> &[Self];
}
pub trait NoteInterface: OutputInterface {
type ZcashNote;
type Nullifier: Copy + Clone + PartialEq + Eq + PartialOrd + Ord;
const SHIELDED_PROTOCOL: ShieldedPool;
fn note(&self) -> &Self::ZcashNote;
fn nullifier(&self) -> Option<Self::Nullifier>;
fn position(&self) -> Option<Position>;
fn memo(&self) -> &Memo;
fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>];
}
#[derive(Debug, Clone)]
pub struct TransparentCoin {
pub(crate) output_id: OutputId,
pub(crate) key_id: TransparentAddressId,
pub(crate) address: String,
pub(crate) script: Script,
pub(crate) value: Zatoshis,
pub(crate) spending_transaction: Option<TxId>,
}
#[cfg(feature = "test-features")]
impl TransparentCoin {
pub fn new_for_test(
output_id: OutputId,
key_id: TransparentAddressId,
address: String,
script: Script,
value: Zatoshis,
) -> Self {
Self {
output_id,
key_id,
address,
script,
value,
spending_transaction: None,
}
}
}
impl TransparentCoin {
#[must_use]
pub fn address(&self) -> &str {
&self.address
}
#[must_use]
pub fn script(&self) -> &Script {
&self.script
}
}
impl OutputInterface for TransparentCoin {
type KeyId = TransparentAddressId;
type Input = OutPoint;
const POOL_TYPE: PoolType = PoolType::Transparent;
fn output_id(&self) -> OutputId {
self.output_id
}
fn key_id(&self) -> Self::KeyId {
self.key_id
}
fn spending_transaction(&self) -> Option<TxId> {
self.spending_transaction
}
fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
self.spending_transaction = spending_transaction;
}
fn value(&self) -> u64 {
self.value.into_u64()
}
fn spend_link(&self) -> Option<Self::Input> {
Some(self.output_id.into())
}
fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
transaction.outpoints()
}
fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
&transaction.transparent_coins
}
}
#[must_use]
pub fn pool_upgrade(pool: ShieldedPool) -> consensus::NetworkUpgrade {
match pool {
ShieldedPool::Sapling => consensus::NetworkUpgrade::Sapling,
ShieldedPool::Orchard => consensus::NetworkUpgrade::Nu5,
ShieldedPool::Ironwood => consensus::NetworkUpgrade::Nu6_3,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PoolActivation(BlockHeight);
impl PoolActivation {
pub fn of(
consensus_parameters: &impl consensus::Parameters,
pool: ShieldedPool,
) -> Option<Self> {
consensus_parameters
.activation_height(pool_upgrade(pool))
.map(Self)
}
#[must_use]
pub fn height(&self) -> BlockHeight {
self.0
}
#[must_use]
pub fn max_with(&self, height: BlockHeight) -> BlockHeight {
self.0.max(height)
}
#[cfg(any(test, feature = "test-features"))]
#[must_use]
pub fn new_for_test(height: BlockHeight) -> Self {
Self(height)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sapling;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Orchard;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ironwood;
#[derive(Debug, Clone)]
pub struct WalletNote<N, Nf: Copy, P> {
pub(crate) output_id: OutputId,
pub(crate) key_id: KeyId,
pub(crate) note: N,
pub(crate) nullifier: Option<Nf>, pub(crate) position: Option<Position>,
pub(crate) memo: Memo,
pub(crate) spending_transaction: Option<TxId>,
pub(crate) refetch_nullifier_ranges: Vec<Range<BlockHeight>>,
pub(crate) marker: PhantomData<P>,
}
pub type SaplingNote = WalletNote<sapling_crypto::Note, sapling_crypto::Nullifier, Sapling>;
impl OutputInterface for SaplingNote {
type KeyId = KeyId;
type Input = sapling_crypto::Nullifier;
const POOL_TYPE: PoolType = PoolType::Shielded(ShieldedPool::Sapling);
fn output_id(&self) -> OutputId {
self.output_id
}
fn key_id(&self) -> KeyId {
self.key_id
}
fn spending_transaction(&self) -> Option<TxId> {
self.spending_transaction
}
fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
self.spending_transaction = spending_transaction;
}
fn value(&self) -> u64 {
self.note.value().inner()
}
fn spend_link(&self) -> Option<Self::Input> {
self.nullifier
}
fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
transaction.sapling_nullifiers()
}
fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
&transaction.sapling_notes
}
}
impl NoteInterface for SaplingNote {
type ZcashNote = sapling_crypto::Note;
type Nullifier = Self::Input;
const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Sapling;
fn note(&self) -> &Self::ZcashNote {
&self.note
}
fn nullifier(&self) -> Option<Self::Nullifier> {
self.nullifier
}
fn position(&self) -> Option<Position> {
self.position
}
fn memo(&self) -> &Memo {
&self.memo
}
fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>] {
&self.refetch_nullifier_ranges
}
}
pub type OrchardNote = WalletNote<orchard::Note, orchard::note::Nullifier, Orchard>;
impl OutputInterface for OrchardNote {
type KeyId = KeyId;
type Input = orchard::note::Nullifier;
const POOL_TYPE: PoolType = PoolType::Shielded(ShieldedPool::Orchard);
fn output_id(&self) -> OutputId {
self.output_id
}
fn key_id(&self) -> KeyId {
self.key_id
}
fn spending_transaction(&self) -> Option<TxId> {
self.spending_transaction
}
fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
self.spending_transaction = spending_transaction;
}
fn value(&self) -> u64 {
self.note.value().inner()
}
fn spend_link(&self) -> Option<Self::Input> {
self.nullifier
}
fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
transaction.orchard_nullifiers()
}
fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
&transaction.orchard_notes
}
}
impl NoteInterface for OrchardNote {
type ZcashNote = orchard::Note;
type Nullifier = Self::Input;
const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Orchard;
fn note(&self) -> &Self::ZcashNote {
&self.note
}
fn nullifier(&self) -> Option<Self::Nullifier> {
self.spend_link()
}
fn position(&self) -> Option<Position> {
self.position
}
fn memo(&self) -> &Memo {
&self.memo
}
fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>] {
&self.refetch_nullifier_ranges
}
}
pub type IronwoodNote = WalletNote<orchard::Note, orchard::note::Nullifier, Ironwood>;
impl OutputInterface for IronwoodNote {
type KeyId = KeyId;
type Input = orchard::note::Nullifier;
const POOL_TYPE: PoolType = PoolType::Shielded(ShieldedPool::Ironwood);
fn output_id(&self) -> OutputId {
self.output_id
}
fn key_id(&self) -> KeyId {
self.key_id
}
fn spending_transaction(&self) -> Option<TxId> {
self.spending_transaction
}
fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
self.spending_transaction = spending_transaction;
}
fn value(&self) -> u64 {
self.note.value().inner()
}
fn spend_link(&self) -> Option<Self::Input> {
self.nullifier
}
fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
transaction.ironwood_nullifiers()
}
fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
&transaction.ironwood_notes
}
}
impl NoteInterface for IronwoodNote {
type ZcashNote = orchard::Note;
type Nullifier = Self::Input;
const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Ironwood;
fn note(&self) -> &Self::ZcashNote {
&self.note
}
fn nullifier(&self) -> Option<Self::Nullifier> {
self.spend_link()
}
fn position(&self) -> Option<Position> {
self.position
}
fn memo(&self) -> &Memo {
&self.memo
}
fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>] {
&self.refetch_nullifier_ranges
}
}
pub trait OutgoingNoteInterface: Sized {
type ZcashNote;
type Address: Clone + Copy + Debug + PartialEq + Eq;
type Error: Debug + std::error::Error;
const SHIELDED_PROTOCOL: ShieldedPool;
fn output_id(&self) -> OutputId;
fn key_id(&self) -> KeyId;
fn value(&self) -> u64;
fn note(&self) -> &Self::ZcashNote;
fn memo(&self) -> &Memo;
fn recipient(&self) -> Self::Address;
fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress>;
fn encoded_recipient<P>(&self, parameters: &P) -> Result<String, Self::Error>
where
P: consensus::Parameters + consensus::NetworkConstants;
fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
where
P: consensus::Parameters + consensus::NetworkConstants;
fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self];
}
#[derive(Debug, Clone, PartialEq)]
pub struct OutgoingNote<N, P> {
pub(crate) output_id: OutputId,
pub(crate) key_id: KeyId,
pub(crate) note: N,
pub(crate) memo: Memo,
pub(crate) recipient_full_unified_address: Option<UnifiedAddress>,
pub(crate) marker: PhantomData<P>,
}
pub type OutgoingSaplingNote = OutgoingNote<sapling_crypto::Note, Sapling>;
impl OutgoingNoteInterface for OutgoingSaplingNote {
type ZcashNote = sapling_crypto::Note;
type Address = sapling_crypto::PaymentAddress;
type Error = Infallible;
const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Sapling;
fn output_id(&self) -> OutputId {
self.output_id
}
fn key_id(&self) -> KeyId {
self.key_id
}
fn value(&self) -> u64 {
self.note.value().inner()
}
fn note(&self) -> &Self::ZcashNote {
&self.note
}
fn memo(&self) -> &Memo {
&self.memo
}
fn recipient(&self) -> Self::Address {
self.note.recipient()
}
fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress> {
self.recipient_full_unified_address.as_ref()
}
fn encoded_recipient<P>(&self, consensus_parameters: &P) -> Result<String, Self::Error>
where
P: consensus::Parameters + consensus::NetworkConstants,
{
Ok(encode_payment_address(
zcash_address::constants::hrp_sapling_payment_address(
consensus_parameters.network_type(),
),
&self.note().recipient(),
))
}
fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
where
P: consensus::Parameters + consensus::NetworkConstants,
{
self.recipient_full_unified_address
.as_ref()
.map(|unified_address| unified_address.encode(consensus_parameters))
}
fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self] {
&transaction.outgoing_sapling_notes
}
}
pub type OutgoingOrchardNote = OutgoingNote<orchard::Note, Orchard>;
impl OutgoingNoteInterface for OutgoingOrchardNote {
type ZcashNote = orchard::Note;
type Address = orchard::Address;
type Error = ParseError;
const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Orchard;
fn output_id(&self) -> OutputId {
self.output_id
}
fn key_id(&self) -> KeyId {
self.key_id
}
fn value(&self) -> u64 {
self.note.value().inner()
}
fn note(&self) -> &Self::ZcashNote {
&self.note
}
fn memo(&self) -> &Memo {
&self.memo
}
fn recipient(&self) -> Self::Address {
self.note.recipient()
}
fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress> {
self.recipient_full_unified_address.as_ref()
}
fn encoded_recipient<P>(&self, parameters: &P) -> Result<String, Self::Error>
where
P: consensus::Parameters + consensus::NetworkConstants,
{
keys::encode_orchard_receiver(parameters, &self.note().recipient())
}
fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
where
P: consensus::Parameters + consensus::NetworkConstants,
{
self.recipient_full_unified_address
.as_ref()
.map(|unified_address| unified_address.encode(consensus_parameters))
}
fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self] {
&transaction.outgoing_orchard_notes
}
}
pub type OutgoingIronwoodNote = OutgoingNote<orchard::Note, Ironwood>;
impl OutgoingNoteInterface for OutgoingIronwoodNote {
type ZcashNote = orchard::Note;
type Address = orchard::Address;
type Error = ParseError;
const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Ironwood;
fn output_id(&self) -> OutputId {
self.output_id
}
fn key_id(&self) -> KeyId {
self.key_id
}
fn value(&self) -> u64 {
self.note.value().inner()
}
fn note(&self) -> &Self::ZcashNote {
&self.note
}
fn memo(&self) -> &Memo {
&self.memo
}
fn recipient(&self) -> Self::Address {
self.note.recipient()
}
fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress> {
self.recipient_full_unified_address.as_ref()
}
fn encoded_recipient<P>(&self, parameters: &P) -> Result<String, Self::Error>
where
P: consensus::Parameters + consensus::NetworkConstants,
{
keys::encode_orchard_receiver(parameters, &self.note().recipient())
}
fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
where
P: consensus::Parameters + consensus::NetworkConstants,
{
self.recipient_full_unified_address
.as_ref()
.map(|unified_address| unified_address.encode(consensus_parameters))
}
fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self] {
&transaction.outgoing_ironwood_notes
}
}
#[cfg(any(test, feature = "test-features"))]
pub fn strip_pool_tracking_for_test<W>(wallet: &mut W, pool: ShieldedPool) -> Result<(), W::Error>
where
W: traits::SyncWallet
+ traits::SyncBlocks
+ traits::SyncTransactions
+ traits::SyncNullifiers
+ traits::SyncShardTrees,
{
for block in wallet.get_wallet_blocks_mut()?.values_mut() {
let tree_bounds = &mut block.tree_bounds;
match pool {
ShieldedPool::Sapling => {
tree_bounds.sapling_initial_tree_size = 0;
tree_bounds.sapling_final_tree_size = 0;
}
ShieldedPool::Orchard => {
tree_bounds.orchard_initial_tree_size = 0;
tree_bounds.orchard_final_tree_size = 0;
}
ShieldedPool::Ironwood => {
tree_bounds.ironwood_initial_tree_size = 0;
tree_bounds.ironwood_final_tree_size = 0;
}
}
}
for transaction in wallet.get_wallet_transactions_mut()?.values_mut() {
match pool {
ShieldedPool::Sapling => {
transaction.sapling_notes.clear();
transaction.outgoing_sapling_notes.clear();
}
ShieldedPool::Orchard => {
transaction.orchard_notes.clear();
transaction.outgoing_orchard_notes.clear();
}
ShieldedPool::Ironwood => {
transaction.ironwood_notes.clear();
transaction.outgoing_ironwood_notes.clear();
}
}
}
let nullifiers = wallet.get_nullifiers_mut()?;
match pool {
ShieldedPool::Sapling => nullifiers.sapling.clear(),
ShieldedPool::Orchard => nullifiers.orchard.clear(),
ShieldedPool::Ironwood => nullifiers.ironwood.clear(),
}
let empty = ShardTrees::new();
let shard_trees = wallet.get_shard_trees_mut()?;
match pool {
ShieldedPool::Sapling => shard_trees.sapling = empty.sapling,
ShieldedPool::Orchard => shard_trees.orchard = empty.orchard,
ShieldedPool::Ironwood => shard_trees.ironwood = empty.ironwood,
}
let sync_state = wallet.get_sync_state_mut()?;
match pool {
ShieldedPool::Sapling => sync_state.sapling_shard_ranges.clear(),
ShieldedPool::Orchard => sync_state.orchard_shard_ranges.clear(),
ShieldedPool::Ironwood => sync_state.ironwood_shard_ranges.clear(),
}
Ok(())
}
pub type SaplingShardStore = MemoryShardStore<sapling_crypto::Node, BlockHeight>;
pub type OrchardShardStore = MemoryShardStore<MerkleHashOrchard, BlockHeight>;
#[derive(Debug)]
pub struct ShardTrees {
pub sapling: ShardTree<
SaplingShardStore,
{ sapling_crypto::NOTE_COMMITMENT_TREE_DEPTH },
{ witness::SHARD_HEIGHT },
>,
pub orchard: ShardTree<
OrchardShardStore,
{ orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
{ witness::SHARD_HEIGHT },
>,
pub ironwood: ShardTree<
OrchardShardStore,
{ orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
{ witness::SHARD_HEIGHT },
>,
}
impl ShardTrees {
#[must_use]
pub fn new() -> Self {
Self {
sapling: empty_shard_tree(),
orchard: empty_shard_tree(),
ironwood: empty_shard_tree(),
}
}
pub(crate) fn clear_pool(&mut self, pool: ShieldedPool) {
tracing::info!("Clearing {pool:?} shard tree.");
match pool {
ShieldedPool::Sapling => self.sapling = empty_shard_tree(),
ShieldedPool::Orchard => self.orchard = empty_shard_tree(),
ShieldedPool::Ironwood => self.ironwood = empty_shard_tree(),
}
}
}
impl Default for ShardTrees {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn empty_shard_tree<H, const DEPTH: u8, const SHARD_HEIGHT: u8>()
-> ShardTree<MemoryShardStore<H, BlockHeight>, DEPTH, SHARD_HEIGHT>
where
H: incrementalmerkletree::Hashable + Clone + PartialEq,
{
let mut tree = ShardTree::new(
MemoryShardStore::empty(),
SHARDTREE_CHECKPOINT_ROLLING_WINDOW_SIZE as usize,
);
assert_eq!(
tree.append_checkpoint(BlockHeight::from_u32(0))
.expect("should never fail"),
CheckpointAppendOutcome::Appended
);
tree
}
use shardtree::store::ShardStore;
pub(crate) trait SyncDomain {
const SHIELDED_PROTOCOL: ShieldedPool;
type Note: NoteInterface;
type ShardStore: ShardStore<CheckpointId = BlockHeight>;
fn notes_mut(wallet_transaction: &mut WalletTransaction) -> Vec<&mut Self::Note>;
}
impl SyncDomain for Sapling {
const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Sapling;
type Note = SaplingNote;
type ShardStore = SaplingShardStore;
fn notes_mut(wallet_transaction: &mut WalletTransaction) -> Vec<&mut Self::Note> {
wallet_transaction.sapling_notes_mut()
}
}
impl SyncDomain for Orchard {
const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Orchard;
type Note = OrchardNote;
type ShardStore = OrchardShardStore;
fn notes_mut(wallet_transaction: &mut WalletTransaction) -> Vec<&mut Self::Note> {
wallet_transaction.orchard_notes_mut()
}
}
impl SyncDomain for Ironwood {
const SHIELDED_PROTOCOL: ShieldedPool = ShieldedPool::Ironwood;
type Note = IronwoodNote;
type ShardStore = OrchardShardStore;
fn notes_mut(wallet_transaction: &mut WalletTransaction) -> Vec<&mut Self::Note> {
wallet_transaction.ironwood_notes_mut()
}
}
#[cfg(test)]
mod test {
use zcash_protocol::TxId;
use zcash_protocol::consensus::BlockHeight;
use zingo_status::confirmation_status::ConfirmationStatus;
use super::WalletTransaction;
fn transaction_with_status(status: ConfirmationStatus) -> WalletTransaction {
WalletTransaction::new_for_test(TxId::from_bytes([0; 32]), status)
}
#[test]
fn update_status_follows_transaction_lifecycle() {
let mut transaction =
transaction_with_status(ConfirmationStatus::Calculated(BlockHeight::from_u32(10)));
transaction.update_status(
ConfirmationStatus::Transmitted(BlockHeight::from_u32(10)),
1,
false,
);
assert!(matches!(
transaction.status(),
ConfirmationStatus::Transmitted(_)
));
transaction.update_status(
ConfirmationStatus::Mempool(BlockHeight::from_u32(11)),
2,
false,
);
assert!(matches!(
transaction.status(),
ConfirmationStatus::Mempool(_)
));
transaction.update_status(
ConfirmationStatus::Confirmed(BlockHeight::from_u32(11)),
3,
false,
);
assert!(matches!(
transaction.status(),
ConfirmationStatus::Confirmed(_)
));
}
#[test]
fn update_status_failed_returns_to_mempool() {
let mut transaction =
transaction_with_status(ConfirmationStatus::Failed(BlockHeight::from_u32(10)));
transaction.update_status(
ConfirmationStatus::Mempool(BlockHeight::from_u32(11)),
1,
false,
);
assert!(matches!(
transaction.status(),
ConfirmationStatus::Mempool(_)
));
}
#[test]
fn update_status_failed_is_not_directly_confirmable() {
let mut transaction =
transaction_with_status(ConfirmationStatus::Failed(BlockHeight::from_u32(10)));
transaction.update_status(
ConfirmationStatus::Confirmed(BlockHeight::from_u32(11)),
1,
false,
);
assert!(matches!(
transaction.status(),
ConfirmationStatus::Failed(_)
));
}
#[test]
fn update_status_confirmed_is_terminal() {
let mut transaction =
transaction_with_status(ConfirmationStatus::Confirmed(BlockHeight::from_u32(10)));
transaction.update_status(
ConfirmationStatus::Mempool(BlockHeight::from_u32(11)),
1,
false,
);
assert!(matches!(
transaction.status(),
ConfirmationStatus::Confirmed(_)
));
transaction.update_status(
ConfirmationStatus::Failed(BlockHeight::from_u32(11)),
2,
false,
);
assert!(matches!(
transaction.status(),
ConfirmationStatus::Confirmed(_)
));
}
#[test]
fn update_status_forces_confirmed_to_fail() {
let mut transaction =
transaction_with_status(ConfirmationStatus::Confirmed(BlockHeight::from_u32(10)));
transaction.update_status(
ConfirmationStatus::Failed(BlockHeight::from_u32(11)),
2,
true,
);
assert!(matches!(
transaction.status(),
ConfirmationStatus::Failed(_)
));
}
}