Skip to main content

bark/
offboard.rs

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	/// Returns every in-progress offboard checkpoint.
14	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	/// Drives every pending offboard forward by one step (or to completion
25	/// if it's ready). Each action runs to its next park independently;
26	/// errors on one don't stop the others.
27	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	/// Fetches the current checkpoint for the given action id, if any.
43	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	/// Send to an onchain address using your offchain balance.
51	///
52	/// We can only offboard whole VTXOs, so this kicks off an arkoor
53	/// split first to produce an exact-sized vtxo plus change, then
54	/// offboards the new vtxo.
55	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	/// Offboard all VTXOs to a given [bitcoin::Address].
67	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	/// Offboard the given VTXOs to a given [bitcoin::Address].
76	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		// Drive once synchronously to get past the server interaction; the
105		// rest (confirmation polling) is left to sync_pending_offboards.
106		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				// A transient error parked the action before broadcast (the
112				// executor logged the error itself). The checkpoint
113				// survives, so the next wallet sync re-drives it.
114				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}