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