1pub mod tip_watcher;
2
3use std::sync::{Arc, Weak};
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::Duration;
6
7use ark::rounds::RoundEvent;
8use futures::{FutureExt, StreamExt};
9use log::{info, trace, warn};
10#[cfg(not(feature = "wasm-web"))]
11use tokio::task::JoinHandle;
12use tokio_util::sync::CancellationToken;
13
14use crate::{Wallet, WalletInner};
15use crate::utils::ReconnectBackoff;
16
17
18
19#[cfg(not(feature = "wasm-web"))]
21pub struct DaemonHandle {
22 shutdown: CancellationToken,
23 jh: JoinHandle<()>,
24}
25
26#[cfg(feature = "wasm-web")]
28pub struct DaemonHandle {
29 shutdown: CancellationToken,
30}
31
32impl DaemonHandle {
33 pub fn stop(&self) {
35 self.shutdown.cancel();
36 }
37
38 pub async fn stop_wait(self) -> anyhow::Result<()> {
40 self.stop();
41 #[cfg(not(feature = "wasm-web"))]
42 self.jh.await?;
43 Ok(())
44 }
45}
46
47pub(crate) fn start_daemon(
48 wallet: &Wallet,
49) -> DaemonHandle {
50 let shutdown = CancellationToken::new();
51 let proc = DaemonProcess::new(shutdown.clone(), wallet);
52
53 #[cfg(not(feature = "wasm-web"))]
54 {
55 let jh = crate::utils::spawn(proc.run());
56 DaemonHandle { shutdown, jh }
57 }
58 #[cfg(feature = "wasm-web")]
59 {
60 crate::utils::spawn(proc.run());
61 DaemonHandle { shutdown }
62 }
63}
64
65struct DaemonProcess {
68 shutdown: CancellationToken,
69
70 connected: AtomicBool,
71
72 wallet: Weak<WalletInner>,
77
78 sync_interval: Duration,
81
82 manual_sync: bool,
84}
85
86impl DaemonProcess {
87 fn new(
88 shutdown: CancellationToken,
89 wallet: &Wallet,
90 ) -> DaemonProcess {
91 DaemonProcess {
92 connected: AtomicBool::new(false),
93 shutdown,
94 sync_interval: Duration::from_secs(wallet.config().daemon_sync_interval_secs),
95 manual_sync: wallet.config().daemon_manual_sync,
96 wallet: Arc::downgrade(&wallet.inner),
97 }
98 }
99
100 fn wallet(&self) -> Option<Wallet> {
113 match self.wallet.upgrade() {
114 Some(inner) => Some(Wallet { inner }),
115 None => {
116 self.shutdown.cancel();
117 None
118 },
119 }
120 }
121
122 async fn run_mailbox_messages_process(&self) {
129 loop {
130 let shutdown = self.shutdown.clone();
131 if self.connected.load(Ordering::Relaxed) {
132 trace!("Daemon subscribing to mailbox message stream");
133 let r = Wallet::subscribe_process_mailbox_messages_weak(
134 self.wallet.clone(), None, shutdown,
135 ).await;
136 if let Err(e) = r {
137 warn!("An error occurred while processing mailbox messages: {e:#}");
138 self.connected.store(false, Ordering::Relaxed);
139 }
140 }
141
142 futures::select! {
143 _ = bark_runtime::sleep(self.sync_interval).fuse() => {},
144 _ = self.shutdown.cancelled().fuse() => {
145 info!("Shutdown signal received! Shutting mailbox messages process...");
146 break;
147 },
148 }
149 }
150 }
151
152 async fn handle_round_event(&self, wallet: &Wallet, event: &RoundEvent) -> anyhow::Result<()> {
153 match &event {
155 &RoundEvent::Attempt(attempt) => {
156 if attempt.attempt_seq == 0 {
157 if let Err(err) = wallet.join_round_for_maintenance_refresh(attempt).await {
158 warn!("Failed to join round for maintenance refresh: {:#}", err);
159 }
160 };
161 },
162 _ => {},
163 };
164
165 wallet.progress_pending_rounds(Some(event)).await
166 }
167
168 async fn process_round_event_stream(
174 &self,
175 backoff: &mut ReconnectBackoff,
176 ) -> anyhow::Result<()> {
177 let mut events = {
180 let Some(wallet) = self.wallet() else { return Ok(()) };
181 trace!("Daemon subscribing to round event stream");
182 wallet.subscribe_round_events().await?
183 };
184 trace!("Daemon connected to round event stream");
185
186 loop {
187 futures::select! {
188 res = events.next().fuse() => {
189 match res {
190 Some(Ok(event)) => {
191 backoff.reset();
192 let Some(wallet) = self.wallet() else { return Ok(()) };
193 if let Err(e) = self.handle_round_event(&wallet, &event).await {
194 warn!("Error processing round event: {e:#}");
195 }
196 },
197 Some(Err(e)) => {
198 return Err(e.context("error on event stream"));
199 },
200 None => {
201 return Ok(());
202 },
203 }
204 },
205 _ = self.shutdown.cancelled().fuse() => {
206 info!("Shutdown signal received! Shutting round events stream...");
207 return Ok(());
208 },
209 }
210 }
211 }
212
213 async fn run_round_events_process(&self) {
216 let mut backoff = ReconnectBackoff::new();
217 loop {
218 if self.shutdown.is_cancelled() {
219 info!("Shutdown signal received! Shutting round events process...");
220 break;
221 }
222
223 match self.process_round_event_stream(&mut backoff).await {
224 Ok(()) => {},
225 Err(e) if crate::utils::is_h2_stream_error(&e) => {
229 trace!("Round events stream reset by server, reconnecting: {e:#}");
230 },
231 Err(e) => {
232 warn!("An error occured while processing pending rounds: {e:#}");
233 },
234 }
235
236 futures::select! {
243 _ = backoff.wait().fuse() => {},
244 _ = self.shutdown.cancelled().fuse() => {
245 info!("Shutdown signal received! Shutting round events process...");
246 break;
247 },
248 }
249 }
250 }
251
252 async fn run_server_connection_check_process(&self) {
257 loop {
258 futures::select! {
259 _ = bark_runtime::sleep(self.sync_interval).fuse() => {},
260 _ = self.shutdown.cancelled().fuse() => {
261 info!("Shutdown signal received! Shutting server connection check process...");
262 break;
263 },
264 }
265
266 if self.connected.load(Ordering::Relaxed) {
267 continue;
268 }
269
270 let Some(wallet) = self.wallet() else { break };
271 let result = wallet.refresh_server().await;
272 if let Err(ref e) = result {
273 warn!("Ark server reconnect failed: {:#}", e);
274 } else {
275 info!("Ark server reconnected");
276 self.connected.store(true, Ordering::Relaxed);
277 }
278 }
279 }
280
281 async fn run_sync_processes(&self) {
282 let mut tip_rx = match self.wallet() {
287 Some(wallet) => match wallet.chain().tip_watcher(self.sync_interval).await {
288 Ok(watcher) => Some(watcher.subscribe()),
289 Err(e) => {
290 warn!("Daemon failed to start tip watcher, syncing on interval only: {e:#}");
291 None
292 },
293 },
294 None => None,
295 };
296
297 loop {
300 if let Some(wallet) = self.wallet() {
302 if self.connected.load(Ordering::Relaxed) {
303 if let Err(e) = wallet.chain().update_fee_rates(wallet.config().fallback_fee_rate).await {
304 warn!("An error occured while updating fee rates: {e:#}");
305 }
306
307 if let Err(e) = wallet.sync_pending_boards().await {
308 warn!("An error occured while syncing pending board: {e:#}");
309 }
310
311 if let Err(e) = wallet.sync_pending_offboards().await {
312 warn!("An error occured while syncing pending offboards: {e:#}");
313 }
314 }
315
316 if let Err(e) = wallet.sync_onchain().await {
317 warn!("An error occured while syncing onchain: {e:#}");
318 }
319
320 if let Err(e) = wallet.sync_pending_rounds().await {
321 warn!("An error occured while syncing pending rounds: {e:#}");
322 }
323 } else {
324 info!("Wallet has been dropped. Shutting down sync processes...");
325 break;
326 }
327
328 futures::select! {
329 _ = bark_runtime::sleep(self.sync_interval).fuse() => {},
330 triggered = async {
331 match tip_rx.as_mut() {
332 Some(rx) => rx.changed().await.is_ok(),
333 None => std::future::pending::<bool>().await,
334 }
335 }.fuse() => {
336 if triggered {
337 trace!("Daemon sync triggered by new chain tip");
338 } else {
339 warn!("Tip watcher stopped; daemon falls back to interval-only syncing");
340 tip_rx = None;
341 }
342 },
343 _ = self.shutdown.cancelled().fuse() => {
344 info!("Shutdown signal received! Shutting down sync processes...");
345 break;
346 },
347 }
348 }
349 }
350
351 async fn run_exit_progress_process(&self) {
358 loop {
359 if let Some(wallet) = self.wallet() {
360 if wallet.inner.onchain.is_some() {
361 if let Err(e) = wallet.exit_mgr().progress_exits_with_cpfp(&wallet, None).await {
362 warn!("An error occurred while progressing exits: {:#}", e);
363 }
364 } else {
365 if let Err(e) = wallet.exit_mgr().progress_exits(&wallet).await {
366 warn!("An error occurred while progressing exits: {:#}", e);
367 }
368 }
369 } else {
370 info!("Wallet has been dropped. Shutting down exit progress process...");
371 break;
372 }
373
374 futures::select! {
375 _ = bark_runtime::sleep(self.sync_interval).fuse() => {},
376 _ = self.shutdown.cancelled().fuse() => {
377 info!("Shutdown signal received! Shutting down exit progress process...");
378 break;
379 },
380 }
381 }
382 }
383
384 async fn run_startup_tasks(&self) {
386 let Some(wallet) = self.wallet() else { return };
387
388 let result = wallet.refresh_server().await;
393 if let Err(ref e) = result {
394 warn!("Ark server refresh failed: {:#}", e);
395 }
396 let connected = wallet.inner.server.initialized();
397 self.connected.store(connected, Ordering::Relaxed);
398
399 if !self.manual_sync {
400 wallet.sync().await;
401 }
402 }
403
404 pub async fn run(self) {
405 {
406 let Some(wallet) = self.wallet() else { return };
407 info!("Starting daemon for wallet {}", wallet.fingerprint());
408 }
409
410 self.run_startup_tasks().await;
411 trace!("Daemon startup tasks complete, starting background processes");
412
413 if self.manual_sync {
414 info!("Daemon running in manual-sync mode; background sync disabled");
417 let _ = self.run_server_connection_check_process().await;
418 } else {
419 #[cfg(not(feature = "wasm-web"))]
420 {
421 let proc = Arc::new(self);
425 let p1 = Arc::clone(&proc);
426 let p2 = Arc::clone(&proc);
427 let p3 = Arc::clone(&proc);
428 let p4 = Arc::clone(&proc);
429 let p5 = Arc::clone(&proc);
430 let _ = futures::join!(
431 supervised("server-connection", move || {
432 let p = Arc::clone(&p1);
433 async move { p.run_server_connection_check_process().await }
434 }),
435 supervised("round-events", move || {
436 let p = Arc::clone(&p2);
437 async move { p.run_round_events_process().await }
438 }),
439 supervised("sync", move || {
440 let p = Arc::clone(&p3);
441 async move { p.run_sync_processes().await }
442 }),
443 supervised("exit-progress", move || {
444 let p = Arc::clone(&p4);
445 async move { p.run_exit_progress_process().await }
446 }),
447 supervised("mailbox", move || {
448 let p = Arc::clone(&p5);
449 async move { p.run_mailbox_messages_process().await }
450 }),
451 );
452 }
453 #[cfg(feature = "wasm-web")]
454 {
455 let _ = futures::join!(
456 self.run_server_connection_check_process(),
457 self.run_round_events_process(),
458 self.run_sync_processes(),
459 self.run_exit_progress_process(),
460 self.run_mailbox_messages_process(),
461 );
462 }
463 }
464
465 info!("Daemon gracefully stopped");
466 }
467}
468
469#[cfg(not(feature = "wasm-web"))]
473async fn supervised<F, Fut>(name: &'static str, f: F)
474where
475 F: Fn() -> Fut,
476 Fut: std::future::Future<Output = ()> + Send + 'static,
477{
478 loop {
479 match tokio::spawn(f()).await {
480 Ok(()) => break,
481 Err(e) => {
482 warn!("Daemon task '{}' terminated unexpectedly, restarting: {e}", name);
483 bark_runtime::sleep(Duration::from_secs(1)).await;
484 },
485 }
486 }
487}