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}