diff --git a/client/src/client.rs b/client/src/client.rs index edb971b4..85c301be 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -42,7 +42,6 @@ pub use crate::event::Loading; pub use crate::handle; pub use crate::service::Service; -use crate::event::Mapper; use crate::peer; use nakamoto_net::{Reactor, Waker}; @@ -216,10 +215,7 @@ impl Client { p.emit((filter, block, height)); } }); - let (publisher, subscriber) = event::broadcast({ - let mut mapper = Mapper::default(); - move |e, p| mapper.process(e, p) - }); + let (publisher, subscriber) = event::broadcast(|e, p| p.emit(e)); let publisher = Publisher::default() .register(event_pub) @@ -703,10 +699,7 @@ impl handle::Handle for Handle { None => event::wait( &events, |e| match e { - Event::BlockHeadersImported { - result: ImportResult::TipChanged { height, hash, .. }, - .. - } if height == h => Some(hash), + Event::BlockHeadersImported { height, hash, .. } if height == h => Some(hash), _ => None, }, self.timeout, diff --git a/client/src/event.rs b/client/src/event.rs index 0d571c83..8fa64057 100644 --- a/client/src/event.rs +++ b/client/src/event.rs @@ -1,12 +1,8 @@ //! Client events. #![allow(clippy::manual_range_contains)] -use std::collections::HashSet; use std::fmt; -use nakamoto_common::block::{Block, BlockHash, Height}; -use nakamoto_net::event::Emitter; -use nakamoto_p2p::fsm; -use nakamoto_p2p::fsm::Event; +use nakamoto_common::block::Height; /// Event emitted by the client during the "loading" phase. #[derive(Clone, Debug)] @@ -35,149 +31,18 @@ impl fmt::Display for Loading { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::BlockHeaderLoaded { height } => { - write!(fmt, "block header #{} loaded", height) + write!(fmt, "Block header #{} loaded", height) } Self::FilterHeaderLoaded { height } => { - write!(fmt, "filter header #{} loaded", height) + write!(fmt, "Filter header #{} loaded", height) } Self::FilterHeaderVerified { height } => { - write!(fmt, "filter header #{} verified", height) + write!(fmt, "Filter header #{} verified", height) } } } } -/// Event mapper for client events. -/// Consumes raw state machine events and emits [`Event`]. -pub(crate) struct Mapper { - /// Best height known. - tip: Height, - /// The height up to which we've processed filters and matching blocks. - sync_height: Height, - /// The height up to which we've processed filters. - /// This is usually going to be greater than `sync_height`. - filter_height: Height, - /// The height up to which we've processed matching blocks. - /// This is always going to be lesser or equal to `filter_height`. - block_height: Height, - /// Filter heights that have been matched, and for which we are awaiting a block to process. - pending: HashSet, -} - -impl Default for Mapper { - /// Create a new client event mapper. - fn default() -> Self { - let tip = 0; - let sync_height = 0; - let filter_height = 0; - let block_height = 0; - let pending = HashSet::new(); - - Self { - tip, - sync_height, - filter_height, - block_height, - pending, - } - } -} - -impl Mapper { - /// Process protocol event and map it to client event(s). - pub fn process(&mut self, event: fsm::Event, emitter: &Emitter) { - match event { - Event::BlockHeadersSynced { height, .. } => { - self.tip = height; - emitter.emit(event); - } - Event::BlockProcessed { - block, - height, - fees, - } => { - let hash = self.process_block(block, height); - - if let Some(fees) = fees { - emitter.emit(Event::FeeEstimated { - block: hash, - height, - fees, - }); - } - } - Event::FilterRescanStarted { start, .. } => { - self.pending.clear(); - - self.filter_height = start; - self.sync_height = start; - self.block_height = start; - } - Event::FilterProcessed { - height, - matched, - valid: true, - .. - } => { - debug_assert!(height >= self.filter_height); - - if matched { - log::debug!("Filter matched for block #{}", height); - self.pending.insert(height); - } - self.filter_height = height; - emitter.emit(event); - } - other => emitter.emit(other), - } - assert!( - self.block_height <= self.filter_height, - "Filters are processed before blocks" - ); - assert!( - self.sync_height <= self.filter_height, - "Filters are processed before we are done" - ); - - // If we have no blocks left to process, we are synced to the height of the last - // processed filter. Otherwise, we're synced up to the last processed block. - let height = if self.pending.is_empty() { - self.filter_height - } else { - self.block_height - }; - - // Ensure we only broadcast sync events when the sync height has changed. - if height > self.sync_height { - self.sync_height = height; - - emitter.emit(Event::Synced { - height, - tip: self.tip, - }); - } - } - - // PRIVATE METHODS ///////////////////////////////////////////////////////// - - // TODO: Instead of receiving the block, fetch it if matched. - fn process_block(&mut self, block: Block, height: Height) -> BlockHash { - let hash = block.block_hash(); - - if !self.pending.remove(&height) { - // Received unexpected block. - return hash; - } - - log::debug!("Received block {} at height {}", hash, height); - debug_assert!(height >= self.block_height); - - self.block_height = height; - - hash - } -} - #[cfg(test)] mod test { //! Properties of the [`client::Client`] we'd like to test. @@ -220,16 +85,13 @@ mod test { //! use std::io; - use quickcheck::TestResult; - use quickcheck_macros::quickcheck; - use nakamoto_common::block::time::Clock as _; use nakamoto_common::network::Network; use nakamoto_net::{Disconnect, Link, LocalTime, StateMachine as _}; + use nakamoto_p2p::fsm; + use nakamoto_p2p::Event; use nakamoto_test::assert_matches; - use nakamoto_test::block::gen; - use super::Event; use super::*; use crate::handle::Handle as _; @@ -412,82 +274,4 @@ mod test { if addr == remote && height == 42 && user_agent == "?" ); } - - #[quickcheck] - fn prop_client_side_filtering(birth: Height, height: Height, seed: u64) -> TestResult { - if height < 1 || height > 24 || birth >= height { - return TestResult::discard(); - } - - let mut rng = fastrand::Rng::with_seed(seed); - let network = Network::Regtest; - let genesis = network.genesis_block(); - let chain = gen::blockchain(genesis, height, &mut rng); - let mut mock = mock::Client::new(network); - let mut client = mock.handle(); - - client.tip = (height, chain[height as usize].header, Default::default()); - - let mut spent = 0; - let (watch, heights, balance) = gen::watchlist_rng(birth, chain.iter(), &mut rng); - - log::debug!( - "-- Test case with birth = {} and height = {}", - birth, - height - ); - let subscriber = client.events(); - - mock.subscriber.broadcast(fsm::Event::BlockHeadersSynced { - hash: chain.last().block_hash(), - height, - }); - - for h in birth..=height { - let matched = heights.contains(&h); - let block = chain[h as usize].clone(); - - mock.subscriber.broadcast(fsm::Event::FilterProcessed { - block: block.block_hash(), - height: h, - matched, - cached: false, - valid: true, - }); - - if matched { - mock.subscriber - .broadcast(fsm::Event::BlockMatched { block, height: h }); - } - } - - for event in subscriber.try_iter() { - match event { - Event::BlockMatched { block, .. } => { - for t in &block.txdata { - for output in &t.output { - if watch.contains(&output.script_pubkey) { - spent += output.value; - } - } - } - } - Event::Synced { - height: sync_height, - tip, - } => { - assert_eq!(height, tip); - - if sync_height == tip { - break; - } - } - _ => {} - } - } - assert_eq!(balance, spent); - client.shutdown().unwrap(); - - TestResult::passed() - } } diff --git a/client/src/tests/mock.rs b/client/src/tests/mock.rs index 4631d1ea..75877374 100644 --- a/client/src/tests/mock.rs +++ b/client/src/tests/mock.rs @@ -30,7 +30,6 @@ use nakamoto_p2p::fsm::Peer; use nakamoto_p2p::fsm::StateMachine; use crate::client::{chan, Event, Loading}; -use crate::event::Mapper; use crate::handle::{self, Handle}; pub struct Client { @@ -103,8 +102,7 @@ impl Default for Client { let (blocks, blocks_) = chan::unbounded(); let (filters, filters_) = chan::unbounded(); let (commands_, commands) = chan::unbounded(); - let mut mapper = Mapper::default(); - let (subscriber, subscriber_) = event::broadcast(move |e, p| mapper.process(e, p)); + let (subscriber, subscriber_) = event::broadcast(|e, p| p.emit(e)); let loading = event::Emitter::default(); let network = Network::default(); let protocol = { diff --git a/p2p/src/fsm/cbfmgr.rs b/p2p/src/fsm/cbfmgr.rs index ca02e42b..e7581d83 100644 --- a/p2p/src/fsm/cbfmgr.rs +++ b/p2p/src/fsm/cbfmgr.rs @@ -4,6 +4,7 @@ //! mod rescan; +use std::collections::BTreeSet; use std::ops::{Bound, RangeInclusive}; use thiserror::Error; @@ -14,9 +15,9 @@ use nakamoto_common::bitcoin::network::message_filter::{CFHeaders, CFilter, GetC use nakamoto_common::bitcoin::{Script, Transaction, Txid}; use nakamoto_common::block::filter::{self, BlockFilter, Filters}; use nakamoto_common::block::time::{Clock, LocalDuration, LocalTime}; -use nakamoto_common::block::tree::{BlockReader, ImportResult}; +use nakamoto_common::block::tree::BlockReader; use nakamoto_common::block::{BlockHash, Height}; -use nakamoto_common::collections::{AddressBook, HashMap, HashSet}; +use nakamoto_common::collections::{AddressBook, HashMap}; use nakamoto_common::source; use super::event::TxStatus; @@ -128,7 +129,7 @@ pub struct FilterManager { /// We use this to figure out when to re-issue filter requests. last_processed: Option, /// Pending block requests. - pending_blocks: HashSet, + pending_blocks: BTreeSet, /// Inflight requests. inflight: HashMap, } @@ -154,7 +155,7 @@ impl FilterManager { outbox: Outbox::default(), clock, filters, - pending_blocks: HashSet::with_hasher(rng.clone().into()), + pending_blocks: BTreeSet::new(), inflight: HashMap::with_hasher(rng.into()), last_idle: None, last_processed: None, @@ -190,16 +191,24 @@ impl FilterManager { Event::BlockProcessed { block, height, .. } => { if self.pending_blocks.remove(&height) { self.outbox.event(Event::BlockMatched { block, height }); + + // Since blocks are processed and matched in-order, we know this is the latest + // block that has matched. + // If there are no pending blocks however, it means there are no other matches, + // therefore we can jump to the current rescan height. + let height = if self.pending_blocks.is_empty() { + self.rescan.current.max(height) + } else { + height + }; + self.outbox.event(Event::Scanned { height }); } } Event::BlockDisconnected { height, .. } => { // In case of a re-org, make sure we don't accept old blocks that were requested. self.pending_blocks.remove(&height); } - Event::BlockHeadersImported { - result: ImportResult::TipChanged { reverted, .. }, - .. - } => { + Event::BlockHeadersImported { reverted, .. } => { // Nb. the reverted blocks are ordered from the tip down to // the oldest ancestor. if let Some((height, _)) = reverted.last() { @@ -270,8 +279,21 @@ impl FilterManager { } } NetworkMessage::CFilter(msg) => { - match self.received_cfilter(&from, msg.clone(), tree, blocks) { - Ok(_) => {} + match self.received_cfilter(&from, msg.clone(), tree) { + Ok(matches) => { + for (height, hash) in matches { + if self.pending_blocks.insert(height) { + blocks.get_block(hash); + } + } + // Filters being processed only updates our progress if there are no + // pending blocks. Otherwise we have to wait for the block to arrive. + if self.pending_blocks.is_empty() { + self.outbox.event(Event::Scanned { + height: self.rescan.current, + }); + } + } Err(Error::InvalidMessage { from, .. }) => { self.outbox.event(Event::PeerMisbehaved { addr: from, @@ -755,12 +777,11 @@ impl FilterManager { /// Handle a `cfilter` message. /// /// Returns a list of blocks that need to be fetched from the network. - fn received_cfilter( + fn received_cfilter( &mut self, from: &PeerId, msg: CFilter, tree: &T, - blocks: &mut B, ) -> Result, Error> { let from = *from; @@ -824,11 +845,6 @@ impl FilterManager { if processed > 0 { self.last_processed = Some(self.clock.local_time()); } - for (height, hash) in &matches { - if self.pending_blocks.insert(*height) { - blocks.get_block(*hash); - } - } return Ok(matches); } else { // Unsolicited filter. @@ -1192,7 +1208,7 @@ mod tests { // Now import the filters. for msg in cfilters { - cbfmgr.received_cfilter(peer, msg, &tree, &mut ()).unwrap(); + cbfmgr.received_cfilter(peer, msg, &tree).unwrap(); } } @@ -1379,12 +1395,7 @@ mod tests { // Receive a filter, but not one that we can process immediately. cbfmgr - .received_cfilter( - &remote, - cfilters[birth as usize + 1].clone(), - &tree, - &mut (), - ) + .received_cfilter(&remote, cfilters[birth as usize + 1].clone(), &tree) .unwrap(); assert!(cbfmgr.last_processed.is_none()); @@ -1392,7 +1403,7 @@ mod tests { // Receive a filter, that we can process immediately. cbfmgr - .received_cfilter(&remote, cfilters[birth as usize].clone(), &tree, &mut ()) + .received_cfilter(&remote, cfilters[birth as usize].clone(), &tree) .unwrap(); // We should be futher along now. @@ -1417,7 +1428,7 @@ mod tests { .expect("`getcfilters` sent"); cbfmgr - .received_cfilter(&remote, cfilters[current as usize].clone(), &tree, &mut ()) + .received_cfilter(&remote, cfilters[current as usize].clone(), &tree) .unwrap(); assert_eq!(cbfmgr.rescan.current, current + 1); } @@ -1520,9 +1531,7 @@ mod tests { ); for msg in util::cfilters(chain.iter().take(best as usize + 1)) { - cbfmgr - .received_cfilter(&remote, msg, &tree, &mut ()) - .unwrap(); + cbfmgr.received_cfilter(&remote, msg, &tree).unwrap(); } assert_eq!(cbfmgr.rescan.cache.start(), Some(birth)); assert_eq!(cbfmgr.rescan.cache.end(), Some(best)); @@ -1614,9 +1623,7 @@ mod tests { ); for msg in util::cfilters(chain.iter().take(best as usize + 1)) { - cbfmgr - .received_cfilter(&remote, msg, &tree, &mut ()) - .unwrap(); + cbfmgr.received_cfilter(&remote, msg, &tree).unwrap(); } assert_eq!(cbfmgr.rescan.cache.start(), Some(birth)); assert_eq!(cbfmgr.rescan.cache.end(), Some(best)); @@ -1652,9 +1659,7 @@ mod tests { // 7. Receive #6. let msg = util::cfilters(iter::once(missing)).next().unwrap(); - cbfmgr - .received_cfilter(&remote, msg, &tree, &mut ()) - .unwrap(); + cbfmgr.received_cfilter(&remote, msg, &tree).unwrap(); // 8. Expect that 6 to 8 are processed and 7 and 8 come from the cache. let mut events = output::test::events(cbfmgr.outbox.drain()) @@ -1732,9 +1737,7 @@ mod tests { ); for msg in util::cfilters(chain.iter().take(best as usize + 1)) { - cbfmgr - .received_cfilter(&remote, msg, &tree, &mut ()) - .unwrap(); + cbfmgr.received_cfilter(&remote, msg, &tree).unwrap(); } assert_eq!(cbfmgr.rescan.cache.start(), Some(birth)); assert_eq!(cbfmgr.rescan.cache.end(), Some(best)); @@ -1826,9 +1829,7 @@ mod tests { let msg = util::cfilters(iter::once(&chain[height as usize])) .next() .unwrap(); - cbfmgr - .received_cfilter(&remote, msg, &tree, &mut ()) - .unwrap(); + cbfmgr.received_cfilter(&remote, msg, &tree).unwrap(); } // Drain the message queue so we can check what is coming from the next rescan. cbfmgr.outbox.drain().for_each(drop); @@ -1852,9 +1853,7 @@ mod tests { // 4. Receive some of the missing filters. for msg in util::cfilters([&chain[5], &chain[7], &chain[9]].into_iter()) { - cbfmgr - .received_cfilter(&remote, msg, &tree, &mut ()) - .unwrap(); + cbfmgr.received_cfilter(&remote, msg, &tree).unwrap(); } let mut events = output::test::events(cbfmgr.outbox.drain()) @@ -1908,9 +1907,7 @@ mod tests { assert!(matched.is_empty()); for msg in util::cfilters(chain.iter().take(best as usize + 1)) { - cbfmgr - .received_cfilter(&remote, msg, &tree, &mut ()) - .unwrap(); + cbfmgr.received_cfilter(&remote, msg, &tree).unwrap(); } assert_eq!(cbfmgr.rescan.cache.len(), (best - birth) as usize + 1); assert_eq!(cbfmgr.rescan.cache.start(), Some(birth)); @@ -2008,9 +2005,7 @@ mod tests { // First let's catch up the client with filters up to the sync height. for filter in util::cfilters(chain.iter().take(sync_height as usize + 1)) { - cbfmgr - .received_cfilter(&remote, filter, &tree, &mut ()) - .unwrap(); + cbfmgr.received_cfilter(&remote, filter, &tree).unwrap(); } assert_eq!(cbfmgr.rescan.current, sync_height + 1); cbfmgr.outbox.drain().for_each(drop); @@ -2191,9 +2186,7 @@ mod tests { let mut matches = Vec::new(); for (_, filter) in filters.into_iter() { - let hashes = cbfmgr - .received_cfilter(&remote, filter, &tree, &mut ()) - .unwrap(); + let hashes = cbfmgr.received_cfilter(&remote, filter, &tree).unwrap(); matches.extend( hashes diff --git a/p2p/src/fsm/event.rs b/p2p/src/fsm/event.rs index 7b036c69..9f57e9da 100644 --- a/p2p/src/fsm/event.rs +++ b/p2p/src/fsm/event.rs @@ -7,8 +7,8 @@ use nakamoto_common::bitcoin::network::constants::ServiceFlags; use nakamoto_common::bitcoin::network::message::NetworkMessage; use nakamoto_common::bitcoin::{Transaction, Txid}; use nakamoto_common::block::filter::BlockFilter; -use nakamoto_common::block::tree::ImportResult; use nakamoto_common::block::{Block, BlockHash, BlockHeader, Height}; +use nakamoto_common::nonempty::NonEmpty; use nakamoto_common::p2p::peer::Source; use nakamoto_net::Disconnect; @@ -152,8 +152,14 @@ pub enum Event { /// Block headers imported. Emitted when headers are fetched from peers, /// or imported by the user. BlockHeadersImported { - /// Import result, - result: ImportResult, + /// New tip hash. + hash: BlockHash, + /// New tip height. + height: Height, + /// Block headers connected to the active chain. + connected: NonEmpty<(Height, BlockHeader)>, + /// Block headers reverted from the active chain. + reverted: Vec<(Height, BlockHeader)>, /// Set if this import triggered a chain reorganization. reorg: bool, }, @@ -217,6 +223,11 @@ pub enum Event { /// The new transaction status. status: TxStatus, }, + /// Scanned the chain up to a certain height. + Scanned { + /// Height up to which we've scanned and processed blocks. + height: Height, + }, /// A gossip message was received from a peer. MessageReceived { /// Peer that sent the message. @@ -226,17 +237,6 @@ pub enum Event { }, /// Address book exhausted. AddressBookExhausted, - /// Compact filters have been synced and processed up to this point and matching blocks have - /// been fetched. - /// - /// If filters have been processed up to the last block in the client's header chain, `height` - /// and `tip` will be equal. - Synced { - /// Height up to which we are synced. - height: Height, - /// Tip of our block header chain. - tip: Height, - }, /// An error occured. Error { /// Error source. @@ -260,20 +260,16 @@ impl fmt::Display for Event { ) } Self::BlockHeadersImported { - result: ImportResult::TipChanged { hash, height, .. }, + hash, + height, reorg, + .. } => { write!( fmt, "Chain tip updated to {hash} at height {height} (reorg={reorg})" ) } - Self::BlockHeadersImported { - result: ImportResult::TipUnchanged, - .. - } => { - write!(fmt, "Chain tip unchanged during import") - } Self::BlockConnected { header, height, .. } => { write!( fmt, @@ -338,7 +334,7 @@ impl fmt::Display for Event { Self::TxStatusChanged { txid, status } => { write!(fmt, "Transaction {} status changed: {}", txid, status) } - Self::Synced { height, .. } => write!(fmt, "filters synced up to height {}", height), + Self::Scanned { height, .. } => write!(fmt, "Chain scanned up to height {height}"), Self::PeerConnected { addr, link, .. } => { write!(fmt, "Peer {} connected ({:?})", &addr, link) } diff --git a/p2p/src/fsm/invmgr.rs b/p2p/src/fsm/invmgr.rs index 5c88f393..f3c802bf 100644 --- a/p2p/src/fsm/invmgr.rs +++ b/p2p/src/fsm/invmgr.rs @@ -24,7 +24,6 @@ use std::collections::BTreeMap; use nakamoto_common::bitcoin::network::message::NetworkMessage; use nakamoto_common::bitcoin::network::{constants::ServiceFlags, message_blockdata::Inventory}; use nakamoto_common::bitcoin::{Block, BlockHash, Transaction, Txid, Wtxid}; -use nakamoto_common::block::tree::ImportResult; // TODO: Timeout should be configurable // TODO: Add exponential back-off @@ -171,10 +170,7 @@ impl InventoryManager { Event::PeerDisconnected { addr, .. } => { self.peers.remove(&addr); } - Event::BlockHeadersImported { - result: ImportResult::TipChanged { reverted, .. }, - .. - } => { + Event::BlockHeadersImported { reverted, .. } => { for (height, _) in reverted { self.block_reverted(height); } diff --git a/p2p/src/fsm/syncmgr.rs b/p2p/src/fsm/syncmgr.rs index d85bee97..60bda438 100644 --- a/p2p/src/fsm/syncmgr.rs +++ b/p2p/src/fsm/syncmgr.rs @@ -245,14 +245,13 @@ impl SyncManager { ) -> Result { let result = tree.import_blocks(blocks, &self.clock); - if let Ok( - result @ ImportResult::TipChanged { - hash, - reverted, - connected, - .. - }, - ) = &result + if let Ok(ImportResult::TipChanged { + hash, + height, + reverted, + connected, + .. + }) = &result { let reorg = !reverted.is_empty(); @@ -265,7 +264,10 @@ impl SyncManager { } self.outbox.event(Event::BlockHeadersImported { reorg, - result: result.clone(), + hash: *hash, + height: *height, + connected: connected.clone(), + reverted: reverted.clone(), }); self.broadcast_tip(hash, tree); } diff --git a/p2p/src/fsm/tests.rs b/p2p/src/fsm/tests.rs index 0719deab..4d679b8e 100644 --- a/p2p/src/fsm/tests.rs +++ b/p2p/src/fsm/tests.rs @@ -22,7 +22,6 @@ use super::{PROTOCOL_VERSION, USER_AGENT}; use peer::{Peer, PeerDummy}; -use nakamoto_chain::ImportResult; use nakamoto_common::bitcoin::network::message_blockdata::GetHeadersMessage; use nakamoto_common::bitcoin::network::message_blockdata::Inventory; use nakamoto_common::bitcoin::network::message_filter::CFilter; @@ -1504,7 +1503,7 @@ fn test_block_events() { } assert_matches!( events.next().unwrap(), - Event::BlockHeadersImported { result: ImportResult::TipChanged { height, .. }, .. } + Event::BlockHeadersImported { height, .. } if height == best ); assert_eq!(events.count(), 0); @@ -1525,7 +1524,7 @@ fn test_block_events() { ); assert_matches!( events.next().unwrap(), - Event::BlockHeadersImported { result: ImportResult::TipChanged { height, .. }, .. } + Event::BlockHeadersImported { height, .. } if height == best + 1 ); assert_eq!(0, events.count()); @@ -1566,7 +1565,7 @@ fn test_block_events() { assert_matches!( events.next().unwrap(), - Event::BlockHeadersImported { result: ImportResult::TipChanged { height, .. }, .. } + Event::BlockHeadersImported { height, .. } if height == fork_best ); assert!(events.next().is_none()); diff --git a/rust-toolchain b/rust-toolchain index 5b6cd6b3..9cf4011b 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.65 +1.66 diff --git a/wallet/src/wallet.rs b/wallet/src/wallet.rs index 0cf2cd42..62a2bff1 100644 --- a/wallet/src/wallet.rs +++ b/wallet/src/wallet.rs @@ -26,6 +26,12 @@ pub use ui::Ui; pub type Utxos = Vec<(OutPoint, TxOut)>; +#[derive(Default)] +pub struct Tips { + header: Height, + cfilter: Height, +} + /// Wallet state. pub struct Wallet { client: H, @@ -34,6 +40,7 @@ pub struct Wallet { hw: Hw, network: client::Network, watch: HashSet
, + tips: Tips, } impl Wallet { @@ -46,6 +53,7 @@ impl Wallet { network, watch: HashSet::new(), ui: Ui::default(), + tips: Tips::default(), } } @@ -239,6 +247,10 @@ impl Wallet { } client::Event::FilterProcessed { height, .. } => { self.ui.handle_filter_processed(height); + self.tips.cfilter = height; + } + client::Event::BlockHeadersImported { height, .. } => { + self.tips.header = height; } client::Event::BlockMatched { block, height } => { for t in &block.txdata { @@ -254,9 +266,8 @@ impl Wallet { balance, ); } - // TODO: This should be called `Scanned`. - client::Event::Synced { height, tip } => { - self.ui.handle_synced(height, tip); + client::Event::Scanned { height, .. } => { + self.ui.handle_synced(height, self.tips.header); } _ => {} } diff --git a/wallet/src/wallet/ui.rs b/wallet/src/wallet/ui.rs index dba67d0c..279ce49c 100644 --- a/wallet/src/wallet/ui.rs +++ b/wallet/src/wallet/ui.rs @@ -268,7 +268,7 @@ enum Status { impl fmt::Display for Status { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn percent(a: f64, b: f64) -> String { - format!("{:.2}%", a as f64 / b as f64 * 100.) + format!("{:.2}%", a / b * 100.) } match self {