#![warn(missing_docs)]
use core::fmt;
use std::collections::BTreeMap;
use bdk_chain::{
keychain_txout::KeychainTxOutIndex,
local_chain::{self, CheckPoint, LocalChain},
spk_client::FullScanResult,
IndexedTxGraph,
};
use bdk_chain::{ConfirmationBlockTime, TxUpdate};
pub use bdk_chain::bitcoin::FeeRate;
pub use bdk_chain::local_chain::MissingGenesisError;
pub extern crate kyoto;
#[cfg(feature = "rusqlite")]
pub use kyoto::core::builder::NodeDefault;
#[cfg(feature = "events")]
pub use kyoto::{DisconnectedHeader, FailurePayload};
#[cfg(all(feature = "wallet", feature = "rusqlite"))]
pub use kyoto::ClientSender as EventSender;
use kyoto::{IndexedBlock, NodeMessage, RejectReason};
pub use kyoto::{NodeState, Receiver, SyncUpdate, TxBroadcast, TxBroadcastPolicy, Txid, Warning};
#[cfg(all(feature = "wallet", feature = "rusqlite"))]
pub mod builder;
#[cfg(feature = "callbacks")]
pub mod logger;
#[derive(Debug)]
pub struct EventReceiver<K> {
receiver: kyoto::Receiver<NodeMessage>,
chain: local_chain::LocalChain,
graph: IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<K>>,
min_broadcast_fee: FeeRate,
}
impl<K> EventReceiver<K>
where
K: fmt::Debug + Clone + Ord,
{
pub fn from_index(
cp: CheckPoint,
index: &KeychainTxOutIndex<K>,
receiver: Receiver<NodeMessage>,
) -> Result<Self, MissingGenesisError> {
Ok(Self {
receiver,
chain: LocalChain::from_tip(cp)?,
graph: IndexedTxGraph::new(index.clone()),
min_broadcast_fee: FeeRate::BROADCAST_MIN,
})
}
#[cfg(feature = "callbacks")]
pub async fn update(&mut self, logger: &dyn NodeEventHandler) -> Option<FullScanResult<K>> {
let mut chain_changeset = BTreeMap::new();
while let Ok(message) = self.receiver.recv().await {
self.log(&message, logger);
match message {
NodeMessage::Block(IndexedBlock { height, block }) => {
let hash = block.header.block_hash();
chain_changeset.insert(height, Some(hash));
let _ = self.graph.apply_block_relevant(&block, height);
}
NodeMessage::BlocksDisconnected(headers) => {
for header in headers {
let height = header.height;
chain_changeset.insert(height, None);
}
}
NodeMessage::Synced(SyncUpdate {
tip,
recent_history,
}) => {
if chain_changeset.is_empty()
&& self.chain.tip().height() == tip.height
&& self.chain.tip().hash() == tip.hash
{
return None;
}
recent_history.into_iter().for_each(|(height, header)| {
chain_changeset.insert(height, Some(header.block_hash()));
});
break;
}
NodeMessage::FeeFilter(fee_filter) => {
if self.min_broadcast_fee < fee_filter {
self.min_broadcast_fee = fee_filter;
}
}
_ => (),
}
}
self.chain
.apply_changeset(&local_chain::ChangeSet::from(chain_changeset))
.expect("chain was initialized with genesis");
Some(self.get_scan_response())
}
#[cfg(feature = "callbacks")]
fn log(&self, message: &NodeMessage, logger: &dyn NodeEventHandler) {
match message {
NodeMessage::Dialog(d) => logger.dialog(d.clone()),
NodeMessage::Warning(w) => logger.warning(w.clone()),
NodeMessage::StateChange(s) => logger.state_changed(*s),
NodeMessage::Block(b) => {
let hash = b.block.header.block_hash();
logger.dialog(format!("Applying Block: {hash}"));
}
NodeMessage::Synced(SyncUpdate {
tip,
recent_history: _,
}) => {
logger.synced(tip.height);
}
NodeMessage::BlocksDisconnected(headers) => {
logger.blocks_disconnected(headers.iter().map(|dc| dc.height).collect());
}
NodeMessage::TxSent(t) => {
logger.tx_sent(*t);
}
NodeMessage::TxBroadcastFailure(r) => {
logger.tx_failed(r.txid, r.reason.map(|reason| reason.into_string()))
}
NodeMessage::ConnectionsMet => logger.connections_met(),
_ => (),
}
}
fn get_scan_response(&mut self) -> FullScanResult<K> {
let tx_update = TxUpdate::from(self.graph.graph().clone());
let graph = core::mem::take(&mut self.graph);
let last_active_indices = graph.index.last_used_indices();
self.graph = IndexedTxGraph::new(graph.index);
FullScanResult {
tx_update,
last_active_indices,
chain_update: Some(self.chain.tip()),
}
}
#[cfg(feature = "events")]
pub async fn next_event(&mut self, log_level: LogLevel) -> Option<Event<K>> {
while let Ok(message) = self.receiver.recv().await {
match message {
NodeMessage::Dialog(log) => {
if matches!(log_level, LogLevel::Info) {
return Some(Event::Log(log));
}
}
NodeMessage::Warning(warning) => return Some(Event::Warning(warning)),
NodeMessage::StateChange(node_state) => {
return Some(Event::StateChange(node_state))
}
NodeMessage::ConnectionsMet => return Some(Event::PeersFound),
NodeMessage::Block(IndexedBlock { height, block }) => {
let mut chain_changeset = BTreeMap::new();
chain_changeset.insert(height, Some(block.block_hash()));
self.chain
.apply_changeset(&local_chain::ChangeSet::from(chain_changeset))
.expect("chain initialized with genesis");
let _ = self.graph.apply_block_relevant(&block, height);
if matches!(log_level, LogLevel::Info) {
return Some(Event::Log(format!(
"Applied block {} to keychain",
block.block_hash()
)));
}
}
NodeMessage::Synced(SyncUpdate {
tip: _,
recent_history,
}) => {
let mut chain_changeset = BTreeMap::new();
recent_history.into_iter().for_each(|(height, header)| {
chain_changeset.insert(height, Some(header.block_hash()));
});
self.chain
.apply_changeset(&local_chain::ChangeSet::from(chain_changeset))
.expect("chain was initialized with genesis");
let result = self.get_scan_response();
return Some(Event::ScanResponse(result));
}
NodeMessage::BlocksDisconnected(headers) => {
let mut chain_changeset = BTreeMap::new();
for dc in &headers {
let height = dc.height;
chain_changeset.insert(height, None);
}
self.chain
.apply_changeset(&local_chain::ChangeSet::from(chain_changeset))
.expect("chain was initialized with genesis.");
return Some(Event::BlocksDisconnected(headers));
}
NodeMessage::TxSent(txid) => {
return Some(Event::TxSent(txid));
}
NodeMessage::TxBroadcastFailure(failure_payload) => {
return Some(Event::TxFailed(failure_payload));
}
NodeMessage::FeeFilter(fee_filter) => {
if self.min_broadcast_fee < fee_filter {
self.min_broadcast_fee = fee_filter;
}
}
_ => continue,
}
}
None
}
pub fn broadcast_minimum(&self) -> FeeRate {
self.min_broadcast_fee
}
}
#[cfg(feature = "callbacks")]
pub trait NodeEventHandler: Send + Sync + fmt::Debug + 'static {
fn dialog(&self, dialog: String);
fn warning(&self, warning: Warning);
fn state_changed(&self, state: NodeState);
fn connections_met(&self);
fn tx_sent(&self, txid: Txid);
fn tx_failed(&self, txid: Txid, reject_reason: Option<String>);
fn blocks_disconnected(&self, blocks: Vec<u32>);
fn synced(&self, tip: u32);
}
#[cfg(feature = "events")]
pub enum Event<K: fmt::Debug + Clone + Ord> {
Log(String),
Warning(Warning),
PeersFound,
TxSent(Txid),
TxFailed(FailurePayload),
StateChange(NodeState),
ScanResponse(FullScanResult<K>),
BlocksDisconnected(Vec<DisconnectedHeader>),
}
#[cfg(feature = "events")]
pub enum LogLevel {
Info,
Warning,
}
trait StringExt {
fn into_string(self) -> String;
}
impl StringExt for RejectReason {
fn into_string(self) -> String {
let message = match self {
RejectReason::Malformed => "Message could not be decoded.",
RejectReason::Invalid => "Transaction was invalid for some reason.",
RejectReason::Obsolete => "Client version is no longer supported.",
RejectReason::Duplicate => "Duplicate version message received.",
RejectReason::NonStandard => "Transaction was nonstandard.",
RejectReason::Dust => "One or more outputs are below the dust threshold.",
RejectReason::Fee => "Transaction does not have enough fee to be mined.",
RejectReason::Checkpoint => "Inconsistent with compiled checkpoint.",
};
message.into()
}
}