#![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};
use kyoto::{IndexedBlock, NodeMessage, SyncUpdate, TxBroadcast};
#[cfg(all(feature = "wallet", feature = "rusqlite"))]
pub mod builder;
#[cfg(feature = "callbacks")]
pub mod logger;
pub use bdk_chain::local_chain::MissingGenesisError;
#[cfg(feature = "rusqlite")]
pub use kyoto::core::builder::NodeDefault;
pub use kyoto::{
ClientError, HeaderCheckpoint, Node, NodeBuilder, NodeState, Receiver, ScriptBuf, ServiceFlags,
Transaction, TrustedPeer, TxBroadcastPolicy, Txid, Warning, MAINNET_HEADER_CP,
SIGNET_HEADER_CP,
};
#[cfg(feature = "events")]
pub use kyoto::{DisconnectedHeader, FailurePayload};
#[derive(Debug)]
pub struct Client<K> {
client: kyoto::Client,
receiver: kyoto::Receiver<NodeMessage>,
chain: local_chain::LocalChain,
graph: IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<K>>,
}
impl<K> Client<K>
where
K: fmt::Debug + Clone + Ord,
{
pub fn from_index(
cp: CheckPoint,
index: &KeychainTxOutIndex<K>,
client: kyoto::Client,
) -> Result<Self, MissingGenesisError> {
let receiver = client.receiver();
Ok(Self {
client,
receiver,
chain: LocalChain::from_tip(cp)?,
graph: IndexedTxGraph::new(index.clone()),
})
}
#[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;
}
_ => (),
}
}
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),
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));
}
_ => continue,
}
}
None
}
pub async fn broadcast(
&self,
tx: Transaction,
policy: TxBroadcastPolicy,
) -> Result<(), ClientError> {
self.client
.broadcast_tx(TxBroadcast {
tx,
broadcast_policy: policy,
})
.await
}
pub async fn add_script(&self, script: impl Into<ScriptBuf>) -> Result<(), ClientError> {
self.client.add_script(script).await
}
pub async fn shutdown(&self) -> Result<(), ClientError> {
self.client.shutdown().await
}
pub fn transaction_broadcaster(&self) -> TransactionBroadcaster {
TransactionBroadcaster::new(self.client.sender())
}
pub fn channel_receiver(&self) -> Receiver<NodeMessage> {
self.client.receiver()
}
}
#[derive(Debug)]
pub struct TransactionBroadcaster {
sender: kyoto::ClientSender,
}
impl TransactionBroadcaster {
fn new(sender: kyoto::ClientSender) -> Self {
Self { sender }
}
pub async fn broadcast(
&mut self,
tx: &Transaction,
policy: TxBroadcastPolicy,
) -> Result<(), ClientError> {
self.sender
.broadcast_tx(TxBroadcast {
tx: tx.clone(),
broadcast_policy: policy,
})
.await
}
}
#[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);
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,
}