1
2use anyhow::Context;
3use bitcoin::{Amount, Txid};
4use log::{info, warn};
5
6use ark::vtxo::VtxoRef;
7
8use crate::Wallet;
9use crate::actions::{DriveMode, WalletActionId};
10use crate::actions::offboard::{Offboard, Progress, StartOffboardSpec, start_offboard};
11
12impl Wallet {
13 pub async fn pending_offboards(&self) -> anyhow::Result<Vec<Offboard>> {
15 let mut result = Vec::new();
16 for cp in self.inner.db.get_all_wallet_action_checkpoints().await? {
17 if let Some(o) = cp.into_offboard() {
18 result.push(o);
19 }
20 }
21 Ok(result)
22 }
23
24 pub async fn sync_pending_offboards(&self) -> anyhow::Result<()> {
28 let pending = self.pending_offboards().await?;
29 if pending.is_empty() {
30 return Ok(());
31 }
32 info!("Syncing {} pending offboard(s)", pending.len());
33 for action in pending {
34 let id = action.id();
35 if let Err(e) = self.drive_action(action, DriveMode::UntilParkOrDone).await {
36 warn!("Failed to sync offboard {}: {:#}", id, e);
37 }
38 }
39 Ok(())
40 }
41
42 pub async fn offboard_checkpoint(&self, id: &WalletActionId)
44 -> anyhow::Result<Option<Offboard>>
45 {
46 Ok(self.inner.db.get_wallet_action_checkpoint(id).await?
47 .and_then(|cp| cp.into_offboard()))
48 }
49
50 pub async fn send_onchain(
56 &self,
57 destination: bitcoin::Address,
58 amount: Amount,
59 ) -> anyhow::Result<Txid> {
60 let action = start_offboard(
61 self, destination, StartOffboardSpec::SendOnchain { amount },
62 ).await?;
63 self.run_offboard(action).await
64 }
65
66 pub async fn offboard_all(&self, address: bitcoin::Address) -> anyhow::Result<Txid> {
68 let input_vtxos = self.spendable_vtxos().await?;
69 let action = start_offboard(
70 self, address, StartOffboardSpec::OffboardWhole { vtxos: input_vtxos },
71 ).await?;
72 self.run_offboard(action).await
73 }
74
75 pub async fn offboard_vtxos<V: VtxoRef>(
77 &self,
78 vtxos: impl IntoIterator<Item = V>,
79 address: bitcoin::Address,
80 ) -> anyhow::Result<Txid> {
81 let mut input_vtxos = vec![];
82 for v in vtxos {
83 let id = v.vtxo_id();
84 let vtxo = match self.inner.db.get_wallet_vtxo(id).await? {
85 Some(vtxo) => vtxo,
86 _ => bail!("cannot find requested vtxo: {}", id),
87 };
88 input_vtxos.push(vtxo);
89 }
90 let action = start_offboard(
91 self, address, StartOffboardSpec::OffboardWhole { vtxos: input_vtxos },
92 ).await?;
93 self.run_offboard(action).await
94 }
95
96 async fn run_offboard(&self, action: Offboard) -> anyhow::Result<Txid> {
97 let offboard_id = action.id();
98 let guard = self.inner.lock_manager.try_lock(&offboard_id).await
99 .context("offboard action already in progress")?;
100
101 self.inner.db.upsert_wallet_action_checkpoint(&offboard_id, &action.clone().into()).await
102 .context("failed to persist initial offboard checkpoint")?;
103
104 self.drive_action_with_guard(action, DriveMode::UntilParkOrDone, guard).await?;
107
108 match self.offboard_checkpoint(&offboard_id).await? {
109 Some(o) => match o.progress {
110 Progress::AwaitingConfirmations { offboard_txid, .. } => Ok(offboard_txid),
111 other => bail!(
115 "offboard {} could not complete yet (parked in {:?}); \
116 it remains pending and will be retried on wallet sync",
117 offboard_id, other,
118 ),
119 },
120 None => bail!("offboard {} finished without producing a txid", offboard_id),
121 }
122 }
123}