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