1use bdk_kyoto::bip157;
2use bdk_kyoto::bip157::lookup_host;
3use bdk_kyoto::bip157::tokio;
4use bdk_kyoto::bip157::AddrV2;
5use bdk_kyoto::bip157::Network;
6use bdk_kyoto::bip157::Node;
7use bdk_kyoto::bip157::ServiceFlags;
8use bdk_kyoto::builder::Builder as BDKCbfBuilder;
9use bdk_kyoto::builder::BuilderExt;
10use bdk_kyoto::HashCheckpoint;
11use bdk_kyoto::Receiver;
12use bdk_kyoto::RejectReason;
13use bdk_kyoto::Requester;
14use bdk_kyoto::TrustedPeer;
15use bdk_kyoto::UnboundedReceiver;
16use bdk_kyoto::UpdateSubscriber;
17use bdk_kyoto::Warning as Warn;
18
19use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
20use std::path::PathBuf;
21use std::sync::Arc;
22use std::time::Duration;
23
24use tokio::sync::Mutex;
25
26use crate::bitcoin::BlockHash;
27use crate::bitcoin::Transaction;
28use crate::bitcoin::Wtxid;
29use crate::error::CbfError;
30use crate::types::BlockId;
31use crate::types::Update;
32use crate::wallet::Wallet;
33use crate::FeeRate;
34
35const DEFAULT_CONNECTIONS: u8 = 2;
36const CWD_PATH: &str = ".";
37const TCP_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2);
38const MESSAGE_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
39
40#[derive(Debug, uniffi::Record)]
42pub struct CbfComponents {
43 pub client: Arc<CbfClient>,
45 pub node: Arc<CbfNode>,
47}
48
49#[derive(Debug, uniffi::Object)]
51pub struct CbfClient {
52 sender: Arc<Requester>,
53 info_rx: Mutex<Receiver<bdk_kyoto::Info>>,
54 warning_rx: Mutex<UnboundedReceiver<bdk_kyoto::Warning>>,
55 update_rx: Mutex<UpdateSubscriber<bdk_kyoto::wallets::Single>>,
56}
57
58#[derive(Debug, uniffi::Object)]
63pub struct CbfNode {
64 node: std::sync::Mutex<Option<Node>>,
65}
66
67#[uniffi::export]
68impl CbfNode {
69 pub fn run(self: Arc<Self>) {
72 let mut lock = self.node.lock().unwrap();
73 let Some(node) = lock.take() else {
74 return;
75 };
76 std::thread::spawn(|| {
77 tokio::runtime::Builder::new_multi_thread()
78 .enable_all()
79 .build()
80 .unwrap()
81 .block_on(async move {
82 let _ = node.run().await;
83 })
84 });
85 }
86}
87
88#[derive(Clone, uniffi::Object)]
102pub struct CbfBuilder {
103 connections: u8,
104 handshake_timeout: Duration,
105 response_timeout: Duration,
106 data_dir: Option<String>,
107 scan_type: ScanType,
108 socks5_proxy: Option<Socks5Proxy>,
109 peers: Vec<Peer>,
110 whitelist_only: bool,
111}
112
113#[allow(clippy::new_without_default)]
114#[uniffi::export]
115impl CbfBuilder {
116 #[uniffi::constructor]
118 pub fn new() -> Self {
119 CbfBuilder {
120 connections: DEFAULT_CONNECTIONS,
121 handshake_timeout: TCP_HANDSHAKE_TIMEOUT,
122 response_timeout: MESSAGE_RESPONSE_TIMEOUT,
123 data_dir: None,
124 scan_type: ScanType::default(),
125 socks5_proxy: None,
126 peers: Vec::new(),
127 whitelist_only: false,
128 }
129 }
130
131 pub fn connections(&self, connections: u8) -> Arc<Self> {
133 Arc::new(CbfBuilder {
134 connections,
135 ..self.clone()
136 })
137 }
138
139 pub fn data_dir(&self, data_dir: String) -> Arc<Self> {
142 Arc::new(CbfBuilder {
143 data_dir: Some(data_dir),
144 ..self.clone()
145 })
146 }
147
148 pub fn scan_type(&self, scan_type: ScanType) -> Arc<Self> {
150 Arc::new(CbfBuilder {
151 scan_type,
152 ..self.clone()
153 })
154 }
155
156 pub fn peers(&self, peers: Vec<Peer>) -> Arc<Self> {
158 Arc::new(CbfBuilder {
159 peers,
160 ..self.clone()
161 })
162 }
163
164 pub fn configure_timeout_millis(&self, handshake: u64, response: u64) -> Arc<Self> {
168 Arc::new(CbfBuilder {
169 handshake_timeout: Duration::from_millis(handshake),
170 response_timeout: Duration::from_millis(response),
171 ..self.clone()
172 })
173 }
174
175 pub fn socks5_proxy(&self, proxy: Socks5Proxy) -> Arc<Self> {
179 Arc::new(CbfBuilder {
180 socks5_proxy: Some(proxy),
181 ..self.clone()
182 })
183 }
184
185 pub fn only_configured_peers(&self) -> Arc<Self> {
188 Arc::new(CbfBuilder {
189 whitelist_only: true,
190 ..self.clone()
191 })
192 }
193
194 pub fn build(&self, wallet: &Wallet) -> CbfComponents {
196 let wallet = wallet.get_wallet();
197
198 let mut trusted_peers = Vec::new();
199 for peer in self.peers.iter() {
200 trusted_peers.push(peer.clone().into());
201 }
202
203 let scan_type = match self.scan_type.clone() {
204 ScanType::Sync => bdk_kyoto::ScanType::Sync,
205 ScanType::Recovery {
206 used_script_index,
207 checkpoint,
208 } => {
209 let network = wallet.network();
210 match checkpoint {
211 RecoveryPoint::GenesisBlock => bdk_kyoto::ScanType::Recovery {
212 used_script_index,
213 checkpoint: HashCheckpoint::from_genesis(network),
214 },
215 RecoveryPoint::SegwitActivation if matches!(network, Network::Bitcoin) => {
216 bdk_kyoto::ScanType::Recovery {
217 used_script_index,
218 checkpoint: HashCheckpoint::segwit_activation(),
219 }
220 }
221 RecoveryPoint::TaprootActivation if matches!(network, Network::Bitcoin) => {
222 bdk_kyoto::ScanType::Recovery {
223 used_script_index,
224 checkpoint: HashCheckpoint::taproot_activation(),
225 }
226 }
227 RecoveryPoint::SegwitActivation | RecoveryPoint::TaprootActivation => {
228 bdk_kyoto::ScanType::Recovery {
229 used_script_index,
230 checkpoint: HashCheckpoint::from_genesis(network),
231 }
232 }
233 RecoveryPoint::Other { birthday } => bdk_kyoto::ScanType::Recovery {
234 used_script_index,
235 checkpoint: HashCheckpoint::new(birthday.height, birthday.hash.0),
236 },
237 }
238 }
239 };
240
241 let path_buf = self
242 .data_dir
243 .clone()
244 .map(|path| PathBuf::from(&path))
245 .unwrap_or(PathBuf::from(CWD_PATH));
246
247 let mut builder = BDKCbfBuilder::new(wallet.network())
248 .required_peers(self.connections)
249 .data_dir(path_buf)
250 .handshake_timeout(self.handshake_timeout)
251 .response_timeout(self.response_timeout)
252 .add_peers(trusted_peers);
253
254 if let Some(proxy) = &self.socks5_proxy {
255 let port = proxy.port;
256 let addr = proxy.address.inner;
257 builder = builder.socks5_proxy(SocketAddr::new(addr, port));
258 }
259
260 if self.whitelist_only {
261 builder = builder.whitelist_only();
262 }
263
264 let (client, logging, update_subscriber) = builder
265 .build_with_wallet(&wallet, scan_type)
266 .expect("networks match by definition")
267 .subscribe();
268 let (client, node) = client.managed_start();
269 let requester = client.requester();
270
271 let node = CbfNode {
272 node: std::sync::Mutex::new(Some(node)),
273 };
274
275 let client = CbfClient {
276 sender: Arc::new(requester),
277 info_rx: Mutex::new(logging.info_subscriber),
278 warning_rx: Mutex::new(logging.warning_subscriber),
279 update_rx: Mutex::new(update_subscriber),
280 };
281
282 CbfComponents {
283 client: Arc::new(client),
284 node: Arc::new(node),
285 }
286 }
287}
288
289#[uniffi::export]
290impl CbfClient {
291 pub async fn next_info(&self) -> Result<Info, CbfError> {
293 let mut info_rx = self.info_rx.lock().await;
294 info_rx
295 .recv()
296 .await
297 .map(|e| e.into())
298 .ok_or(CbfError::NodeStopped)
299 }
300
301 pub async fn next_warning(&self) -> Result<Warning, CbfError> {
303 let mut warn_rx = self.warning_rx.lock().await;
304 warn_rx
305 .recv()
306 .await
307 .map(|warn| warn.into())
308 .ok_or(CbfError::NodeStopped)
309 }
310
311 pub async fn update(&self) -> Result<Update, CbfError> {
314 let update = self
315 .update_rx
316 .lock()
317 .await
318 .update()
319 .await
320 .map_err(|_| CbfError::NodeStopped)?;
321 Ok(Update(update))
322 }
323
324 pub async fn broadcast(&self, transaction: &Transaction) -> Result<Arc<Wtxid>, CbfError> {
326 let tx: bip157::Transaction = transaction.into();
327 self.sender
328 .submit_package(tx)
329 .await
330 .map_err(From::from)
331 .map(|wtxid| Arc::new(Wtxid(wtxid)))
332 }
333
334 pub async fn min_broadcast_feerate(&self) -> Result<Arc<FeeRate>, CbfError> {
336 self.sender
337 .broadcast_min_feerate()
338 .await
339 .map_err(|_| CbfError::NodeStopped)
340 .map(|fee| Arc::new(FeeRate(fee)))
341 }
342
343 pub async fn average_fee_rate(
346 &self,
347 blockhash: Arc<BlockHash>,
348 ) -> Result<Arc<FeeRate>, CbfError> {
349 let fee_rate = self
350 .sender
351 .average_fee_rate(blockhash.0)
352 .await
353 .map_err(|_| CbfError::NodeStopped)?;
354 Ok(Arc::new(fee_rate.into()))
355 }
356
357 pub fn connect(&self, peer: Peer) -> Result<(), CbfError> {
359 self.sender
360 .add_peer(peer)
361 .map_err(|_| CbfError::NodeStopped)
362 }
363
364 pub fn lookup_host(&self, hostname: String) -> Vec<Arc<IpAddress>> {
370 let node_handle = std::thread::spawn(move || {
371 tokio::runtime::Builder::new_current_thread()
372 .enable_all()
373 .build()
374 .unwrap()
375 .block_on(lookup_host(hostname))
376 });
377 let nodes = node_handle.join().unwrap_or_default();
378 nodes
379 .into_iter()
380 .map(|ip| Arc::new(IpAddress { inner: ip }))
381 .collect()
382 }
383
384 pub async fn peer_info(&self) -> Result<Vec<Arc<IpAddress>>, CbfError> {
386 let peers = self
387 .sender
388 .peer_info()
389 .await
390 .map_err(|_| CbfError::NodeStopped)?;
391 Ok(peers
392 .into_iter()
393 .filter_map(|(ip, _)| match ip {
394 AddrV2::Ipv4(ip) => Some(IpAddr::V4(ip)),
395 AddrV2::Ipv6(ip) => Some(IpAddr::V6(ip)),
396 _ => None,
397 })
398 .map(|ip| Arc::new(IpAddress { inner: ip }))
399 .collect())
400 }
401
402 pub fn is_running(&self) -> bool {
404 self.sender.is_running()
405 }
406
407 pub fn shutdown(&self) -> Result<(), CbfError> {
409 self.sender.shutdown().map_err(From::from)
410 }
411}
412
413#[derive(Debug, uniffi::Enum)]
415pub enum Info {
416 ConnectionsMet,
418 SuccessfulHandshake,
420 Progress {
422 chain_height: u32,
424 filters_downloaded_percent: f32,
426 },
427 BlockReceived(String),
429}
430
431impl From<bdk_kyoto::Info> for Info {
432 fn from(value: bdk_kyoto::Info) -> Info {
433 match value {
434 bdk_kyoto::Info::ConnectionsMet => Info::ConnectionsMet,
435 bdk_kyoto::Info::SuccessfulHandshake => Info::SuccessfulHandshake,
436 bdk_kyoto::Info::Progress(progress) => Info::Progress {
437 filters_downloaded_percent: progress.percentage_complete(),
438 chain_height: progress.chain_height(),
439 },
440 bdk_kyoto::Info::BlockReceived(block) => Info::BlockReceived(block.to_string()),
441 }
442 }
443}
444
445#[derive(Debug, uniffi::Enum)]
447pub enum Warning {
448 NeedConnections,
450 PeerTimedOut,
452 CouldNotConnect,
454 NoCompactFilters,
456 PotentialStaleTip,
458 UnsolicitedMessage,
460 TransactionRejected {
462 wtxid: String,
463 reason: Option<String>,
464 },
465 EvaluatingFork,
467 UnexpectedSyncError { warning: String },
469 RequestFailed,
471}
472
473impl From<Warn> for Warning {
474 fn from(value: Warn) -> Warning {
475 match value {
476 Warn::NeedConnections {
477 connected: _,
478 required: _,
479 } => Warning::NeedConnections,
480 Warn::PeerTimedOut => Warning::PeerTimedOut,
481 Warn::CouldNotConnect => Warning::CouldNotConnect,
482 Warn::NoCompactFilters => Warning::NoCompactFilters,
483 Warn::PotentialStaleTip => Warning::PotentialStaleTip,
484 Warn::UnsolicitedMessage => Warning::UnsolicitedMessage,
485 Warn::TransactionRejected { payload } => {
486 let reason = payload.reason.map(|r| r.into_string());
487 Warning::TransactionRejected {
488 wtxid: payload.wtxid.to_string(),
489 reason,
490 }
491 }
492 Warn::EvaluatingFork => Warning::EvaluatingFork,
493 Warn::UnexpectedSyncError { warning } => Warning::UnexpectedSyncError { warning },
494 Warn::ChannelDropped => Warning::RequestFailed,
495 }
496 }
497}
498
499#[derive(Debug, Clone, Default, uniffi::Enum)]
502pub enum ScanType {
503 #[default]
505 Sync,
506 Recovery {
508 used_script_index: u32,
511 checkpoint: RecoveryPoint,
513 },
514}
515
516#[derive(Debug, Clone, Default, uniffi::Enum)]
517pub enum RecoveryPoint {
518 GenesisBlock,
519 #[default]
520 SegwitActivation,
521 TaprootActivation,
522 Other {
523 birthday: BlockId,
524 },
525}
526
527#[derive(Clone, uniffi::Record)]
529pub struct Peer {
530 pub address: Arc<IpAddress>,
532 pub port: Option<u16>,
535 pub v2_transport: bool,
537}
538
539#[derive(Debug, uniffi::Object)]
541#[uniffi::export(Display)]
542pub struct IpAddress {
543 inner: IpAddr,
544}
545
546impl core::fmt::Display for IpAddress {
547 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
548 write!(f, "{}", self.inner)
549 }
550}
551
552#[uniffi::export]
553impl IpAddress {
554 #[uniffi::constructor]
556 pub fn from_ipv4(q1: u8, q2: u8, q3: u8, q4: u8) -> Self {
557 Self {
558 inner: IpAddr::V4(Ipv4Addr::new(q1, q2, q3, q4)),
559 }
560 }
561
562 #[allow(clippy::too_many_arguments)]
564 #[uniffi::constructor]
565 pub fn from_ipv6(a: u16, b: u16, c: u16, d: u16, e: u16, f: u16, g: u16, h: u16) -> Self {
566 Self {
567 inner: IpAddr::V6(Ipv6Addr::new(a, b, c, d, e, f, g, h)),
568 }
569 }
570}
571
572#[derive(Debug, Clone, uniffi::Record)]
575pub struct Socks5Proxy {
576 pub address: Arc<IpAddress>,
578 pub port: u16,
580}
581
582impl From<Peer> for TrustedPeer {
583 fn from(peer: Peer) -> Self {
584 let services = if peer.v2_transport {
585 let mut services = ServiceFlags::P2P_V2;
586 services.add(ServiceFlags::NETWORK);
587 services.add(ServiceFlags::COMPACT_FILTERS);
588 services
589 } else {
590 let mut services = ServiceFlags::COMPACT_FILTERS;
591 services.add(ServiceFlags::NETWORK);
592 services
593 };
594 let addr_v2 = match peer.address.inner {
595 IpAddr::V4(ipv4_addr) => AddrV2::Ipv4(ipv4_addr),
596 IpAddr::V6(ipv6_addr) => AddrV2::Ipv6(ipv6_addr),
597 };
598 TrustedPeer::new(addr_v2, peer.port, services)
599 }
600}
601
602trait DisplayExt {
603 fn into_string(self) -> String;
604}
605
606impl DisplayExt for RejectReason {
607 fn into_string(self) -> String {
608 let message = match self {
609 RejectReason::Malformed => "Message could not be decoded.",
610 RejectReason::Invalid => "Transaction was invalid for some reason.",
611 RejectReason::Obsolete => "Client version is no longer supported.",
612 RejectReason::Duplicate => "Duplicate version message received.",
613 RejectReason::NonStandard => "Transaction was nonstandard.",
614 RejectReason::Dust => "One or more outputs are below the dust threshold.",
615 RejectReason::Fee => "Transaction does not have enough fee to be mined.",
616 RejectReason::Checkpoint => "Inconsistent with compiled checkpoint.",
617 };
618 message.into()
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::CbfNode;
625 use std::sync::{Arc, Mutex};
626
627 #[test]
628 fn running_a_consumed_node_is_a_no_op() {
629 let node = Arc::new(CbfNode {
630 node: Mutex::new(None),
631 });
632
633 node.run();
634 }
635}