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::utils::time::sleep;
28use crate::vtxo::{VtxoState, VtxoStateKind, VtxoValidationError};
29
30pub(crate) const BASE_RETRY_BACKOFF: Duration = Duration::from_secs(1);
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub enum WalletActionCheckpoint {
39 LightningSend(LightningSend),
40 LightningReceive(LightningReceive),
41 ArkoorSend(ArkoorSend),
42 Board(Board),
43 Offboard(Offboard),
44}
45
46impl WalletActionCheckpoint {
47 pub fn id(&self) -> WalletActionId {
48 match self {
49 WalletActionCheckpoint::LightningSend(s) => s.id(),
50 WalletActionCheckpoint::LightningReceive(r) => r.id(),
51 WalletActionCheckpoint::ArkoorSend(s) => s.id(),
52 WalletActionCheckpoint::Board(s) => s.id(),
53 WalletActionCheckpoint::Offboard(o) => o.id(),
54 }
55 }
56
57 pub fn as_lightning_send(&self) -> Option<&LightningSend> {
58 match self {
59 WalletActionCheckpoint::LightningSend(s) => Some(s),
60 _ => None,
61 }
62 }
63
64 pub fn into_lightning_send(self) -> Option<LightningSend> {
65 match self {
66 WalletActionCheckpoint::LightningSend(s) => Some(s),
67 _ => None,
68 }
69 }
70
71 pub fn as_lightning_receive(&self) -> Option<&LightningReceive> {
72 match self {
73 WalletActionCheckpoint::LightningReceive(r) => Some(r),
74 _ => None,
75 }
76 }
77
78 pub fn into_lightning_receive(self) -> Option<LightningReceive> {
79 match self {
80 WalletActionCheckpoint::LightningReceive(r) => Some(r),
81 _ => None,
82 }
83 }
84
85 pub fn as_arkoor_send(&self) -> Option<&ArkoorSend> {
86 match self {
87 WalletActionCheckpoint::ArkoorSend(s) => Some(s),
88 _ => None,
89 }
90 }
91
92 pub fn into_arkoor_send(self) -> Option<ArkoorSend> {
93 match self {
94 WalletActionCheckpoint::ArkoorSend(s) => Some(s),
95 _ => None,
96 }
97 }
98
99 pub fn as_board(&self) -> Option<&Board> {
100 match self {
101 WalletActionCheckpoint::Board(s) => Some(s),
102 _ => None,
103 }
104 }
105
106 pub fn into_board(self) -> Option<Board> {
107 match self {
108 WalletActionCheckpoint::Board(s) => Some(s),
109 _ => None,
110 }
111 }
112
113 pub fn as_offboard(&self) -> Option<&Offboard> {
114 match self {
115 WalletActionCheckpoint::Offboard(o) => Some(o),
116 _ => None,
117 }
118 }
119
120 pub fn into_offboard(self) -> Option<Offboard> {
121 match self {
122 WalletActionCheckpoint::Offboard(o) => Some(o),
123 _ => None,
124 }
125 }
126}
127
128impl From<LightningSend> for WalletActionCheckpoint {
129 fn from(s: LightningSend) -> Self {
130 WalletActionCheckpoint::LightningSend(s)
131 }
132}
133
134impl From<LightningReceive> for WalletActionCheckpoint {
135 fn from(r: LightningReceive) -> Self {
136 WalletActionCheckpoint::LightningReceive(r)
137 }
138}
139
140impl From<ArkoorSend> for WalletActionCheckpoint {
141 fn from(s: ArkoorSend) -> Self {
142 WalletActionCheckpoint::ArkoorSend(s)
143 }
144}
145
146impl From<Board> for WalletActionCheckpoint {
147 fn from(s: Board) -> Self {
148 WalletActionCheckpoint::Board(s)
149 }
150}
151
152impl From<Offboard> for WalletActionCheckpoint {
153 fn from(o: Offboard) -> Self {
154 WalletActionCheckpoint::Offboard(o)
155 }
156}
157
158pub type WalletActionId = String;
164
165pub enum Advance<A> {
170 Next(A),
173 Park {
183 state: A,
184 wake_after: Option<Duration>,
185 error: Option<AdvanceError>,
186 },
187 Done,
191 Failed(anyhow::Error),
196}
197
198#[derive(Debug, thiserror::Error)]
199pub enum AdvanceError {
200 #[error("An error occurred while communicating with the server: {0}")]
201 Server(tonic::Status),
202 #[error("An error occurred while validating a VTXO: {0}")]
203 Vtxo(VtxoValidationError),
204 #[error("An error occurred while processing the action: {0}")]
205 Other(#[from] anyhow::Error),
206}
207
208impl AdvanceError {
209 pub fn is_server_rejection(&self) -> bool {
210 match self {
211 AdvanceError::Server(err) => err.is_rejection(),
212 _ => false,
213 }
214 }
215}
216
217pub fn park_with_backoff<A: WalletAction>(state: A, attempts: u32) -> Advance<A> {
218 let delay = attempts.pow(2) * BASE_RETRY_BACKOFF;
219 debug!("action {} retrying; sleeping {:?} before re-drive", state.id(), delay);
220 Advance::Park { state, wake_after: Some(delay), error: None }
221}
222
223#[cfg(debug_assertions)]
227fn double_drive_actions() -> bool {
228 std::env::var_os("BARK_DOUBLE_DRIVE_ACTIONS").is_some()
229}
230
231#[cfg(debug_assertions)]
236fn assert_reentrant<A>(
237 first: &Result<Advance<A>, AdvanceError>,
238 second: &Result<Advance<A>, AdvanceError>,
239) where
240 A: Into<WalletActionCheckpoint> + Clone,
241{
242 fn describe<A: Into<WalletActionCheckpoint> + Clone>(
243 result: &Result<Advance<A>, AdvanceError>,
244 ) -> (&'static str, Option<WalletActionCheckpoint>) {
245 match result {
246 Ok(Advance::Next(state)) => ("Next", Some(state.clone().into())),
247 Ok(Advance::Park { state, .. }) => ("Park", Some(state.clone().into())),
248 Ok(Advance::Done) => ("Done", None),
249 Ok(Advance::Failed(_)) => ("Failed", None),
250 Err(_) => ("Err", None),
251 }
252 }
253
254 assert_eq!(
255 describe(first), describe(second),
256 "wallet action is not reentrant: advancing the same state twice diverged",
257 );
258}
259
260#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
265#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
266pub trait WalletAction: Sized + Send + Sync {
267 fn id(&self) -> WalletActionId;
272
273 async fn advance(self, wallet: &Wallet) -> Result<Advance<Self>, AdvanceError>;
279
280 async fn on_retry(
286 self,
287 _wallet: &Wallet,
288 attempts: u32,
289 _error: AdvanceError,
290 ) -> anyhow::Result<Advance<Self>> {
291 Ok(park_with_backoff(self, attempts))
292 }
293
294 async fn on_rejection(self, _wallet: &Wallet, _error: AdvanceError)
300 -> anyhow::Result<Advance<Self>>;
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub enum DriveMode {
306 UntilParkOrDone,
308 UntilDone,
311}
312
313impl Wallet {
314 async fn get_vtxos_locked_by_action(
320 &self,
321 action_id: &WalletActionId,
322 ) -> anyhow::Result<Vec<WalletVtxo>> {
323 let all = self.inner.db.get_vtxos_by_state(&[VtxoStateKind::Locked]).await?;
324 Ok(all.into_iter().filter(|v| match &v.state {
325 VtxoState::Locked { holder: Some(crate::vtxo::VtxoLockHolder::Action { id }) } => {
326 id == action_id
327 },
328 _ => false,
329 }).collect())
330 }
331
332 pub async fn release_action_locks(&self, action_id: &WalletActionId) -> anyhow::Result<()> {
339 let vtxos = self.get_vtxos_locked_by_action(action_id).await?;
340 if vtxos.is_empty() {
341 return Ok(());
342 }
343 debug!("releasing {} vtxo lock(s) held by action {}", vtxos.len(), action_id);
344 self.unlock_vtxos(vtxos).await
345 }
346
347 pub async fn stop_wallet_action(&self, action_id: &WalletActionId) -> anyhow::Result<()> {
351 self.release_action_locks(action_id).await?;
352 self.inner.db.remove_wallet_action_checkpoint(action_id).await?;
353 Ok(())
354 }
355
356 pub async fn drive_action<A>(&self, action: A, mode: DriveMode) -> anyhow::Result<()>
362 where
363 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
364 {
365 let guard = match self.inner.lock_manager.try_lock(&action.id()).await {
366 Some(g) => g,
367 None => {
368 trace!("action {} is already being driven, skipping", action.id());
369 return Ok(());
370 },
371 };
372
373 self.drive_action_with_guard(action, mode, guard).await
374 }
375
376 pub(crate) async fn drive_action_with_guard<A>(
381 &self,
382 action: A,
383 mode: DriveMode,
384 _lock_guard: Box<dyn LockGuard>,
385 ) -> anyhow::Result<()>
386 where
387 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
388 {
389 Box::pin(self.run_action_loop(action, mode)).await
392 }
393
394 async fn advance_step<A>(&self, action: A) -> Result<Advance<A>, AdvanceError>
402 where
403 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
404 {
405 #[cfg(debug_assertions)]
406 if double_drive_actions() {
407 let snapshot = action.clone();
408 let first = action.advance(self).await;
409 let second = snapshot.advance(self).await;
410
411 let first_parked = matches!(first, Ok(Advance::Park { .. }));
420 if !first_parked {
421 assert_reentrant(&first, &second);
422 }
423 return second;
424 }
425
426 action.advance(self).await
427 }
428
429 async fn run_action_loop<A>(&self, mut action: A, mode: DriveMode) -> anyhow::Result<()>
430 where
431 A: WalletAction + Into<WalletActionCheckpoint> + Clone,
432 {
433 let mut retries: u32 = 0;
436
437 loop {
438 let id = action.id();
439 let snapshot = action.clone();
443
444 let advance = match self.advance_step(action).await {
445 Ok(advance) => { advance },
446 Err(e) if e.is_server_rejection() => {
447 warn!("action {} got rejected by server: {:#}", id, e);
448 snapshot.on_rejection(self, e).await.inspect_err(|err| {
449 warn!("action {} on_rejection failed, leaving checkpoint for retry: {:#}", id, err);
450 })?
451 }
452 Err(e) => {
453 retries = retries.saturating_add(1);
454 log::error!("Got error {:?} from action {}, retrying", e, id);
455 snapshot.on_retry(self, retries, e).await.inspect_err(|err| {
456 warn!("action {} on_retry failed, leaving checkpoint for retry: {:#}", id, err);
457 })?
458 },
459 };
460
461 match advance {
462 Advance::Next(next) => {
463 retries = 0;
464 let checkpoint: WalletActionCheckpoint = next.clone().into();
465 self.inner.db.upsert_wallet_action_checkpoint(&id, &checkpoint).await?;
466 action = next;
467 },
468 Advance::Park { state, wake_after, error } => {
469 let checkpoint: WalletActionCheckpoint = state.clone().into();
470 self.inner.db.upsert_wallet_action_checkpoint(&id, &checkpoint).await?;
471 match mode {
472 DriveMode::UntilParkOrDone => {
473 return match error {
474 Some(error) => Err(error.into()),
475 None => Ok(()),
476 };
477 },
478 DriveMode::UntilDone => {
479 if let Some(delay) = wake_after {
480 debug!("action {} parked; sleeping {:?} before re-drive", id, delay);
481 sleep(delay).await;
482 action = state;
483 } else {
484 return match error {
485 Some(error) => Err(error.into()),
486 None => Ok(()),
487 };
488 }
489 },
490 }
491 },
492 Advance::Done => {
493 if let Err(e) = self.stop_wallet_action(&id).await {
494 warn!("action {} done but couldn't cancel: {:#}", id, e);
495 }
496 return Ok(());
497 },
498 Advance::Failed(e) => {
499 if let Err(e) = self.stop_wallet_action(&id).await {
500 warn!("action {} failed but couldn't cancel: {:#}", id, e);
501 }
502 return Err(e);
503 },
504 }
505 }
506 }
507}