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>(state: A, attempts: u32) -> Advance<A> {
217 let delay = attempts.pow(2) * BASE_RETRY_BACKOFF;
218 debug!("action {} retrying; sleeping {:?} before re-drive", state.id(), delay);
219 Advance::Park { state, wake_after: Some(delay), error: None }
220}
221
222#[cfg(debug_assertions)]
227fn double_drive_actions() -> bool {
228 match std::env::var("BARK_DOUBLE_DRIVE_ACTIONS") {
229 Ok(v) => !matches!(v.trim(), "" | "0" | "false"),
230 Err(_) => false,
231 }
232}
233
234#[cfg(debug_assertions)]
239fn assert_reentrant<A>(
240 first: &Result<Advance<A>, AdvanceError>,
241 second: &Result<Advance<A>, AdvanceError>,
242) where
243 A: Into<WalletActionCheckpoint> + Clone,
244{
245 fn describe<A: Into<WalletActionCheckpoint> + Clone>(
246 result: &Result<Advance<A>, AdvanceError>,
247 ) -> (&'static str, Option<WalletActionCheckpoint>) {
248 match result {
249 Ok(Advance::Next(state)) => ("Next", Some(state.clone().into())),
250 Ok(Advance::Park { state, .. }) => ("Park", Some(state.clone().into())),
251 Ok(Advance::Done) => ("Done", None),
252 Ok(Advance::Failed(_)) => ("Failed", None),
253 Err(_) => ("Err", None),
254 }
255 }
256
257 assert_eq!(
258 describe(first), describe(second),
259 "wallet action is not reentrant: advancing the same state twice diverged",
260 );
261}
262
263#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
268#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
269pub trait WalletAction: Sized + Send + Sync {
270 fn id(&self) -> WalletActionId;
275
276 async fn advance(self, wallet: &Wallet) -> Result<Advance<Self>, AdvanceError>;
282
283 async fn on_retry(
289 self,
290 _wallet: &Wallet,
291 attempts: u32,
292 _error: AdvanceError,
293 ) -> anyhow::Result<Advance<Self>> {
294 Ok(park_with_backoff(self, attempts))
295 }
296
297 async fn on_rejection(self, _wallet: &Wallet, _error: AdvanceError)
303 -> anyhow::Result<Advance<Self>>;
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub enum DriveMode {
309 UntilParkOrDone,
311 UntilDone,
314}
315
316impl Wallet {
317 async fn get_vtxos_locked_by_action(
323 &self,
324 action_id: &WalletActionId,
325 ) -> anyhow::Result<Vec<WalletVtxo>> {
326 let all = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Locked]).await?;
327 Ok(all.into_iter().filter(|v| match &v.state {
328 VtxoState::Locked { holder: Some(crate::vtxo::VtxoLockHolder::Action { id }) } => {
329 id == action_id
330 },
331 _ => false,
332 }).collect())
333 }
334
335 pub async fn release_action_locks(&self, action_id: &WalletActionId) -> anyhow::Result<()> {
342 let vtxos = self.get_vtxos_locked_by_action(action_id).await?;
343 if vtxos.is_empty() {
344 return Ok(());
345 }
346 debug!("releasing {} vtxo lock(s) held by action {}", vtxos.len(), action_id);
347 self.unlock_vtxos(vtxos).await
348 }
349
350 pub async fn stop_wallet_action(&self, action_id: &WalletActionId) -> anyhow::Result<()> {
354 self.release_action_locks(action_id).await?;
355 self.inner.db.remove_wallet_action_checkpoint(action_id).await?;
356 Ok(())
357 }
358
359 pub async fn drive_action<A>(&self, action: A, mode: DriveMode) -> anyhow::Result<()>
365 where
366 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
367 {
368 let guard = match self.inner.lock_manager.try_lock(&action.id()).await {
369 Some(g) => g,
370 None => {
371 trace!("action {} is already being driven, skipping", action.id());
372 return Ok(());
373 },
374 };
375
376 self.drive_action_with_guard(action, mode, guard).await
377 }
378
379 pub(crate) async fn drive_action_with_guard<A>(
384 &self,
385 action: A,
386 mode: DriveMode,
387 _lock_guard: Box<dyn LockGuard>,
388 ) -> anyhow::Result<()>
389 where
390 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
391 {
392 Box::pin(self.run_action_loop(action, mode)).await
395 }
396
397 async fn advance_step<A>(&self, action: A) -> Result<Advance<A>, AdvanceError>
405 where
406 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
407 {
408 #[cfg(debug_assertions)]
409 if double_drive_actions() {
410 let snapshot = action.clone();
411 let first = action.advance(self).await;
412 let second = snapshot.advance(self).await;
413
414 let first_parked = matches!(first, Ok(Advance::Park { .. }));
423 if !first_parked {
424 assert_reentrant(&first, &second);
425 }
426 return second;
427 }
428
429 action.advance(self).await
430 }
431
432 async fn run_action_loop<A>(&self, mut action: A, mode: DriveMode) -> anyhow::Result<()>
433 where
434 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
435 {
436 let mut retries: u32 = 0;
439
440 loop {
441 let id = action.id();
442 let snapshot = action.clone();
446
447 let advance = match self.advance_step(action).await {
448 Ok(advance) => { advance },
449 Err(e) if e.is_server_rejection() => {
450 warn!("action {} got rejected by server: {:#}", id, e);
451 snapshot.on_rejection(self, e).await.inspect_err(|err| {
452 warn!("action {} on_rejection failed, leaving checkpoint for retry: {:#}", id, err);
453 })?
454 }
455 Err(e) => {
456 retries = retries.saturating_add(1);
457 log::error!("Got error {:?} from action {}, retrying", e, id);
458 snapshot.on_retry(self, retries, e).await.inspect_err(|err| {
459 warn!("action {} on_retry failed, leaving checkpoint for retry: {:#}", id, err);
460 })?
461 },
462 };
463
464 match advance {
465 Advance::Next(next) => {
466 retries = 0;
467 let checkpoint: WalletActionCheckpoint = next.clone().into();
468 self.inner.db.upsert_wallet_action_checkpoint(&id, &checkpoint).await?;
469 action = next;
470 },
471 Advance::Park { state, wake_after, error } => {
472 let checkpoint: WalletActionCheckpoint = state.clone().into();
473 self.inner.db.upsert_wallet_action_checkpoint(&id, &checkpoint).await?;
474 match mode {
475 DriveMode::UntilParkOrDone => {
476 return match error {
477 Some(error) => Err(error.into()),
478 None => Ok(()),
479 };
480 },
481 DriveMode::UntilDone => {
482 if let Some(delay) = wake_after {
483 debug!("action {} parked; sleeping {:?} before re-drive", id, delay);
484 bark_runtime::sleep(delay).await;
485 action = state;
486 } else {
487 return match error {
488 Some(error) => Err(error.into()),
489 None => Ok(()),
490 };
491 }
492 },
493 }
494 },
495 Advance::Done => {
496 if let Err(e) = self.stop_wallet_action(&id).await {
497 warn!("action {} done but couldn't cancel: {:#}", id, e);
498 }
499 return Ok(());
500 },
501 Advance::Failed(e) => {
502 if let Err(e) = self.stop_wallet_action(&id).await {
503 warn!("action {} failed but couldn't cancel: {:#}", id, e);
504 }
505 return Err(e);
506 },
507 }
508 }
509 }
510}