//! Compact Filters
//!
-//! This module contains a multithreaded implementation of an [`OnlineBlockchain`] backend that
+//! This module contains a multithreaded implementation of an [`Blockchain`] backend that
//! uses BIP157 (aka "Neutrino") to populate the wallet's [database](crate::database::Database)
//! by downloading compact filters from the P2P network.
//!
mod store;
mod sync;
-use super::{Blockchain, Capability, OnlineBlockchain, Progress};
+use super::{Blockchain, Capability, Progress};
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::error::Error;
use crate::types::{ScriptType, TransactionDetails, UTXO};
/// ## Example
/// See the [`blockchain::compact_filters`](crate::blockchain::compact_filters) module for a usage example.
#[derive(Debug)]
-pub struct CompactFiltersBlockchain(Option<CompactFilters>);
+pub struct CompactFiltersBlockchain {
+ peers: Vec<Arc<Peer>>,
+ headers: Arc<ChainStore<Full>>,
+ skip_blocks: Option<usize>,
+}
impl CompactFiltersBlockchain {
/// Construct a new instance given a list of peers, a path to store headers and block
/// in parallel. It's currently recommended to only connect to a single peer to avoid
/// inconsistencies in the data returned, optionally with multiple connections in parallel to
/// speed-up the sync process.
- pub fn new<P: AsRef<Path>>(
- peers: Vec<Peer>,
- storage_dir: P,
- skip_blocks: Option<usize>,
- ) -> Result<Self, CompactFiltersError> {
- Ok(CompactFiltersBlockchain(Some(CompactFilters::new(
- peers,
- storage_dir,
- skip_blocks,
- )?)))
- }
-}
-
-/// Internal struct that contains the state of a [`CompactFiltersBlockchain`]
-#[derive(Debug)]
-struct CompactFilters {
- peers: Vec<Arc<Peer>>,
- headers: Arc<ChainStore<Full>>,
- skip_blocks: Option<usize>,
-}
-
-impl CompactFilters {
- /// Constructor, see [`CompactFiltersBlockchain::new`] for the documentation
pub fn new<P: AsRef<Path>>(
peers: Vec<Peer>,
storage_dir: P,
headers.recover_snapshot(cf_name)?;
}
- Ok(CompactFilters {
+ Ok(CompactFiltersBlockchain {
peers: peers.into_iter().map(Arc::new).collect(),
headers,
skip_blocks,
}
impl Blockchain for CompactFiltersBlockchain {
- fn offline() -> Self {
- CompactFiltersBlockchain(None)
- }
-
- fn is_online(&self) -> bool {
- self.0.is_some()
- }
-}
-
-impl OnlineBlockchain for CompactFiltersBlockchain {
fn get_capabilities(&self) -> HashSet<Capability> {
vec![Capability::FullHistory].into_iter().collect()
}
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
- let inner = self.0.as_ref().ok_or(Error::OfflineClient)?;
- let first_peer = &inner.peers[0];
+ let first_peer = &self.peers[0];
- let skip_blocks = inner.skip_blocks.unwrap_or(0);
+ let skip_blocks = self.skip_blocks.unwrap_or(0);
- let cf_sync = Arc::new(CFSync::new(Arc::clone(&inner.headers), skip_blocks, 0x00)?);
+ let cf_sync = Arc::new(CFSync::new(Arc::clone(&self.headers), skip_blocks, 0x00)?);
- let initial_height = inner.headers.get_height()?;
+ let initial_height = self.headers.get_height()?;
let total_bundles = (first_peer.get_version().start_height as usize)
.checked_sub(skip_blocks)
.map(|x| x / 1000)
if let Some(snapshot) = sync::sync_headers(
Arc::clone(&first_peer),
- Arc::clone(&inner.headers),
+ Arc::clone(&self.headers),
|new_height| {
let local_headers_cost =
new_height.checked_sub(initial_height).unwrap_or(0) as f32 * SYNC_HEADERS_COST;
)
},
)? {
- if snapshot.work()? > inner.headers.work()? {
+ if snapshot.work()? > self.headers.work()? {
info!("Applying snapshot with work: {}", snapshot.work()?);
- inner.headers.apply_snapshot(snapshot)?;
+ self.headers.apply_snapshot(snapshot)?;
}
}
- let synced_height = inner.headers.get_height()?;
+ let synced_height = self.headers.get_height()?;
let buried_height = synced_height
.checked_sub(sync::BURIED_CONFIRMATIONS)
.unwrap_or(0);
let synced_bundles = Arc::new(AtomicUsize::new(0));
let progress_update = Arc::new(Mutex::new(progress_update));
- let mut threads = Vec::with_capacity(inner.peers.len());
- for peer in &inner.peers {
+ let mut threads = Vec::with_capacity(self.peers.len());
+ for peer in &self.peers {
let cf_sync = Arc::clone(&cf_sync);
let peer = Arc::clone(&peer);
- let headers = Arc::clone(&inner.headers);
+ let headers = Arc::clone(&self.headers);
let all_scripts = Arc::clone(&all_scripts);
let last_synced_block = Arc::clone(&last_synced_block);
let progress_update = Arc::clone(&progress_update);
let mut internal_max_deriv = None;
let mut external_max_deriv = None;
- for (height, block) in inner.headers.iter_full_blocks()? {
+ for (height, block) in self.headers.iter_full_blocks()? {
for tx in &block.txdata {
- inner.process_tx(
+ self.process_tx(
database,
tx,
Some(height as u32),
}
}
for tx in first_peer.get_mempool().iter_txs().iter() {
- inner.process_tx(
+ self.process_tx(
database,
tx,
None,
}
info!("Dropping blocks until {}", buried_height);
- inner.headers.delete_blocks_until(buried_height)?;
+ self.headers.delete_blocks_until(buried_height)?;
progress_update
.lock()
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
- let inner = self.0.as_ref().ok_or(Error::OfflineClient)?;
-
- Ok(inner.peers[0]
+ Ok(self.peers[0]
.get_mempool()
.get_tx(&Inventory::Transaction(*txid)))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
- let inner = self.0.as_ref().ok_or(Error::OfflineClient)?;
- inner.peers[0].broadcast_tx(tx.clone())?;
+ self.peers[0].broadcast_tx(tx.clone())?;
Ok(())
}
fn get_height(&self) -> Result<u32, Error> {
- let inner = self.0.as_ref().ok_or(Error::OfflineClient)?;
-
- Ok(inner.headers.get_height()? as u32)
+ Ok(self.headers.get_height()? as u32)
}
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
/// Add a transaction to the mempool
///
/// Note that this doesn't propagate the transaction to other
- /// peers. To do that, [`broadcast`](crate::blockchain::OnlineBlockchain::broadcast) should be used.
+ /// peers. To do that, [`broadcast`](crate::blockchain::Blockchain::broadcast) should be used.
pub fn add_tx(&self, tx: Transaction) {
self.txs.write().unwrap().insert(tx.txid(), tx);
}
//! Electrum
//!
-//! This module defines an [`OnlineBlockchain`] struct that wraps an [`electrum_client::Client`]
+//! This module defines a [`Blockchain`] struct that wraps an [`electrum_client::Client`]
//! and implements the logic required to populate the wallet's [database](crate::database::Database) by
//! querying the inner client.
//!
///
/// ## Example
/// See the [`blockchain::electrum`](crate::blockchain::electrum) module for a usage example.
-pub struct ElectrumBlockchain(Option<Client>);
+pub struct ElectrumBlockchain(Client);
#[cfg(test)]
#[cfg(feature = "test-electrum")]
impl std::convert::From<Client> for ElectrumBlockchain {
fn from(client: Client) -> Self {
- ElectrumBlockchain(Some(client))
+ ElectrumBlockchain(client)
}
}
impl Blockchain for ElectrumBlockchain {
- fn offline() -> Self {
- ElectrumBlockchain(None)
- }
-
- fn is_online(&self) -> bool {
- self.0.is_some()
- }
-}
-
-impl OnlineBlockchain for ElectrumBlockchain {
fn get_capabilities(&self) -> HashSet<Capability> {
vec![
Capability::FullHistory,
progress_update: P,
) -> Result<(), Error> {
self.0
- .as_ref()
- .ok_or(Error::OfflineClient)?
.electrum_like_setup(stop_gap, database, progress_update)
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
- Ok(self
- .0
- .as_ref()
- .ok_or(Error::OfflineClient)?
- .transaction_get(txid)
- .map(Option::Some)?)
+ Ok(self.0.transaction_get(txid).map(Option::Some)?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
- Ok(self
- .0
- .as_ref()
- .ok_or(Error::OfflineClient)?
- .transaction_broadcast(tx)
- .map(|_| ())?)
+ Ok(self.0.transaction_broadcast(tx).map(|_| ())?)
}
fn get_height(&self) -> Result<u32, Error> {
Ok(self
.0
- .as_ref()
- .ok_or(Error::OfflineClient)?
.block_headers_subscribe()
.map(|data| data.height as u32)?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
Ok(FeeRate::from_btc_per_kvb(
- self.0
- .as_ref()
- .ok_or(Error::OfflineClient)?
- .estimate_fee(target)? as f32,
+ self.0.estimate_fee(target)? as f32
))
}
}
//! Esplora
//!
-//! This module defines an [`OnlineBlockchain`] struct that can query an Esplora backend
+//! This module defines a [`Blockchain`] struct that can query an Esplora backend
//! populate the wallet's [database](crate::database::Database) by
//!
//! ## Example
/// ## Example
/// See the [`blockchain::esplora`](crate::blockchain::esplora) module for a usage example.
#[derive(Debug)]
-pub struct EsploraBlockchain(Option<UrlClient>);
+pub struct EsploraBlockchain(UrlClient);
impl std::convert::From<UrlClient> for EsploraBlockchain {
fn from(url_client: UrlClient) -> Self {
- EsploraBlockchain(Some(url_client))
+ EsploraBlockchain(url_client)
}
}
impl EsploraBlockchain {
/// Create a new instance of the client from a base URL
pub fn new(base_url: &str) -> Self {
- EsploraBlockchain(Some(UrlClient {
+ EsploraBlockchain(UrlClient {
url: base_url.to_string(),
client: Client::new(),
- }))
- }
-}
-
-impl Blockchain for EsploraBlockchain {
- fn offline() -> Self {
- EsploraBlockchain(None)
- }
-
- fn is_online(&self) -> bool {
- self.0.is_some()
+ })
}
}
#[maybe_async]
-impl OnlineBlockchain for EsploraBlockchain {
+impl Blockchain for EsploraBlockchain {
fn get_capabilities(&self) -> HashSet<Capability> {
vec, [Esplora](crate::blockchain::esplora) and
-//! [Compact Filters/Neutrino](crate::blockchain::compact_filters), along with two generalized
-//! traits [`Blockchain`] and [`OnlineBlockchain`] that can be implemented to build customized
-//! backends.
-//!
-//! Types that only implement the [`Blockchain`] trait can be used as backends for [`Wallet`](crate::wallet::Wallet)s, but any
-//! action that requires interacting with the blockchain won't be available ([`Wallet::sync`](crate::wallet::Wallet::sync) and
-//! [`Wallet::broadcast`](crate::wallet::Wallet::broadcast)). This allows the creation of physically air-gapped wallets, that have no
-//! ability to contact the outside world. An example of an offline-only client is [`OfflineBlockchain`].
-//!
-//! Types that also implement [`OnlineBlockchain`] will make the two aforementioned actions
-//! available.
+//! [Compact Filters/Neutrino](crate::blockchain::compact_filters), along with a generalized trait
+//! [`Blockchain`] that can be implemented to build customized backends.
use std::collections::HashSet;
use std::ops::Deref;
#[cfg(feature = "compact_filters")]
pub use self::compact_filters::CompactFiltersBlockchain;
-/// Capabilities that can be supported by an [`OnlineBlockchain`] backend
+/// Capabilities that can be supported by a [`Blockchain`] backend
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Capability {
/// Can recover the full history of a wallet and not only the set of currently spendable UTXOs
AccurateFees,
}
-/// Base trait for a blockchain backend
+/// Marker trait for a blockchain backend
///
-/// This trait is always required, even for "air-gapped" backends that don't actually make any
-/// external call. Clients that have the ability to make external calls must also implement `OnlineBlockchain`.
-pub trait Blockchain {
- /// Return whether or not the client has the ability to fullfill requests
- ///
- /// This should always be `false` for offline-only types, and can be true for types that also
- /// implement [`OnlineBlockchain`], if they have the ability to fullfill requests.
- fn is_online(&self) -> bool;
+/// This is a marker trait for blockchain types. It is automatically implemented for types that
+/// implement [`Blockchain`], so as a user of the library you won't have to implement this
+/// manually.
+///
+/// Users of the library will probably never have to implement this trait manually, but they
+/// could still need to import it to define types and structs with generics;
+/// Implementing only the marker trait is pointless, since [`OfflineBlockchain`]
+/// already does that, and whenever [`Blockchain`] is implemented, the marker trait is also
+/// automatically implemented by the library.
+pub trait BlockchainMarker {}
- /// Create a new instance of the client that is offline-only
- ///
- /// For types that also implement [`OnlineBlockchain`], this means creating an instance that
- /// returns [`Error::OfflineClient`](crate::error::Error::OfflineClient) if any of the "online"
- /// methods are called.
- ///
- /// This is generally implemented by wrapping the client in an [`Option`] that has [`Option::None`] value
- /// when created with this method, and is [`Option::Some`] if properly instantiated.
- fn offline() -> Self;
-}
+/// The [`BlockchainMarker`] marker trait is automatically implemented for [`Blockchain`] types
+impl<T: Blockchain> BlockchainMarker for T {}
-/// Type that only implements [`Blockchain`] and is always offline
+/// Type that only implements [`Blockchain`] and is always "offline"
pub struct OfflineBlockchain;
-impl Blockchain for OfflineBlockchain {
- fn offline() -> Self {
- OfflineBlockchain
- }
-
- fn is_online(&self) -> bool {
- false
- }
-}
+impl BlockchainMarker for OfflineBlockchain {}
-/// Trait that defines the actions that must be supported by an online [`Blockchain`]
+/// Trait that defines the actions that must be supported by a blockchain backend
#[maybe_async]
-pub trait OnlineBlockchain: Blockchain {
+pub trait Blockchain: BlockchainMarker {
/// Return the set of [`Capability`] supported by this backend
fn get_capabilities(&self) -> HashSet<Capability>;
/// Setup the backend and populate the internal database for the first time
///
- /// This method is the equivalent of [`OnlineBlockchain::sync`], but it's guaranteed to only be
+ /// This method is the equivalent of [`Blockchain::sync`], but it's guaranteed to only be
/// called once, at the first [`Wallet::sync`](crate::wallet::Wallet::sync).
///
/// The rationale behind the distinction between `sync` and `setup` is that some custom backends
/// might need to perform specific actions only the first time they are synced.
///
/// For types that do not have that distinction, only this method can be implemented, since
- /// [`OnlineBlockchain::sync`] defaults to calling this internally if not overridden.
+ /// [`Blockchain::sync`] defaults to calling this internally if not overridden.
fn setup<D: BatchDatabase, P: 'static + Progress>(
&self,
stop_gap: Option<usize>,
) -> Result<(), Error>;
/// Populate the internal database with transactions and UTXOs
///
- /// If not overridden, it defaults to calling [`OnlineBlockchain::setup`] internally.
+ /// If not overridden, it defaults to calling [`Blockchain::setup`] internally.
///
/// This method should implement the logic required to iterate over the list of the wallet's
/// script_pubkeys using [`Database::iter_script_pubkeys`] and look for relevant transactions
/// Data sent with a progress update over a [`channel`]
pub type ProgressData = (f32, Option<String>);
-/// Trait for types that can receive and process progress updates during [`OnlineBlockchain::sync`] and
-/// [`OnlineBlockchain::setup`]
+/// Trait for types that can receive and process progress updates during [`Blockchain::sync`] and
+/// [`Blockchain::setup`]
pub trait Progress: Send {
/// Send a new progress update
///
}
}
-impl<T: Blockchain> Blockchain for Arc<T> {
- fn is_online(&self) -> bool {
- self.deref().is_online()
- }
-
- fn offline() -> Self {
- Arc::new(T::offline())
- }
-}
-
#[maybe_async]
-impl<T: OnlineBlockchain> OnlineBlockchain for Arc<T> {
+impl<T: Blockchain> Blockchain for Arc<T> {
fn get_capabilities(&self) -> HashSet<Capability> {
maybe_await!(self.deref().get_capabilities())
}
matches: ArgMatches<'_>,
) -> Result<serde_json::Value, Error>
where
- C: crate::blockchain::OnlineBlockchain,
+ C: crate::blockchain::Blockchain,
D: crate::database::BatchDatabase,
{
if let Some(_sub_matches) = matches.subcommand_matches("get_new_address") {
use miniscript::{Descriptor, ScriptContext, Terminal};
-use crate::blockchain::Blockchain;
+use crate::blockchain::BlockchainMarker;
use crate::database::BatchDatabase;
use crate::wallet::Wallet;
///
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
/// returned will be `0`.
- pub fn export_wallet<B: Blockchain, D: BatchDatabase>(
+ pub fn export_wallet<B: BlockchainMarker, D: BatchDatabase>(
wallet: &Wallet<B, D>,
label: &str,
include_blockheight: bool,
use tx_builder::TxBuilder;
use utils::{After, Older};
-use crate::blockchain::{Blockchain, OfflineBlockchain, OnlineBlockchain, Progress};
+use crate::blockchain::{Blockchain, BlockchainMarker, OfflineBlockchain, Progress};
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::descriptor::{
get_checksum, DescriptorMeta, DescriptorScripts, ExtendedDescriptor, ExtractPolicy, Policy,
/// [creating transactions](Wallet::create_tx), etc.
///
/// A wallet can be either "online" if the [`blockchain`](crate::blockchain) type provided
-/// implements [`OnlineBlockchain`], or "offline" if it doesn't. Offline wallets only expose
+/// implements [`Blockchain`], or "offline" [`OfflineBlockchain`] is used. Offline wallets only expose
/// methods that don't need any interaction with the blockchain to work.
-pub struct Wallet<B: Blockchain, D: BatchDatabase> {
+pub struct Wallet<B: BlockchainMarker, D: BatchDatabase> {
descriptor: ExtendedDescriptor,
change_descriptor: Option<ExtendedDescriptor>,
current_height: Option<u32>,
- client: B,
+ client: Option<B>,
database: RefCell<D>,
}
// offline actions, always available
impl<B, D> Wallet<B, D>
where
- B: Blockchain,
+ B: BlockchainMarker,
D: BatchDatabase,
{
/// Create a new "offline" wallet
current_height: None,
- client: B::offline(),
+ client: None,
database: RefCell::new(database),
})
}
impl<B, D> Wallet<B, D>
where
- B: OnlineBlockchain,
+ B: Blockchain,
D: BatchDatabase,
{
/// Create a new "online" wallet
let mut wallet = Self::new_offline(descriptor, change_descriptor, network, database)?;
wallet.current_height = Some(maybe_await!(client.get_height())? as u32);
- wallet.client = client;
+ wallet.client = Some(client);
Ok(wallet)
}
// TODO: what if i generate an address first and cache some addresses?
// TODO: we should sync if generating an address triggers a new batch to be stored
if run_setup {
- maybe_await!(self.client.setup(
+ maybe_await!(self.client.as_ref().ok_or(Error::OfflineClient)?.setup(
None,
self.database.borrow_mut().deref_mut(),
progress_update,
))
} else {
- maybe_await!(self.client.sync(
+ maybe_await!(self.client.as_ref().ok_or(Error::OfflineClient)?.sync(
None,
self.database.borrow_mut().deref_mut(),
progress_update,
}
/// Return a reference to the internal blockchain client
- pub fn client(&self) -> &B {
- &self.client
+ pub fn client(&self) -> Option<&B> {
+ self.client.as_ref()
}
/// Broadcast a transaction to the network
#[maybe_async]
pub fn broadcast(&self, tx: Transaction) -> Result<Txid, Error> {
- maybe_await!(self.client.broadcast(&tx))?;
+ maybe_await!(self
+ .client
+ .as_ref()
+ .ok_or(Error::OfflineClient)?
+ .broadcast(&tx))?;
Ok(tx.txid())
}
ReturnType::Type(_, ref t) => t.clone(),
ReturnType::Default => {
return (quote! {
- compile_error!("The tagged function must return a type that impl `OnlineBlockchain`")
+ compile_error!("The tagged function must return a type that impl `Blockchain`")
}).into();
}
};
use testutils::{TestClient, serial};
- use #root_ident::blockchain::{OnlineBlockchain, noop_progress};
+ use #root_ident::blockchain::{Blockchain, noop_progress};
use #root_ident::descriptor::ExtendedDescriptor;
use #root_ident::database::MemoryDatabase;
use #root_ident::types::ScriptType;