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}