Skip to main content

bark/vtxo/
mod.rs

1
2pub mod selection;
3mod signing;
4mod state;
5
6pub use self::selection::{FilterVtxos, RefreshStrategy, VtxoFilter};
7pub use self::state::{VtxoLockHolder, VtxoState, VtxoStateKind, WalletVtxo};
8
9use log::{debug, error, trace};
10use ark::{ProtocolEncoding, Vtxo};
11use ark::vtxo::{Full, VtxoRef};
12
13use crate::Wallet;
14
15#[derive(Debug, thiserror::Error)]
16pub enum VtxoValidationError {
17	#[error("chain error: {0}")]
18	Chain(anyhow::Error),
19	#[error("anchor not found")]
20	AnchorNotFound,
21	#[error("invalid: {0}")]
22	Invalid(#[from] ark::vtxo::VtxoValidationError),
23}
24
25impl Wallet {
26	/// Attempts to lock VTXOs with the given [VtxoId](ark::VtxoId) values.
27	///
28	/// Only [VtxoStateKind::Spendable] vtxos can be locked; re-locking a
29	/// vtxo that is already in the exact target state (same holder) is a
30	/// no-op success, but any other prior state — including a Locked vtxo
31	/// owned by a different holder — fails. The whole batch is atomic:
32	/// if any vtxo fails the check, no vtxo's state changes.
33	///
34	/// `holder` records which operation is reserving the vtxos so
35	/// "who holds this vtxo?" is a typed lookup. Pass `None` only for
36	/// the narrow window before the operation's holder identity is
37	/// known (e.g. offboard's preparatory arkoor).
38	///
39	/// # Errors
40	/// - If any VTXO is not Spendable (and not already locked by the same holder).
41	/// - If a VTXO doesn't exist.
42	/// - If a database error occurs.
43	pub async fn lock_vtxos(
44		&self,
45		vtxos: impl IntoIterator<Item = impl VtxoRef>,
46		holder: Option<VtxoLockHolder>,
47	) -> anyhow::Result<()> {
48		self.set_vtxo_states(
49			vtxos, &VtxoState::Locked { holder }, &[VtxoStateKind::Spendable],
50		).await
51	}
52
53	/// Marks VTXOs as [VtxoState::Spent].
54	///
55	/// This operation is idempotent: VTXOs already in [VtxoState::Spent] will
56	/// remain spent without inserting a redundant state entry.
57	///
58	/// # Errors
59	/// - If the VTXO doesn't exist.
60	/// - If a database error occurs.
61	pub async fn mark_vtxos_as_spent(
62		&self,
63		vtxos: impl IntoIterator<Item = impl VtxoRef>,
64	) -> anyhow::Result<()> {
65		const ALLOWED: &[VtxoStateKind] = &[
66			VtxoStateKind::Spendable,
67			VtxoStateKind::Locked,
68			VtxoStateKind::Spent,
69		];
70		self.set_vtxo_states(vtxos, &VtxoState::Spent, ALLOWED).await
71	}
72
73	/// Marks VTXOs as [VtxoState::Exited]. Called from the unilateral exit progression once
74	/// every exit transaction has been broadcast — at that point the VTXO is effectively gone
75	/// from the protocol's view, but it shouldn't be confused with a forfeited VTXO.
76	///
77	/// This operation is idempotent: VTXOs already in [VtxoState::Exited] will remain exited
78	/// without inserting a redundant state entry.
79	///
80	/// # Errors
81	/// - If the VTXO is in a state other than `Spendable`, `Locked`, or `Exited`.
82	/// - If the VTXO doesn't exist.
83	/// - If a database error occurs.
84	pub async fn mark_vtxos_as_exited(
85		&self,
86		vtxos: impl IntoIterator<Item = impl VtxoRef>,
87	) -> anyhow::Result<()> {
88		const ALLOWED: &[VtxoStateKind] = &[
89			VtxoStateKind::Spendable,
90			VtxoStateKind::Locked,
91			VtxoStateKind::Exited,
92		];
93		self.set_vtxo_states(vtxos, &VtxoState::Exited, ALLOWED).await
94	}
95
96	/// Updates the state set the [VtxoState] of VTXOs corresponding to each given
97	/// [VtxoId](ark::VtxoId) while validating if the transition is allowed based
98	/// on the current state and allowed transitions.
99	///
100	/// # Parameters
101	/// - `vtxos`: The [VtxoId](ark::VtxoId) of each [Vtxo] to update.
102	/// - `state`: A reference to the new [VtxoState] that the VTXOs should be transitioned to.
103	/// - `allowed_states`: A slice of [VtxoStateKind] representing the permissible current states
104	///   from which the VTXOs are allowed to transition to the given `state`. If an empty
105	///   slice is passed, all states are allowed.
106	///
107	/// # Errors
108	/// - The database operation to update the states fails.
109	/// - The state transition is invalid or does not match the allowed transitions.
110	pub async fn set_vtxo_states(
111		&self,
112		vtxos: impl IntoIterator<Item = impl VtxoRef>,
113		state: &VtxoState,
114		mut allowed_states: &[VtxoStateKind],
115	) -> anyhow::Result<()> {
116		if allowed_states.is_empty() {
117			allowed_states = VtxoStateKind::ALL;
118		}
119
120		let ids: Vec<_> = vtxos.into_iter().map(|v| v.vtxo_id()).collect();
121		self.inner.db.update_vtxo_states_checked(&ids, state.clone(), allowed_states).await
122	}
123
124	/// Stores the given collection of VTXOs in the wallet with an initial state of
125	/// [VtxoState::Locked].
126	///
127	/// It does nothing if the VTXOs already exist.
128	///
129	/// Also posts the vtxo IDs to the server's recovery mailbox (non-critical, errors are logged).
130	///
131	/// # Parameters
132	/// - `vtxos`: The VTXOs to store in the wallet.
133	pub async fn store_locked_vtxos<'a>(
134		&self,
135		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>> + Clone,
136		holder: Option<VtxoLockHolder>,
137	) -> anyhow::Result<()> {
138		self.store_vtxos(vtxos.clone(), &VtxoState::Locked { holder }).await?;
139
140		// Post vtxo IDs to server for recovery (non-critical, just log errors)
141		if let Err(e) = self.post_recovery_vtxo_ids(vtxos.into_iter().map(|v| v.id())).await {
142			error!("Failed to post recovery vtxo IDs to server: {:#}", e);
143		}
144
145		Ok(())
146	}
147
148	/// Stores the given collection of VTXOs in the wallet with an initial state of
149	/// [VtxoState::Spendable].
150	///
151	/// It does nothing if the VTXOs already exist.
152	///
153	/// Also posts the vtxo IDs to the server's recovery mailbox (non-critical, errors are logged).
154	///
155	/// # Parameters
156	/// - `vtxos`: The VTXOs to store in the wallet.
157	pub async fn store_spendable_vtxos<'a>(
158		&self,
159		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>> + Clone,
160	) -> anyhow::Result<()> {
161		self.store_vtxos(vtxos.clone(), &VtxoState::Spendable).await?;
162
163		// Post vtxo IDs to server for recovery (non-critical, just log errors)
164		if let Err(e) = self.post_recovery_vtxo_ids(vtxos.into_iter().map(|v| v.id())).await {
165			error!("Failed to post recovery vtxo IDs to server: {:#}", e);
166		}
167
168		Ok(())
169	}
170
171	/// Stores the given collection of VTXOs in the wallet with an initial state of
172	/// [VtxoState::Spent].
173	///
174	/// It does nothing if the VTXOs already exist.
175	///
176	/// # Parameters
177	/// - `vtxos`: The VTXOs to store in the wallet.
178	pub async fn store_spent_vtxos<'a>(
179		&self,
180		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>>,
181	) -> anyhow::Result<()> {
182		self.store_vtxos(vtxos, &VtxoState::Spent).await
183	}
184
185	/// Stores the given collection of VTXOs in the wallet with the given initial state.
186	///
187	/// It does nothing if the VTXOs already exist.
188	///
189	/// # Parameters
190	/// - `vtxos`: The VTXOs to store in the wallet.
191	/// - `state`: The initial state of the VTXOs.
192	pub async fn store_vtxos<'a>(
193		&self,
194		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>>,
195		state: &VtxoState,
196	) -> anyhow::Result<()> {
197		let vtxos = vtxos.into_iter().map(|v| (v, state)).collect::<Vec<_>>();
198		if let Err(e) = self.inner.db.store_vtxos(&vtxos).await {
199			error!("An error occurred while storing {} VTXOs: {:#}", vtxos.len(), e);
200			error!("Raw VTXOs for debugging:");
201			for (vtxo, _) in vtxos {
202				error!(" - {}", vtxo.serialize_hex());
203			}
204			Err(e)
205		} else {
206			debug!("Stored {} VTXOs", vtxos.len());
207			trace!("New VTXO IDs: {:?}", vtxos.into_iter().map(|(v, _)| v.id()).collect::<Vec<_>>());
208			Ok(())
209		}
210	}
211
212	/// Attempts to unlock VTXOs with the given [VtxoId](ark::VtxoId) values. This will only work if the current
213	/// [VtxoState] is [VtxoStateKind::Locked] or [VtxoStateKind::Spendable].
214	///
215	/// This operation is idempotent: VTXOs already in [VtxoState::Spendable] will
216	/// remain spendable without inserting a redundant state entry.
217	///
218	/// # Errors
219	/// - If the VTXO is not currently locked or spendable.
220	/// - If the VTXO doesn't exist.
221	/// - If a database error occurs.
222	pub async fn unlock_vtxos(
223		&self,
224		vtxos: impl IntoIterator<Item = impl VtxoRef>,
225	) -> anyhow::Result<()> {
226		self.set_vtxo_states(
227			vtxos, &VtxoState::Spendable, &[VtxoStateKind::Locked, VtxoStateKind::Spendable],
228		).await
229	}
230}