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}