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>) {
71 let mut lock = self.node.lock().unwrap();
72 let node = lock.take().expect("cannot call run more than once");
73 std::thread::spawn(|| {
74 tokio::runtime::Builder::new_multi_thread()
75 .enable_all()
76 .build()
77 .unwrap()
78 .block_on(async move {
79 let _ = node.run().await;
80 })
81 });
82 }
83}
84
85#[derive(Clone, uniffi::Object)]
99pub struct CbfBuilder {
100 connections: u8,
101 handshake_timeout: Duration,
102 response_timeout: Duration,
103 data_dir: Option<String>,
104 scan_type: ScanType,
105 socks5_proxy: Option<Socks5Proxy>,
106 peers: Vec<Peer>,
107}
108
109#[allow(clippy::new_without_default)]
110#[uniffi::export]
111impl CbfBuilder {
112 #[uniffi::constructor]
114 pub fn new() -> Self {
115 CbfBuilder {
116 connections: DEFAULT_CONNECTIONS,
117 handshake_timeout: TCP_HANDSHAKE_TIMEOUT,
118 response_timeout: MESSAGE_RESPONSE_TIMEOUT,
119 data_dir: None,
120 scan_type: ScanType::default(),
121 socks5_proxy: None,
122 peers: Vec::new(),
123 }
124 }
125
126 pub fn connections(&self, connections: u8) -> Arc<Self> {
128 Arc::new(CbfBuilder {
129 connections,
130 ..self.clone()
131 })
132 }
133
134 pub fn data_dir(&self, data_dir: String) -> Arc<Self> {
137 Arc::new(CbfBuilder {
138 data_dir: Some(data_dir),
139 ..self.clone()
140 })
141 }
142
143 pub fn scan_type(&self, scan_type: ScanType) -> Arc<Self> {
145 Arc::new(CbfBuilder {
146 scan_type,
147 ..self.clone()
148 })
149 }
150
151 pub fn peers(&self, peers: Vec<Peer>) -> Arc<Self> {
153 Arc::new(CbfBuilder {
154 peers,
155 ..self.clone()
156 })
157 }
158
159 pub fn configure_timeout_millis(&self, handshake: u64, response: u64) -> Arc<Self> {
163 Arc::new(CbfBuilder {
164 handshake_timeout: Duration::from_millis(handshake),
165 response_timeout: Duration::from_millis(response),
166 ..self.clone()
167 })
168 }
169
170 pub fn socks5_proxy(&self, proxy: Socks5Proxy) -> Arc<Self> {
174 Arc::new(CbfBuilder {
175 socks5_proxy: Some(proxy),
176 ..self.clone()
177 })
178 }
179
180 pub fn build(&self, wallet: &Wallet) -> CbfComponents {
182 let wallet = wallet.get_wallet();
183
184 let mut trusted_peers = Vec::new();
185 for peer in self.peers.iter() {
186 trusted_peers.push(peer.clone().into());
187 }
188
189 let scan_type = match self.scan_type.clone() {
190 ScanType::Sync => bdk_kyoto::ScanType::Sync,
191 ScanType::Recovery {
192 used_script_index,
193 checkpoint,
194 } => {
195 let network = wallet.network();
196 if !matches!(network, Network::Bitcoin) {
198 bdk_kyoto::ScanType::Recovery {
199 used_script_index,
200 checkpoint: HashCheckpoint::from_genesis(network),
201 }
202 } else {
203 match checkpoint {
204 RecoveryPoint::GenesisBlock => bdk_kyoto::ScanType::Recovery {
205 used_script_index,
206 checkpoint: HashCheckpoint::from_genesis(wallet.network()),
207 },
208 RecoveryPoint::SegwitActivation => bdk_kyoto::ScanType::Recovery {
209 used_script_index,
210 checkpoint: HashCheckpoint::segwit_activation(),
211 },
212 RecoveryPoint::TaprootActivation => bdk_kyoto::ScanType::Recovery {
213 used_script_index,
214 checkpoint: HashCheckpoint::taproot_activation(),
215 },
216 RecoveryPoint::Other { birthday } => bdk_kyoto::ScanType::Recovery {
217 used_script_index,
218 checkpoint: HashCheckpoint::new(birthday.height, birthday.hash.0),
219 },
220 }
221 }
222 }
223 };
224
225 let path_buf = self
226 .data_dir
227 .clone()
228 .map(|path| PathBuf::from(&path))
229 .unwrap_or(PathBuf::from(CWD_PATH));
230
231 let mut builder = BDKCbfBuilder::new(wallet.network())
232 .required_peers(self.connections)
233 .data_dir(path_buf)
234 .handshake_timeout(self.handshake_timeout)
235 .response_timeout(self.response_timeout)
236 .add_peers(trusted_peers);
237
238 if let Some(proxy) = &self.socks5_proxy {
239 let port = proxy.port;
240 let addr = proxy.address.inner;
241 builder = builder.socks5_proxy(SocketAddr::new(addr, port));
242 }
243
244 let (client, logging, update_subscriber) = builder
245 .build_with_wallet(&wallet, scan_type)
246 .expect("networks match by definition")
247 .subscribe();
248 let (client, node) = client.managed_start();
249 let requester = client.requester();
250
251 let node = CbfNode {
252 node: std::sync::Mutex::new(Some(node)),
253 };
254
255 let client = CbfClient {
256 sender: Arc::new(requester),
257 info_rx: Mutex::new(logging.info_subscriber),
258 warning_rx: Mutex::new(logging.warning_subscriber),
259 update_rx: Mutex::new(update_subscriber),
260 };
261
262 CbfComponents {
263 client: Arc::new(client),
264 node: Arc::new(node),
265 }
266 }
267}
268
269#[uniffi::export]
270impl CbfClient {
271 pub async fn next_info(&self) -> Result<Info, CbfError> {
273 let mut info_rx = self.info_rx.lock().await;
274 info_rx
275 .recv()
276 .await
277 .map(|e| e.into())
278 .ok_or(CbfError::NodeStopped)
279 }
280
281 pub async fn next_warning(&self) -> Result<Warning, CbfError> {
283 let mut warn_rx = self.warning_rx.lock().await;
284 warn_rx
285 .recv()
286 .await
287 .map(|warn| warn.into())
288 .ok_or(CbfError::NodeStopped)
289 }
290
291 pub async fn update(&self) -> Result<Update, CbfError> {
294 let update = self
295 .update_rx
296 .lock()
297 .await
298 .update()
299 .await
300 .map_err(|_| CbfError::NodeStopped)?;
301 Ok(Update(update))
302 }
303
304 pub async fn broadcast(&self, transaction: &Transaction) -> Result<Arc<Wtxid>, CbfError> {
306 let tx: bip157::Transaction = transaction.into();
307 self.sender
308 .submit_package(tx)
309 .await
310 .map_err(From::from)
311 .map(|wtxid| Arc::new(Wtxid(wtxid)))
312 }
313
314 pub async fn min_broadcast_feerate(&self) -> Result<Arc<FeeRate>, CbfError> {
316 self.sender
317 .broadcast_min_feerate()
318 .await
319 .map_err(|_| CbfError::NodeStopped)
320 .map(|fee| Arc::new(FeeRate(fee)))
321 }
322
323 pub async fn average_fee_rate(
326 &self,
327 blockhash: Arc<BlockHash>,
328 ) -> Result<Arc<FeeRate>, CbfError> {
329 let fee_rate = self
330 .sender
331 .average_fee_rate(blockhash.0)
332 .await
333 .map_err(|_| CbfError::NodeStopped)?;
334 Ok(Arc::new(fee_rate.into()))
335 }
336
337 pub fn connect(&self, peer: Peer) -> Result<(), CbfError> {
339 self.sender
340 .add_peer(peer)
341 .map_err(|_| CbfError::NodeStopped)
342 }
343
344 pub fn lookup_host(&self, hostname: String) -> Vec<Arc<IpAddress>> {
350 let node_handle = std::thread::spawn(move || {
351 tokio::runtime::Builder::new_current_thread()
352 .enable_all()
353 .build()
354 .unwrap()
355 .block_on(lookup_host(hostname))
356 });
357 let nodes = node_handle.join().unwrap_or_default();
358 nodes
359 .into_iter()
360 .map(|ip| Arc::new(IpAddress { inner: ip }))
361 .collect()
362 }
363
364 pub async fn peer_info(&self) -> Result<Vec<Arc<IpAddress>>, CbfError> {
366 let peers = self
367 .sender
368 .peer_info()
369 .await
370 .map_err(|_| CbfError::NodeStopped)?;
371 Ok(peers
372 .into_iter()
373 .filter_map(|(ip, _)| match ip {
374 AddrV2::Ipv4(ip) => Some(IpAddr::V4(ip)),
375 AddrV2::Ipv6(ip) => Some(IpAddr::V6(ip)),
376 _ => None,
377 })
378 .map(|ip| Arc::new(IpAddress { inner: ip }))
379 .collect())
380 }
381
382 pub fn is_running(&self) -> bool {
384 self.sender.is_running()
385 }
386
387 pub fn shutdown(&self) -> Result<(), CbfError> {
389 self.sender.shutdown().map_err(From::from)
390 }
391}
392
393#[derive(Debug, uniffi::Enum)]
395pub enum Info {
396 ConnectionsMet,
398 SuccessfulHandshake,
400 Progress {
402 chain_height: u32,
404 filters_downloaded_percent: f32,
406 },
407 BlockReceived(String),
409}
410
411impl From<bdk_kyoto::Info> for Info {
412 fn from(value: bdk_kyoto::Info) -> Info {
413 match value {
414 bdk_kyoto::Info::ConnectionsMet => Info::ConnectionsMet,
415 bdk_kyoto::Info::SuccessfulHandshake => Info::SuccessfulHandshake,
416 bdk_kyoto::Info::Progress(progress) => Info::Progress {
417 filters_downloaded_percent: progress.percentage_complete(),
418 chain_height: progress.chain_height(),
419 },
420 bdk_kyoto::Info::BlockReceived(block) => Info::BlockReceived(block.to_string()),
421 }
422 }
423}
424
425#[derive(Debug, uniffi::Enum)]
427pub enum Warning {
428 NeedConnections,
430 PeerTimedOut,
432 CouldNotConnect,
434 NoCompactFilters,
436 PotentialStaleTip,
438 UnsolicitedMessage,
440 TransactionRejected {
442 wtxid: String,
443 reason: Option<String>,
444 },
445 EvaluatingFork,
447 UnexpectedSyncError { warning: String },
449 RequestFailed,
451}
452
453impl From<Warn> for Warning {
454 fn from(value: Warn) -> Warning {
455 match value {
456 Warn::NeedConnections {
457 connected: _,
458 required: _,
459 } => Warning::NeedConnections,
460 Warn::PeerTimedOut => Warning::PeerTimedOut,
461 Warn::CouldNotConnect => Warning::CouldNotConnect,
462 Warn::NoCompactFilters => Warning::NoCompactFilters,
463 Warn::PotentialStaleTip => Warning::PotentialStaleTip,
464 Warn::UnsolicitedMessage => Warning::UnsolicitedMessage,
465 Warn::TransactionRejected { payload } => {
466 let reason = payload.reason.map(|r| r.into_string());
467 Warning::TransactionRejected {
468 wtxid: payload.wtxid.to_string(),
469 reason,
470 }
471 }
472 Warn::EvaluatingFork => Warning::EvaluatingFork,
473 Warn::UnexpectedSyncError { warning } => Warning::UnexpectedSyncError { warning },
474 Warn::ChannelDropped => Warning::RequestFailed,
475 }
476 }
477}
478
479#[derive(Debug, Clone, Default, uniffi::Enum)]
482pub enum ScanType {
483 #[default]
485 Sync,
486 Recovery {
488 used_script_index: u32,
491 checkpoint: RecoveryPoint,
493 },
494}
495
496#[derive(Debug, Clone, Default, uniffi::Enum)]
497pub enum RecoveryPoint {
498 GenesisBlock,
499 #[default]
500 SegwitActivation,
501 TaprootActivation,
502 Other {
503 birthday: BlockId,
504 },
505}
506
507#[derive(Clone, uniffi::Record)]
509pub struct Peer {
510 pub address: Arc<IpAddress>,
512 pub port: Option<u16>,
515 pub v2_transport: bool,
517}
518
519#[derive(Debug, uniffi::Object)]
521#[uniffi::export(Display)]
522pub struct IpAddress {
523 inner: IpAddr,
524}
525
526impl core::fmt::Display for IpAddress {
527 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
528 write!(f, "{}", self.inner)
529 }
530}
531
532#[uniffi::export]
533impl IpAddress {
534 #[uniffi::constructor]
536 pub fn from_ipv4(q1: u8, q2: u8, q3: u8, q4: u8) -> Self {
537 Self {
538 inner: IpAddr::V4(Ipv4Addr::new(q1, q2, q3, q4)),
539 }
540 }
541
542 #[allow(clippy::too_many_arguments)]
544 #[uniffi::constructor]
545 pub fn from_ipv6(a: u16, b: u16, c: u16, d: u16, e: u16, f: u16, g: u16, h: u16) -> Self {
546 Self {
547 inner: IpAddr::V6(Ipv6Addr::new(a, b, c, d, e, f, g, h)),
548 }
549 }
550}
551
552#[derive(Debug, Clone, uniffi::Record)]
555pub struct Socks5Proxy {
556 pub address: Arc<IpAddress>,
558 pub port: u16,
560}
561
562impl From<Peer> for TrustedPeer {
563 fn from(peer: Peer) -> Self {
564 let services = if peer.v2_transport {
565 let mut services = ServiceFlags::P2P_V2;
566 services.add(ServiceFlags::NETWORK);
567 services.add(ServiceFlags::COMPACT_FILTERS);
568 services
569 } else {
570 let mut services = ServiceFlags::COMPACT_FILTERS;
571 services.add(ServiceFlags::NETWORK);
572 services
573 };
574 let addr_v2 = match peer.address.inner {
575 IpAddr::V4(ipv4_addr) => AddrV2::Ipv4(ipv4_addr),
576 IpAddr::V6(ipv6_addr) => AddrV2::Ipv6(ipv6_addr),
577 };
578 TrustedPeer::new(addr_v2, peer.port, services)
579 }
580}
581
582trait DisplayExt {
583 fn into_string(self) -> String;
584}
585
586impl DisplayExt for RejectReason {
587 fn into_string(self) -> String {
588 let message = match self {
589 RejectReason::Malformed => "Message could not be decoded.",
590 RejectReason::Invalid => "Transaction was invalid for some reason.",
591 RejectReason::Obsolete => "Client version is no longer supported.",
592 RejectReason::Duplicate => "Duplicate version message received.",
593 RejectReason::NonStandard => "Transaction was nonstandard.",
594 RejectReason::Dust => "One or more outputs are below the dust threshold.",
595 RejectReason::Fee => "Transaction does not have enough fee to be mined.",
596 RejectReason::Checkpoint => "Inconsistent with compiled checkpoint.",
597 };
598 message.into()
599 }
600}