1pub mod arkoor_send;
11pub mod board;
12pub mod lightning;
13pub mod offboard;
14
15use std::time::Duration;
16
17use log::{debug, trace, warn};
18use server_rpc::StatusExt;
19
20use crate::{Wallet, WalletVtxo};
21use crate::actions::arkoor_send::ArkoorSend;
22use crate::actions::board::Board;
23use crate::actions::lightning::pay::LightningSend;
24use crate::actions::lightning::receive::LightningReceive;
25use crate::actions::offboard::Offboard;
26use crate::lock_manager::LockGuard;
27use crate::vtxo::{VtxoState, VtxoStateKind, VtxoValidationError};
28
29pub(crate) const BASE_RETRY_BACKOFF: Duration = Duration::from_secs(1);
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub enum WalletActionCheckpoint {
38 LightningSend(LightningSend),
39 LightningReceive(LightningReceive),
40 ArkoorSend(ArkoorSend),
41 Board(Board),
42 Offboard(Offboard),
43}
44
45impl WalletActionCheckpoint {
46 pub fn id(&self) -> WalletActionId {
47 match self {
48 WalletActionCheckpoint::LightningSend(s) => s.id(),
49 WalletActionCheckpoint::LightningReceive(r) => r.id(),
50 WalletActionCheckpoint::ArkoorSend(s) => s.id(),
51 WalletActionCheckpoint::Board(s) => s.id(),
52 WalletActionCheckpoint::Offboard(o) => o.id(),
53 }
54 }
55
56 pub fn as_lightning_send(&self) -> Option<&LightningSend> {
57 match self {
58 WalletActionCheckpoint::LightningSend(s) => Some(s),
59 _ => None,
60 }
61 }
62
63 pub fn into_lightning_send(self) -> Option<LightningSend> {
64 match self {
65 WalletActionCheckpoint::LightningSend(s) => Some(s),
66 _ => None,
67 }
68 }
69
70 pub fn as_lightning_receive(&self) -> Option<&LightningReceive> {
71 match self {
72 WalletActionCheckpoint::LightningReceive(r) => Some(r),
73 _ => None,
74 }
75 }
76
77 pub fn into_lightning_receive(self) -> Option<LightningReceive> {
78 match self {
79 WalletActionCheckpoint::LightningReceive(r) => Some(r),
80 _ => None,
81 }
82 }
83
84 pub fn as_arkoor_send(&self) -> Option<&ArkoorSend> {
85 match self {
86 WalletActionCheckpoint::ArkoorSend(s) => Some(s),
87 _ => None,
88 }
89 }
90
91 pub fn into_arkoor_send(self) -> Option<ArkoorSend> {
92 match self {
93 WalletActionCheckpoint::ArkoorSend(s) => Some(s),
94 _ => None,
95 }
96 }
97
98 pub fn as_board(&self) -> Option<&Board> {
99 match self {
100 WalletActionCheckpoint::Board(s) => Some(s),
101 _ => None,
102 }
103 }
104
105 pub fn into_board(self) -> Option<Board> {
106 match self {
107 WalletActionCheckpoint::Board(s) => Some(s),
108 _ => None,
109 }
110 }
111
112 pub fn as_offboard(&self) -> Option<&Offboard> {
113 match self {
114 WalletActionCheckpoint::Offboard(o) => Some(o),
115 _ => None,
116 }
117 }
118
119 pub fn into_offboard(self) -> Option<Offboard> {
120 match self {
121 WalletActionCheckpoint::Offboard(o) => Some(o),
122 _ => None,
123 }
124 }
125}
126
127impl From<LightningSend> for WalletActionCheckpoint {
128 fn from(s: LightningSend) -> Self {
129 WalletActionCheckpoint::LightningSend(s)
130 }
131}
132
133impl From<LightningReceive> for WalletActionCheckpoint {
134 fn from(r: LightningReceive) -> Self {
135 WalletActionCheckpoint::LightningReceive(r)
136 }
137}
138
139impl From<ArkoorSend> for WalletActionCheckpoint {
140 fn from(s: ArkoorSend) -> Self {
141 WalletActionCheckpoint::ArkoorSend(s)
142 }
143}
144
145impl From<Board> for WalletActionCheckpoint {
146 fn from(s: Board) -> Self {
147 WalletActionCheckpoint::Board(s)
148 }
149}
150
151impl From<Offboard> for WalletActionCheckpoint {
152 fn from(o: Offboard) -> Self {
153 WalletActionCheckpoint::Offboard(o)
154 }
155}
156
157pub type WalletActionId = String;
163
164pub enum Advance<A> {
169 Next(A),
172 Park {
182 state: A,
183 wake_after: Option<Duration>,
184 error: Option<AdvanceError>,
185 },
186 Done,
190 Failed(anyhow::Error),
195}
196
197#[derive(Debug, thiserror::Error)]
198pub enum AdvanceError {
199 #[error("An error occurred while communicating with the server: {0}")]
200 Server(tonic::Status),
201 #[error("An error occurred while validating a VTXO: {0}")]
202 Vtxo(VtxoValidationError),
203 #[error("An error occurred while processing the action: {0}")]
204 Other(#[from] anyhow::Error),
205}
206
207impl AdvanceError {
208 pub fn is_server_rejection(&self) -> bool {
209 match self {
210 AdvanceError::Server(err) => err.is_rejection(),
211 _ => false,
212 }
213 }
214}
215
216pub fn park_with_backoff<A: WalletAction>(
220 state: A,
221 attempts: u32,
222 error: Option<AdvanceError>,
223) -> Advance<A> {
224 let delay = attempts.pow(2) * BASE_RETRY_BACKOFF;
225 debug!("action {} retrying; sleeping {:?} before re-drive", state.id(), delay);
226 Advance::Park { state, wake_after: Some(delay), error }
227}
228
229#[cfg(debug_assertions)]
234fn double_drive_actions() -> bool {
235 match std::env::var("BARK_DOUBLE_DRIVE_ACTIONS") {
236 Ok(v) => !matches!(v.trim(), "" | "0" | "false"),
237 Err(_) => false,
238 }
239}
240
241#[cfg(debug_assertions)]
246fn assert_reentrant<A>(
247 first: &Result<Advance<A>, AdvanceError>,
248 second: &Result<Advance<A>, AdvanceError>,
249) where
250 A: Into<WalletActionCheckpoint> + Clone,
251{
252 fn describe<A: Into<WalletActionCheckpoint> + Clone>(
253 result: &Result<Advance<A>, AdvanceError>,
254 ) -> (&'static str, Option<WalletActionCheckpoint>) {
255 match result {
256 Ok(Advance::Next(state)) => ("Next", Some(state.clone().into())),
257 Ok(Advance::Park { state, .. }) => ("Park", Some(state.clone().into())),
258 Ok(Advance::Done) => ("Done", None),
259 Ok(Advance::Failed(_)) => ("Failed", None),
260 Err(_) => ("Err", None),
261 }
262 }
263
264 assert_eq!(
265 describe(first), describe(second),
266 "wallet action is not reentrant: advancing the same state twice diverged",
267 );
268}
269
270#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
275#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
276pub trait WalletAction: Sized + Send + Sync {
277 fn id(&self) -> WalletActionId;
282
283 async fn advance(self, wallet: &Wallet) -> Result<Advance<Self>, AdvanceError>;
289
290 async fn on_retry(
296 self,
297 _wallet: &Wallet,
298 attempts: u32,
299 _error: AdvanceError,
300 ) -> anyhow::Result<Advance<Self>> {
301 Ok(park_with_backoff(self, attempts, None))
302 }
303
304 async fn on_rejection(self, _wallet: &Wallet, _error: AdvanceError)
310 -> anyhow::Result<Advance<Self>>;
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum DriveMode {
316 UntilParkOrDone,
318 UntilDone,
321}
322
323impl Wallet {
324 async fn get_vtxos_locked_by_action(
330 &self,
331 action_id: &WalletActionId,
332 ) -> anyhow::Result<Vec<WalletVtxo>> {
333 let all = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Locked]).await?;
334 Ok(all.into_iter().filter(|v| match &v.state {
335 VtxoState::Locked { holder: Some(crate::vtxo::VtxoLockHolder::Action { id }) } => {
336 id == action_id
337 },
338 _ => false,
339 }).collect())
340 }
341
342 pub async fn release_action_locks(&self, action_id: &WalletActionId) -> anyhow::Result<()> {
349 let vtxos = self.get_vtxos_locked_by_action(action_id).await?;
350 if vtxos.is_empty() {
351 return Ok(());
352 }
353 debug!("releasing {} vtxo lock(s) held by action {}", vtxos.len(), action_id);
354 self.unlock_vtxos(
355 vtxos, Some(crate::vtxo::VtxoLockHolder::Action { id: action_id.clone() }),
356 ).await
357 }
358
359 pub async fn stop_wallet_action(&self, action_id: &WalletActionId) -> anyhow::Result<()> {
363 self.release_action_locks(action_id).await?;
364 self.inner.db.remove_wallet_action_checkpoint(action_id).await?;
365 Ok(())
366 }
367
368 pub async fn drive_action<A>(&self, action: A, mode: DriveMode) -> anyhow::Result<()>
374 where
375 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
376 {
377 let guard = match self.inner.lock_manager.try_lock(&action.id()).await {
378 Some(g) => g,
379 None => {
380 trace!("action {} is already being driven, skipping", action.id());
381 return Ok(());
382 },
383 };
384
385 self.drive_action_with_guard(action, mode, guard).await
386 }
387
388 pub(crate) async fn drive_action_with_guard<A>(
393 &self,
394 action: A,
395 mode: DriveMode,
396 _lock_guard: Box<dyn LockGuard>,
397 ) -> anyhow::Result<()>
398 where
399 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
400 {
401 Box::pin(self.run_action_loop(action, mode)).await
404 }
405
406 async fn advance_step<A>(&self, action: A) -> Result<Advance<A>, AdvanceError>
414 where
415 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
416 {
417 #[cfg(debug_assertions)]
418 if double_drive_actions() {
419 let snapshot = action.clone();
420 let first = action.advance(self).await;
421 let second = snapshot.advance(self).await;
422
423 let first_parked = matches!(first, Ok(Advance::Park { .. }));
432 if !first_parked {
433 assert_reentrant(&first, &second);
434 }
435 return second;
436 }
437
438 action.advance(self).await
439 }
440
441 async fn run_action_loop<A>(&self, mut action: A, mode: DriveMode) -> anyhow::Result<()>
442 where
443 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
444 {
445 let mut retries: u32 = 0;
448
449 loop {
450 let id = action.id();
451 let snapshot = action.clone();
455
456 let advance = match self.advance_step(action).await {
457 Ok(advance) => { advance },
458 Err(e) if e.is_server_rejection() => {
459 warn!("action {} got rejected by server: {:#}", id, e);
460 snapshot.on_rejection(self, e).await.inspect_err(|err| {
461 warn!("action {} on_rejection failed, leaving checkpoint for retry: {:#}", id, err);
462 })?
463 }
464 Err(e) => {
465 retries = retries.saturating_add(1);
466 log::error!("Got error {:?} from action {}, retrying", e, id);
467 snapshot.on_retry(self, retries, e).await.inspect_err(|err| {
468 warn!("action {} on_retry failed, leaving checkpoint for retry: {:#}", id, err);
469 })?
470 },
471 };
472
473 match advance {
474 Advance::Next(next) => {
475 retries = 0;
476 let checkpoint: WalletActionCheckpoint = next.clone().into();
477 self.inner.db.upsert_wallet_action_checkpoint(&id, &checkpoint).await?;
478 action = next;
479 },
480 Advance::Park { state, wake_after, error } => {
481 let checkpoint: WalletActionCheckpoint = state.clone().into();
482 self.inner.db.upsert_wallet_action_checkpoint(&id, &checkpoint).await?;
483 match mode {
484 DriveMode::UntilParkOrDone => {
485 return match error {
486 Some(error) => Err(error.into()),
487 None => Ok(()),
488 };
489 },
490 DriveMode::UntilDone => {
491 if let Some(delay) = wake_after {
492 debug!("action {} parked; sleeping {:?} before re-drive", id, delay);
493 bark_runtime::sleep(delay).await;
494 action = state;
495 } else {
496 return match error {
497 Some(error) => Err(error.into()),
498 None => Ok(()),
499 };
500 }
501 },
502 }
503 },
504 Advance::Done => {
505 if let Err(e) = self.stop_wallet_action(&id).await {
506 warn!("action {} done but couldn't cancel: {:#}", id, e);
507 }
508 return Ok(());
509 },
510 Advance::Failed(e) => {
511 if let Err(e) = self.stop_wallet_action(&id).await {
512 warn!("action {} failed but couldn't cancel: {:#}", id, e);
513 }
514 return Err(e);
515 },
516 }
517 }
518 }
519}