use std::{collections::HashSet, path::PathBuf, time::Duration};
use bdk_chain::local_chain::MissingGenesisError;
use bdk_wallet::{KeychainKind, Wallet};
use kyoto::{
chain::checkpoints::HeaderCheckpoint, core::builder::NodeDefault,
db::error::SqlInitializationError, NodeBuilder, ScriptBuf, TrustedPeer,
};
use crate::Client;
const RECOMMENDED_PEERS: u8 = 2;
#[derive(Debug)]
pub struct LightClientBuilder<'a> {
wallet: &'a Wallet,
peers: Option<Vec<TrustedPeer>>,
connections: Option<u8>,
birthday_height: Option<u32>,
data_dir: Option<PathBuf>,
timeout: Option<Duration>,
}
impl<'a> LightClientBuilder<'a> {
pub fn new(wallet: &'a Wallet) -> Self {
Self {
wallet,
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) -> Result<(NodeDefault, Client<KeychainKind>), BuilderError> {
let network = self.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 < self.wallet.local_chain().tip().height() {
let block_id = self.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 = self.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 mut spks: HashSet<ScriptBuf> = HashSet::new();
for keychain in [KeychainKind::External, KeychainKind::Internal] {
let last_revealed = self
.wallet
.spk_index()
.last_revealed_index(keychain)
.unwrap_or(0);
let lookahead_index = last_revealed + self.wallet.spk_index().lookahead();
for index in 0..=lookahead_index {
spks.insert(self.wallet.peek_address(keychain, index).script_pubkey());
}
}
let (node, kyoto_client) = node_builder.add_scripts(spks).build_node()?;
let client = Client::from_index(
self.wallet.local_chain().tip(),
self.wallet.spk_index(),
kyoto_client,
)?;
Ok((node, client))
}
}
#[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)
}
}