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}