Skip to main content

bark/vtxo/
mod.rs

1
2mod selection;
3mod signing;
4mod state;
5
6pub use self::selection::{FilterVtxos, RefreshStrategy, VtxoFilter};
7pub use self::state::{VtxoState, VtxoStateKind, WalletVtxo};
8
9use log::{debug, error, trace};
10use ark::{ProtocolEncoding, Vtxo};
11use ark::vtxo::{Full, VtxoRef};
12
13use crate::Wallet;
14use crate::movement::MovementId;
15
16impl Wallet {
17	/// Attempts to lock VTXOs with the given [VtxoId](ark::VtxoId) values. This will only work if the current
18	/// [VtxoState] is contained by [VtxoStateKind::UNSPENT_STATES].
19	///
20	/// # Errors
21	/// - If the VTXO is not in a lockable [VtxoState].
22	/// - If the VTXO doesn't exist.
23	/// - If a database error occurs.
24	pub async fn lock_vtxos(
25		&self,
26		vtxos: impl IntoIterator<Item = impl VtxoRef>,
27		movement_id: Option<MovementId>,
28	) -> anyhow::Result<()> {
29		self.set_vtxo_states(
30			vtxos, &VtxoState::Locked { movement_id }, &VtxoStateKind::UNSPENT_STATES,
31		).await
32	}
33
34	/// Marks VTXOs as [VtxoState::Spent].
35	///
36	/// This operation is idempotent: VTXOs already in [VtxoState::Spent] will
37	/// remain spent without inserting a redundant state entry.
38	///
39	/// # Errors
40	/// - If the VTXO doesn't exist.
41	/// - If a database error occurs.
42	pub async fn mark_vtxos_as_spent(
43		&self,
44		vtxos: impl IntoIterator<Item = impl VtxoRef>,
45	) -> anyhow::Result<()> {
46		const ALLOWED: &[VtxoStateKind] = &[
47			VtxoStateKind::Spendable,
48			VtxoStateKind::Locked,
49			VtxoStateKind::Spent,
50		];
51		self.set_vtxo_states(vtxos, &VtxoState::Spent, ALLOWED).await
52	}
53
54	/// Updates the state set the [VtxoState] of VTXOs corresponding to each given
55	/// [VtxoId](ark::VtxoId) while validating if the transition is allowed based
56	/// on the current state and allowed transitions.
57	///
58	/// # Parameters
59	/// - `vtxos`: The [VtxoId](ark::VtxoId) of each [Vtxo] to update.
60	/// - `state`: A reference to the new [VtxoState] that the VTXOs should be transitioned to.
61	/// - `allowed_states`: A slice of [VtxoStateKind] representing the permissible current states
62	///   from which the VTXOs are allowed to transition to the given `state`. If an empty
63	///   slice is passed, all states are allowed.
64	///
65	/// # Errors
66	/// - The database operation to update the states fails.
67	/// - The state transition is invalid or does not match the allowed transitions.
68	pub async fn set_vtxo_states(
69		&self,
70		vtxos: impl IntoIterator<Item = impl VtxoRef>,
71		state: &VtxoState,
72		mut allowed_states: &[VtxoStateKind],
73	) -> anyhow::Result<()> {
74		if allowed_states.is_empty() {
75			allowed_states = VtxoStateKind::ALL;
76		}
77
78		let mut problematic_vtxos = Vec::new();
79		for vtxo in vtxos {
80			let id = vtxo.vtxo_id();
81			if let Err(e) = self.db.update_vtxo_state_checked(
82				id,
83				state.clone(),
84				allowed_states,
85			).await {
86				error!(
87					"Failed to set {} state with allowed states {:?} for VTXO {}: {:#}",
88					state.kind(), allowed_states, id, e,
89				);
90				problematic_vtxos.push(id);
91			}
92		}
93
94		if problematic_vtxos.is_empty() {
95			Ok(())
96		} else {
97			Err(anyhow!(
98				"Failed to set {} state for {} VTXOs: {:?}",
99				state.kind(),
100				problematic_vtxos.len(),
101				problematic_vtxos
102			))
103		}
104	}
105
106	/// Stores the given collection of VTXOs in the wallet with an initial state of
107	/// [VtxoState::Locked].
108	///
109	/// It does nothing if the VTXOs already exist.
110	///
111	/// # Parameters
112	/// - `vtxos`: The VTXOs to store in the wallet.
113	pub async fn store_locked_vtxos<'a>(
114		&self,
115		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>>,
116		movement_id: Option<MovementId>,
117	) -> anyhow::Result<()> {
118		self.store_vtxos(vtxos, &VtxoState::Locked { movement_id }).await
119	}
120
121	/// Stores the given collection of VTXOs in the wallet with an initial state of
122	/// [VtxoState::Spendable].
123	///
124	/// It does nothing if the VTXOs already exist.
125	///
126	/// Also posts the vtxo IDs to the server's recovery mailbox (non-critical, errors are logged).
127	///
128	/// # Parameters
129	/// - `vtxos`: The VTXOs to store in the wallet.
130	pub async fn store_spendable_vtxos<'a>(
131		&self,
132		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>> + Clone,
133	) -> anyhow::Result<()> {
134		self.store_vtxos(vtxos.clone(), &VtxoState::Spendable).await?;
135
136		// Post vtxo IDs to server for recovery (non-critical, just log errors)
137		if let Err(e) = self.post_recovery_vtxo_ids(vtxos.into_iter().map(|v| v.id())).await {
138			error!("Failed to post recovery vtxo IDs to server: {:#}", e);
139		}
140
141		Ok(())
142	}
143
144	/// Stores the given collection of VTXOs in the wallet with an initial state of
145	/// [VtxoState::Spent].
146	///
147	/// It does nothing if the VTXOs already exist.
148	///
149	/// # Parameters
150	/// - `vtxos`: The VTXOs to store in the wallet.
151	pub async fn store_spent_vtxos<'a>(
152		&self,
153		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>>,
154	) -> anyhow::Result<()> {
155		self.store_vtxos(vtxos, &VtxoState::Spent).await
156	}
157
158	/// Stores the given collection of VTXOs in the wallet with the given initial state.
159	///
160	/// It does nothing if the VTXOs already exist.
161	///
162	/// # Parameters
163	/// - `vtxos`: The VTXOs to store in the wallet.
164	/// - `state`: The initial state of the VTXOs.
165	pub async fn store_vtxos<'a>(
166		&self,
167		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>>,
168		state: &VtxoState,
169	) -> anyhow::Result<()> {
170		let vtxos = vtxos.into_iter().map(|v| (v, state)).collect::<Vec<_>>();
171		if let Err(e) = self.db.store_vtxos(&vtxos).await {
172			error!("An error occurred while storing {} VTXOs: {:#}", vtxos.len(), e);
173			error!("Raw VTXOs for debugging:");
174			for (vtxo, _) in vtxos {
175				error!(" - {}", vtxo.serialize_hex());
176			}
177			Err(e)
178		} else {
179			debug!("Stored {} VTXOs", vtxos.len());
180			trace!("New VTXO IDs: {:?}", vtxos.into_iter().map(|(v, _)| v.id()).collect::<Vec<_>>());
181			Ok(())
182		}
183	}
184
185	/// Attempts to unlock VTXOs with the given [VtxoId](ark::VtxoId) values. This will only work if the current
186	/// [VtxoState] is [VtxoStateKind::Locked] or [VtxoStateKind::Spendable].
187	///
188	/// This operation is idempotent: VTXOs already in [VtxoState::Spendable] will
189	/// remain spendable without inserting a redundant state entry.
190	///
191	/// # Errors
192	/// - If the VTXO is not currently locked or spendable.
193	/// - If the VTXO doesn't exist.
194	/// - If a database error occurs.
195	pub async fn unlock_vtxos(
196		&self,
197		vtxos: impl IntoIterator<Item = impl VtxoRef>,
198	) -> anyhow::Result<()> {
199		self.set_vtxo_states(
200			vtxos, &VtxoState::Spendable, &[VtxoStateKind::Locked, VtxoStateKind::Spendable],
201		).await
202	}
203}