bark/exit/
vtxo.rs

1//! Unilateral exit tracking and progression for individual VTXOs.
2//!
3//! This module defines types that track the lifecycle of a single [Vtxo] exit, including its current
4//! state, onchain transaction IDs, and a history of prior states for auditing and troubleshooting.
5//!
6//! The primary type is [ExitVtxo], which provides an async [`ExitVtxo::progress`] method to advance
7//! the unilateral exit state machine until completion or until the next step actionable step such
8//! as requiring more onchain funds or waiting for a confirmation.
9//!
10//! See [ExitModel] for persisting the state machine in a database.
11
12use bitcoin::{Amount, FeeRate, Txid};
13use log::{debug, trace};
14
15use ark::{Vtxo, VtxoId};
16use ark::vtxo::Full;
17
18use crate::exit::models::{ExitError, ExitState};
19use crate::exit::progress::{ExitStateProgress, ProgressContext, ProgressStep};
20use crate::exit::transaction_manager::ExitTransactionManager;
21use crate::onchain::ExitUnilaterally;
22use crate::persist::BarkPersister;
23use crate::persist::models::StoredExit;
24use crate::{Wallet, WalletVtxo};
25
26/// Tracks the exit lifecycle for a single [Vtxo].
27///
28/// An `ExitVtxo` maintains:
29/// - the underlying [Vtxo] being exited,
30/// - the set of related onchain transaction IDs in topographical order,
31/// - the current state [ExitState],
32/// - and a history of prior states for debugging and auditing.
33///
34/// Use [ExitVtxo::progress] to drive the state machine forward. The method is idempotent and will
35/// only persist when a logical state transition occurs.
36#[derive(Debug, Clone)]
37pub struct ExitVtxo {
38	vtxo_id: VtxoId,
39	amount: Amount,
40	state: ExitState,
41	history: Vec<ExitState>,
42	txids: Option<Vec<Txid>>,
43}
44
45impl ExitVtxo {
46	/// Create a new instance for the given [VtxoId] with an initial state of [ExitState::Start].
47	/// The unilateral exit can't progress until [ExitVtxo::initialize] is called.
48	///
49	/// # Parameters
50	/// - `vtxo_id`: the [VtxoId] being exited.
51	/// - `tip`: current chain tip used to initialize the starting state.
52	pub fn new(vtxo: &Vtxo<Full>, tip: u32) -> Self {
53		Self {
54			vtxo_id: vtxo.id(),
55			amount: vtxo.amount(),
56			state: ExitState::new_start(tip),
57			history: vec![],
58			txids: None,
59		}
60	}
61
62	/// Reconstruct an `ExitVtxo` from its parts. This leaves the instance in an uninitialized
63	/// state. Useful when loading a tracked exit from storage.
64	///
65	/// # Parameters
66	/// - `entry`: The persisted data to reconstruct this instance from.
67	/// - `vtxo`: The [Vtxo] that this exit is tracking.
68	pub fn from_entry(entry: StoredExit, vtxo: &WalletVtxo) -> Self {
69		assert_eq!(entry.vtxo_id, vtxo.id());
70		ExitVtxo {
71			vtxo_id: entry.vtxo_id,
72			amount: vtxo.amount(),
73			state: entry.state,
74			history: entry.history,
75			txids: None,
76		}
77	}
78
79	/// Returns the ID of the tracked [Vtxo].
80	pub fn id(&self) -> VtxoId {
81		self.vtxo_id
82	}
83
84	/// Returns the amount being exited.
85	pub fn amount(&self) -> Amount {
86		self.amount
87	}
88
89	/// Returns the current state of the unilateral exit.
90	pub fn state(&self) -> &ExitState {
91		&self.state
92	}
93
94	/// Returns the history of the exit machine in the order that states were observed.
95	pub fn history(&self) -> &Vec<ExitState> {
96		&self.history
97	}
98
99	/// Returns the set of exit-related transaction IDs, these may not be broadcast yet. If the
100	/// instance is not yet initialized, None will be returned.
101	pub fn txids(&self) -> Option<&Vec<Txid>> {
102		self.txids.as_ref()
103	}
104
105	/// True if the exit is currently [ExitState::Claimable] and can be claimed/spent.
106	pub fn is_claimable(&self) -> bool {
107		matches!(self.state, ExitState::Claimable(..))
108	}
109
110	/// True if [ExitVtxo::initialize] has been called and the exit is ready to progress.
111	pub fn is_initialized(&self) -> bool {
112		self.txids.is_some()
113	}
114
115	/// Prepares an [ExitVtxo] for progression by querying the list of transactions required to
116	/// process the unilateral exit and adds them to the exit transaction manager.
117	pub async fn initialize(
118		&mut self,
119		tx_manager: &mut ExitTransactionManager,
120		persister: &dyn BarkPersister,
121		onchain: &dyn ExitUnilaterally,
122	) -> anyhow::Result<(), ExitError> {
123		trace!("Initializing VTXO for exit {}", self.vtxo_id);
124		let vtxo = self.get_vtxo(persister).await?;
125		self.txids = Some(tx_manager.track_vtxo_exits(&vtxo, onchain).await?);
126		Ok(())
127	}
128
129	/// Advances the exit state machine for this [Vtxo].
130	///
131	/// The method:
132	/// - Attempts to transition the unilateral exit state machine.
133	/// - Persists only when a logical state change occurs.
134	///
135	/// Returns:
136	/// - `Ok(())` when no more immediate work is required, such as when we're waiting for a
137	///   confirmation or when the exit is complete.
138	/// - `Err(ExitError)` when an unrecoverable issue occurs, such as requiring more onchain funds
139	///   or if an exit transaction fails to broadcast; if the error includes a newer state, it will
140	///   be committed before returning.
141	///
142	/// Notes:
143	/// - If `fee_rate_override` is `None`, a suitable fee rate will be calculated.
144	pub async fn progress(
145		&mut self,
146		wallet: &Wallet,
147		tx_manager: &mut ExitTransactionManager,
148		onchain: &mut dyn ExitUnilaterally,
149		fee_rate_override: Option<FeeRate>,
150		continue_until_finished: bool,
151	) -> anyhow::Result<(), ExitError> {
152		if self.txids.is_none() {
153			return Err(ExitError::InternalError {
154				error: String::from("Unilateral exit not yet initialized"),
155			});
156		}
157
158		let wallet_vtxo = self.get_vtxo(&*wallet.db).await?;
159		const MAX_ITERATIONS: usize = 100;
160		for _ in 0..MAX_ITERATIONS {
161			let mut context = ProgressContext {
162				vtxo: &wallet_vtxo.vtxo,
163				exit_txids: self.txids.as_ref().unwrap(),
164				wallet,
165				fee_rate: fee_rate_override.unwrap_or(wallet.chain.fee_rates().await.fast),
166				tx_manager,
167			};
168			// Attempt to move to the next state, which may or may not generate a new state
169			trace!("Progressing VTXO {} at height {}", self.id(), wallet.chain.tip().await.unwrap());
170			match self.state.clone().progress(&mut context, onchain).await {
171				Ok(new_state) => {
172					self.update_state_if_newer(new_state, &*wallet.db).await?;
173					if !continue_until_finished {
174						return Ok(());
175					}
176					match ProgressStep::from_exit_state(&self.state) {
177						ProgressStep::Continue => debug!("VTXO {} can continue", self.id()),
178						ProgressStep::Done => return Ok(())
179					}
180				},
181				Err(e) => {
182					// We may need to commit a new state before returning an error
183					if let Some(new_state) = e.state {
184						self.update_state_if_newer(new_state, &*wallet.db).await?;
185					}
186					return Err(e.error);
187				}
188			}
189		}
190		debug_assert!(false, "Exceeded maximum iterations for progressing VTXO {}", self.id());
191		Ok(())
192	}
193
194	pub async fn get_vtxo(&self, persister: &dyn BarkPersister) -> anyhow::Result<WalletVtxo, ExitError> {
195		persister.get_wallet_vtxo(self.vtxo_id).await
196			.map_err(|e| ExitError::InvalidWalletState { error: e.to_string() })?
197			.ok_or_else(|| ExitError::InternalError {
198				error: format!("VTXO for exit couldn't be found: {}", self.vtxo_id)
199			})
200	}
201
202	async fn update_state_if_newer(
203		&mut self,
204		new: ExitState,
205		persister: &dyn BarkPersister,
206	) -> anyhow::Result<(), ExitError> {
207		// We don't want to push a new history item unless the state has changed logically
208		if new != self.state {
209			self.history.push(self.state.clone());
210			self.state = new;
211			self.persist(persister).await
212		} else {
213			Ok(())
214		}
215	}
216
217	async fn persist(&self, persister: &dyn BarkPersister) -> anyhow::Result<(), ExitError> {
218		persister.store_exit_vtxo_entry(&StoredExit::new(self)).await
219			.map_err(|e| ExitError::DatabaseVtxoStoreFailure {
220				vtxo_id: self.id(), error: e.to_string(),
221			})
222	}
223}