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	/// Marks VTXOs as [VtxoState::Exited]. Called from the unilateral exit progression once
64	/// every exit transaction has been broadcast — at that point the VTXO is effectively gone
65	/// from the protocol's view, but it shouldn't be confused with a forfeited VTXO.
66	///
67	/// This operation is idempotent: VTXOs already in [VtxoState::Exited] will remain exited
68	/// without inserting a redundant state entry.
69	///
70	/// # Errors
71	/// - If the VTXO is in a state other than `Spendable`, `Locked`, or `Exited`.
72	/// - If the VTXO doesn't exist.
73	/// - If a database error occurs.
74	pub async fn mark_vtxos_as_exited(
75		&self,
76		vtxos: impl IntoIterator<Item = impl VtxoRef>,
77	) -> anyhow::Result<()> {
78		const ALLOWED: &[VtxoStateKind] = &[
79			VtxoStateKind::Spendable,
80			VtxoStateKind::Locked,
81			VtxoStateKind::Exited,
82		];
83		self.set_vtxo_states(vtxos, &VtxoState::Exited, ALLOWED).await
84	}
85
86	/// Updates the state set the [VtxoState] of VTXOs corresponding to each given
87	/// [VtxoId](ark::VtxoId) while validating if the transition is allowed based
88	/// on the current state and allowed transitions.
89	///
90	/// # Parameters
91	/// - `vtxos`: The [VtxoId](ark::VtxoId) of each [Vtxo] to update.
92	/// - `state`: A reference to the new [VtxoState] that the VTXOs should be transitioned to.
93	/// - `allowed_states`: A slice of [VtxoStateKind] representing the permissible current states
94	///   from which the VTXOs are allowed to transition to the given `state`. If an empty
95	///   slice is passed, all states are allowed.
96	///
97	/// # Errors
98	/// - The database operation to update the states fails.
99	/// - The state transition is invalid or does not match the allowed transitions.
100	pub async fn set_vtxo_states(
101		&self,
102		vtxos: impl IntoIterator<Item = impl VtxoRef>,
103		state: &VtxoState,
104		mut allowed_states: &[VtxoStateKind],
105	) -> anyhow::Result<()> {
106		if allowed_states.is_empty() {
107			allowed_states = VtxoStateKind::ALL;
108		}
109
110		let ids: Vec<_> = vtxos.into_iter().map(|v| v.vtxo_id()).collect();
111		self.inner.db.update_vtxo_states_checked(&ids, state.clone(), allowed_states).await
112	}
113
114	/// Stores the given collection of VTXOs in the wallet with an initial state of
115	/// [VtxoState::Locked].
116	///
117	/// It does nothing if the VTXOs already exist.
118	///
119	/// # Parameters
120	/// - `vtxos`: The VTXOs to store in the wallet.
121	pub async fn store_locked_vtxos<'a>(
122		&self,
123		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>>,
124		holder: Option<VtxoLockHolder>,
125	) -> anyhow::Result<()> {
126		self.store_vtxos(vtxos, &VtxoState::Locked { holder }).await
127	}
128
129	/// Stores the given collection of VTXOs in the wallet with an initial state of
130	/// [VtxoState::Spendable].
131	///
132	/// It does nothing if the VTXOs already exist.
133	///
134	/// Also posts the vtxo IDs to the server's recovery mailbox (non-critical, errors are logged).
135	///
136	/// # Parameters
137	/// - `vtxos`: The VTXOs to store in the wallet.
138	pub async fn store_spendable_vtxos<'a>(
139		&self,
140		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>> + Clone,
141	) -> anyhow::Result<()> {
142		self.store_vtxos(vtxos.clone(), &VtxoState::Spendable).await?;
143
144		// Post vtxo IDs to server for recovery (non-critical, just log errors)
145		if let Err(e) = self.post_recovery_vtxo_ids(vtxos.into_iter().map(|v| v.id())).await {
146			error!("Failed to post recovery vtxo IDs to server: {:#}", e);
147		}
148
149		Ok(())
150	}
151
152	/// Stores the given collection of VTXOs in the wallet with an initial state of
153	/// [VtxoState::Spent].
154	///
155	/// It does nothing if the VTXOs already exist.
156	///
157	/// # Parameters
158	/// - `vtxos`: The VTXOs to store in the wallet.
159	pub async fn store_spent_vtxos<'a>(
160		&self,
161		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>>,
162	) -> anyhow::Result<()> {
163		self.store_vtxos(vtxos, &VtxoState::Spent).await
164	}
165
166	/// Stores the given collection of VTXOs in the wallet with the given initial state.
167	///
168	/// It does nothing if the VTXOs already exist.
169	///
170	/// # Parameters
171	/// - `vtxos`: The VTXOs to store in the wallet.
172	/// - `state`: The initial state of the VTXOs.
173	pub async fn store_vtxos<'a>(
174		&self,
175		vtxos: impl IntoIterator<Item = &'a Vtxo<Full>>,
176		state: &VtxoState,
177	) -> anyhow::Result<()> {
178		let vtxos = vtxos.into_iter().map(|v| (v, state)).collect::<Vec<_>>();
179		if let Err(e) = self.inner.db.store_vtxos(&vtxos).await {
180			error!("An error occurred while storing {} VTXOs: {:#}", vtxos.len(), e);
181			error!("Raw VTXOs for debugging:");
182			for (vtxo, _) in vtxos {
183				error!(" - {}", vtxo.serialize_hex());
184			}
185			Err(e)
186		} else {
187			debug!("Stored {} VTXOs", vtxos.len());
188			trace!("New VTXO IDs: {:?}", vtxos.into_iter().map(|(v, _)| v.id()).collect::<Vec<_>>());
189			Ok(())
190		}
191	}
192
193	/// Attempts to unlock VTXOs with the given [VtxoId](ark::VtxoId) values. This will only work if the current
194	/// [VtxoState] is [VtxoStateKind::Locked] or [VtxoStateKind::Spendable].
195	///
196	/// This operation is idempotent: VTXOs already in [VtxoState::Spendable] will
197	/// remain spendable without inserting a redundant state entry.
198	///
199	/// # Errors
200	/// - If the VTXO is not currently locked or spendable.
201	/// - If the VTXO doesn't exist.
202	/// - If a database error occurs.
203	pub async fn unlock_vtxos(
204		&self,
205		vtxos: impl IntoIterator<Item = impl VtxoRef>,
206	) -> anyhow::Result<()> {
207		self.set_vtxo_states(
208			vtxos, &VtxoState::Spendable, &[VtxoStateKind::Locked, VtxoStateKind::Spendable],
209		).await
210	}
211}