1use std::sync::atomic::{AtomicBool, Ordering};
2use std::time::Duration;
3
4use ark::rounds::RoundEvent;
5use futures::{FutureExt, StreamExt};
6use log::{info, trace, warn};
7#[cfg(not(feature = "wasm-web"))]
8use tokio::task::JoinHandle;
9use tokio_util::sync::CancellationToken;
10
11use crate::Wallet;
12use crate::utils::ReconnectBackoff;
13use crate::utils::time::sleep;
14
15
16
17#[cfg(not(feature = "wasm-web"))]
19pub struct DaemonHandle {
20 shutdown: CancellationToken,
21 jh: JoinHandle<()>,
22}
23
24#[cfg(feature = "wasm-web")]
26pub struct DaemonHandle {
27 shutdown: CancellationToken,
28}
29
30impl DaemonHandle {
31 pub fn stop(&self) {
33 self.shutdown.cancel();
34 }
35
36 pub async fn stop_wait(self) -> anyhow::Result<()> {
38 self.stop();
39 #[cfg(not(feature = "wasm-web"))]
40 self.jh.await?;
41 Ok(())
42 }
43}
44
45pub(crate) fn start_daemon(
46 wallet: Wallet,
47) -> DaemonHandle {
48 let shutdown = CancellationToken::new();
49 let proc = DaemonProcess::new(shutdown.clone(), wallet);
50
51 #[cfg(not(feature = "wasm-web"))]
52 {
53 let jh = crate::utils::spawn(proc.run());
54 DaemonHandle { shutdown, jh }
55 }
56 #[cfg(feature = "wasm-web")]
57 {
58 crate::utils::spawn(proc.run());
59 DaemonHandle { shutdown }
60 }
61}
62
63struct DaemonProcess {
66 shutdown: CancellationToken,
67
68 connected: AtomicBool,
69 wallet: Wallet,
70}
71
72impl DaemonProcess {
73 fn new(
74 shutdown: CancellationToken,
75 wallet: Wallet,
76 ) -> DaemonProcess {
77 DaemonProcess {
78 connected: AtomicBool::new(false),
79 shutdown,
80 wallet,
81 }
82 }
83
84 fn sync_interval(&self) -> Duration {
85 Duration::from_secs(self.wallet.config().daemon_sync_interval_secs)
86 }
87
88 async fn run_mailbox_messages_process(&self) {
95 loop {
96 let shutdown = self.shutdown.clone();
97 if self.connected.load(Ordering::Relaxed) {
98 trace!("Daemon subscribing to mailbox message stream");
99 let r = self.wallet.subscribe_process_mailbox_messages(None, shutdown).await;
100 if let Err(e) = r {
101 warn!("An error occurred while processing mailbox messages: {e:#}");
102 self.connected.store(false, Ordering::Relaxed);
103 }
104 }
105
106 futures::select! {
107 _ = sleep(self.sync_interval()).fuse() => {},
108 _ = self.shutdown.cancelled().fuse() => {
109 info!("Shutdown signal received! Shutting mailbox messages process...");
110 break;
111 },
112 }
113 }
114 }
115
116 async fn run_boards_sync(&self) {
118 if let Err(e) = self.wallet.sync_pending_boards().await {
119 warn!("An error occured while syncing pending board: {e:#}");
120 }
121 }
122
123 async fn run_offboards_sync(&self) {
125 if let Err(e) = self.wallet.sync_pending_offboards().await {
126 warn!("An error occured while syncing pending offboards: {e:#}");
127 }
128 }
129
130 async fn run_rounds_sync(&self) {
132 if let Err(e) = self.wallet.sync_pending_rounds().await {
133 warn!("An error occured while syncing pending rounds: {e:#}");
134 }
135 }
136
137 async fn run_fee_rate_update(&self) {
139 if let Err(e) = self.wallet.chain().update_fee_rates(self.wallet.config().fallback_fee_rate).await {
140 warn!("An error occured while updating fee rates: {e:#}");
141 }
142 }
143
144 async fn run_onchain_sync(&self) {
146 if let Err(e) = self.wallet.sync_onchain().await {
147 warn!("An error occured while syncing onchain: {e:#}");
148 }
149 }
150
151 async fn run_exits(&self) {
153 if self.wallet.inner.onchain.is_some() {
154 if let Err(e) = self.wallet.exit_mgr().progress_exits_with_cpfp(&self.wallet, None).await {
155 warn!("An error occurred while progressing exits: {:#}", e);
156 }
157 } else {
158 if let Err(e) = self.wallet.exit_mgr().progress_exits(&self.wallet).await {
159 warn!("An error occurred while progressing exits: {:#}", e);
160 }
161 }
162 }
163
164 async fn handle_round_event(&self, event: &RoundEvent) -> anyhow::Result<()> {
165 match &event {
167 &RoundEvent::Attempt(attempt) => {
168 if attempt.attempt_seq == 0 {
169 if let Err(err) = self.wallet.join_round_for_maintenance_refresh(attempt).await {
170 warn!("Failed to join round for maintenance refresh: {:#}", err);
171 }
172 };
173 },
174 _ => {},
175 };
176
177 self.wallet.progress_pending_rounds(Some(event)).await
178 }
179
180 async fn process_round_event_stream(
186 &self,
187 backoff: &mut ReconnectBackoff,
188 ) -> anyhow::Result<()> {
189 trace!("Daemon subscribing to round event stream");
190 let mut events = self.wallet.subscribe_round_events().await?;
191 trace!("Daemon connected to round event stream");
192
193 loop {
194 futures::select! {
195 res = events.next().fuse() => {
196 match res {
197 Some(Ok(event)) => {
198 backoff.reset();
199 if let Err(e) = self.handle_round_event(&event).await {
200 warn!("Error processing round event: {e:#}");
201 }
202 },
203 Some(Err(e)) => {
204 return Err(e.context("error on event stream"));
205 },
206 None => {
207 return Ok(());
208 },
209 }
210 },
211 _ = self.shutdown.cancelled().fuse() => {
212 info!("Shutdown signal received! Shutting round events stream...");
213 return Ok(());
214 },
215 }
216 }
217 }
218
219 async fn run_round_events_process(&self) {
222 let mut backoff = ReconnectBackoff::new();
223 loop {
224 if self.shutdown.is_cancelled() {
225 info!("Shutdown signal received! Shutting round events process...");
226 break;
227 }
228
229 match self.process_round_event_stream(&mut backoff).await {
230 Ok(()) => {},
231 Err(e) if crate::utils::is_h2_stream_error(&e) => {
235 trace!("Round events stream reset by server, reconnecting: {e:#}");
236 },
237 Err(e) => {
238 warn!("An error occured while processing pending rounds: {e:#}");
239 },
240 }
241
242 futures::select! {
249 _ = backoff.wait().fuse() => {},
250 _ = self.shutdown.cancelled().fuse() => {
251 info!("Shutdown signal received! Shutting round events process...");
252 break;
253 },
254 }
255 }
256 }
257
258 async fn run_server_connection_check_process(&self) {
263 loop {
264 futures::select! {
265 _ = sleep(self.sync_interval()).fuse() => {},
266 _ = self.shutdown.cancelled().fuse() => {
267 info!("Shutdown signal received! Shutting server connection check process...");
268 break;
269 },
270 }
271
272 if self.connected.load(Ordering::Relaxed) {
273 continue;
274 }
275
276 let result = self.wallet.refresh_server().await;
277 if let Err(ref e) = result {
278 warn!("Ark server reconnect failed: {:#}", e);
279 } else {
280 info!("Ark server reconnected");
281 self.connected.store(true, Ordering::Relaxed);
282 }
283 }
284 }
285
286 async fn run_sync_processes(&self) {
287 loop {
289 if self.connected.load(Ordering::Relaxed) {
290 self.run_fee_rate_update().await;
291 self.run_boards_sync().await;
292 self.run_offboards_sync().await;
293 }
294 self.run_onchain_sync().await;
295 self.run_rounds_sync().await;
296 self.run_exits().await;
297
298 futures::select! {
299 _ = sleep(self.sync_interval()).fuse() => {},
300 _ = self.shutdown.cancelled().fuse() => {
301 info!("Shutdown signal received! Shutting sync processes...");
302 break;
303 },
304 }
305 }
306 }
307
308 async fn run_startup_tasks(&self) {
310 let result = self.wallet.refresh_server().await;
315 if let Err(ref e) = result {
316 warn!("Ark server refresh failed: {:#}", e);
317 }
318 let connected = self.wallet.inner.server.initialized();
319 self.connected.store(connected, Ordering::Relaxed);
320
321 if !self.wallet.config().daemon_manual_sync {
322 self.wallet.sync().await;
323 }
324 }
325
326 pub async fn run(self) {
327 info!("Starting daemon for wallet {}", self.wallet.fingerprint());
328
329 self.run_startup_tasks().await;
330 trace!("Daemon startup tasks complete, starting background processes");
331
332 if self.wallet.config().daemon_manual_sync {
333 info!("Daemon running in manual-sync mode; background sync disabled");
336 let _ = self.run_server_connection_check_process().await;
337 } else {
338 #[cfg(not(feature = "wasm-web"))]
339 {
340 use std::sync::Arc;
341
342 let proc = Arc::new(self);
346 let p1 = Arc::clone(&proc);
347 let p2 = Arc::clone(&proc);
348 let p3 = Arc::clone(&proc);
349 let p4 = Arc::clone(&proc);
350 let _ = futures::join!(
351 supervised("server-connection", move || {
352 let p = Arc::clone(&p1);
353 async move { p.run_server_connection_check_process().await }
354 }),
355 supervised("round-events", move || {
356 let p = Arc::clone(&p2);
357 async move { p.run_round_events_process().await }
358 }),
359 supervised("sync", move || {
360 let p = Arc::clone(&p3);
361 async move { p.run_sync_processes().await }
362 }),
363 supervised("mailbox", move || {
364 let p = Arc::clone(&p4);
365 async move { p.run_mailbox_messages_process().await }
366 }),
367 );
368 }
369 #[cfg(feature = "wasm-web")]
370 {
371 let _ = futures::join!(
372 self.run_server_connection_check_process(),
373 self.run_round_events_process(),
374 self.run_sync_processes(),
375 self.run_mailbox_messages_process(),
376 );
377 }
378 }
379
380 info!("Daemon gracefully stopped");
381 }
382}
383
384#[cfg(not(feature = "wasm-web"))]
388async fn supervised<F, Fut>(name: &'static str, f: F)
389where
390 F: Fn() -> Fut,
391 Fut: std::future::Future<Output = ()> + Send + 'static,
392{
393 loop {
394 match tokio::spawn(f()).await {
395 Ok(()) => break,
396 Err(e) => {
397 warn!("Daemon task '{}' terminated unexpectedly, restarting: {e}", name);
398 tokio::time::sleep(Duration::from_secs(1)).await;
399 },
400 }
401 }
402}