use std::{path::PathBuf, time::Duration};
use bdk_chain::local_chain::MissingGenesisError;
use bdk_wallet::Wallet;
use kyoto::NodeBuilder;
pub use kyoto::{
db::error::SqlInitializationError, AddrV2, HeaderCheckpoint, ScriptBuf, ServiceFlags,
TrustedPeer,
};
use crate::{EventReceiver, LightClient, WalletExt};
const RECOMMENDED_PEERS: u8 = 2;
#[derive(Debug)]
pub struct LightClientBuilder {
peers: Option<Vec<TrustedPeer>>,
connections: Option<u8>,
birthday_height: Option<u32>,
data_dir: Option<PathBuf>,
timeout: Option<Duration>,
}
impl LightClientBuilder {
pub fn new() -> Self {
Self {
peers: None,
connections: None,
birthday_height: None,
data_dir: None,
timeout: None,
}
}
pub fn peers(mut self, peers: Vec<TrustedPeer>) -> Self {
self.peers = Some(peers);
self
}
pub fn connections(mut self, num_connections: u8) -> Self {
self.connections = Some(num_connections);
self
}
pub fn data_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.data_dir = Some(dir.into());
self
}
pub fn scan_after(mut self, height: u32) -> Self {
self.birthday_height = Some(height);
self
}
pub fn timeout_duration(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn build(self, wallet: &Wallet) -> Result<LightClient, BuilderError> {
let network = wallet.network();
let mut node_builder = NodeBuilder::new(network);
if let Some(whitelist) = self.peers {
node_builder = node_builder.add_peers(whitelist);
}
match self.birthday_height {
Some(birthday) => {
if birthday < wallet.local_chain().tip().height() {
let block_id = wallet.local_chain().tip();
let header_cp = HeaderCheckpoint::new(block_id.height(), block_id.hash());
node_builder = node_builder.anchor_checkpoint(header_cp)
} else {
let cp = HeaderCheckpoint::closest_checkpoint_below_height(birthday, network);
node_builder = node_builder.anchor_checkpoint(cp)
}
}
None => {
let block_id = wallet.local_chain().tip();
if block_id.height() > 0 {
let header_cp = HeaderCheckpoint::new(block_id.height(), block_id.hash());
node_builder = node_builder.anchor_checkpoint(header_cp)
}
}
}
if let Some(dir) = self.data_dir {
node_builder = node_builder.add_data_dir(dir);
}
if let Some(duration) = self.timeout {
node_builder = node_builder.set_response_timeout(duration)
}
node_builder =
node_builder.num_required_peers(self.connections.unwrap_or(RECOMMENDED_PEERS));
let (node, kyoto_client) = node_builder
.add_scripts(wallet.peek_revealed_plus_lookahead().collect())
.build_node()?;
let (sender, receiver) = kyoto_client.split();
let event_receiver =
EventReceiver::from_index(wallet.local_chain().tip(), wallet.spk_index(), receiver)?;
Ok(LightClient {
sender,
receiver: event_receiver,
node,
})
}
}
impl Default for LightClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub enum BuilderError {
Chain(MissingGenesisError),
Database(SqlInitializationError),
}
impl std::fmt::Display for BuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuilderError::Chain(e) => write!(f, "genesis block not found: {e}"),
BuilderError::Database(e) => write!(f, "fatal database error: {e}"),
}
}
}
impl std::error::Error for BuilderError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
BuilderError::Chain(e) => Some(e),
BuilderError::Database(e) => Some(e),
}
}
}
impl From<MissingGenesisError> for BuilderError {
fn from(value: MissingGenesisError) -> Self {
BuilderError::Chain(value)
}
}
impl From<SqlInitializationError> for BuilderError {
fn from(value: SqlInitializationError) -> Self {
BuilderError::Database(value)
}
}