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