bark/vtxo/state.rs
1//! VTXO state tracking.
2//!
3//! This module defines the state machine used to track the lifecycle of each individual [Vtxo]
4//! managed by the wallet. A [Vtxo] can be:
5//! - created and ready to spend on Ark: [VtxoStateKind::Spendable]
6//! - owned but not usable because it is locked by subsystem: [VtxoStateKind::Locked]
7//! - consumed (no longer part of the wallet's balance): [VtxoStateKind::Spent]
8//!
9//! Two layers of state are provided:
10//! - [VtxoStateKind]: a compact, serialization-friendly discriminator intended for storage, logs,
11//! and wire formats. It maps to stable string identifiers via `as_str()`.
12//! - [VtxoState]: A richer state that might include metadata
13//!
14//! [WalletVtxo] pairs a concrete [Vtxo] with its current [VtxoState], providing the primary
15//! representation used by persistence and higher-level wallet logic.
16
17use std::fmt;
18use std::ops::Deref;
19
20use bitcoin::Weight;
21
22use ark::Vtxo;
23use ark::vtxo::{Bare, Full, VtxoRef};
24
25use crate::actions::WalletActionId;
26use crate::movement::MovementId;
27
28/// What kind of entity holds a [VtxoState::Locked] reservation.
29///
30/// The wallet's invariant is "every vtxo lock is owned by exactly one
31/// operation." For subsystems modelled as a [WalletAction] (today: the
32/// lightning send), that's an `Action(id)`. For subsystems that still
33/// run pre-action machinery (round, offboard, board, lightning receive)
34/// the holder is the operation's movement, captured as
35/// `Movement(MovementId)`. As those subsystems get converted to actions,
36/// new variants land here and the migration from `Movement` happens
37/// per-subsystem.
38#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(tag = "type", rename_all = "kebab-case")]
40pub enum VtxoLockHolder {
41 /// A [WalletAction] checkpointed in `bark_wallet_action_checkpoint`.
42 Action { id: WalletActionId },
43 /// A pre-action subsystem (round, offboard, board, lightning
44 /// receive). The movement is used as a stable handle.
45 Movement { id: MovementId },
46}
47
48impl From<MovementId> for VtxoLockHolder {
49 fn from(id: MovementId) -> Self {
50 VtxoLockHolder::Movement { id }
51 }
52}
53
54const SPENDABLE: &'static str = "Spendable";
55const LOCKED: &'static str = "Locked";
56const SPENT: &'static str = "Spent";
57
58/// A compact, serialization-friendly representation of a VTXO's state.
59///
60/// Use [VtxoState::kind] to derive it from a richer [VtxoState].
61#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
62pub enum VtxoStateKind {
63 /// The [Vtxo] is available and can be selected as an input for a new offboard/round.
64 Spendable,
65 /// The [Vtxo] is currently locked in an action.
66 Locked,
67 /// The [Vtxo] has been consumed and is no longer part of the wallet's balance.
68 Spent,
69}
70
71impl VtxoStateKind {
72 /// Returns a stable string identifier for this state, suitable for DB rows, logs, and APIs.
73 pub fn as_str(&self) -> &str {
74 match self {
75 VtxoStateKind::Spendable => SPENDABLE,
76 VtxoStateKind::Locked => LOCKED,
77 VtxoStateKind::Spent => SPENT,
78 }
79 }
80
81 pub fn as_byte(&self) -> u8 {
82 match self {
83 VtxoStateKind::Spendable => 0,
84 VtxoStateKind::Locked { .. } => 1,
85 VtxoStateKind::Spent => 2,
86 }
87 }
88
89 /// List of all existing states
90 pub const ALL: &[VtxoStateKind] = &[
91 VtxoStateKind::Spendable,
92 VtxoStateKind::Locked,
93 VtxoStateKind::Spent,
94 ];
95
96 /// List of the different states considered unspent
97 pub const UNSPENT_STATES: &[VtxoStateKind] = &[
98 VtxoStateKind::Spendable,
99 VtxoStateKind::Locked,
100 ];
101}
102
103impl fmt::Display for VtxoStateKind {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 f.write_str(self.as_str())
106 }
107}
108
109impl fmt::Debug for VtxoStateKind {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.write_str(self.as_str())
112 }
113}
114
115/// Rich [Vtxo] state carrying additional context needed at runtime.
116#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
117#[serde(tag = "type", rename_all = "kebab-case")]
118pub enum VtxoState {
119 /// The [Vtxo] is available and can be spent in a future round.
120 Spendable,
121 /// The [Vtxo] is currently locked by an operation.
122 ///
123 /// `holder` is `None` for the narrow window between creating a
124 /// fresh locked vtxo and pinning it to a specific operation (e.g.
125 /// during the offboard's preparatory arkoor). Production code
126 /// should set the holder explicitly whenever it knows the owner.
127 Locked {
128 holder: Option<VtxoLockHolder>,
129 },
130 /// The [Vtxo] has been consumed.
131 Spent,
132}
133
134impl VtxoState {
135 /// Returns the compact [VtxoStateKind] discriminator for this rich state.
136 pub fn kind(&self) -> VtxoStateKind {
137 match self {
138 VtxoState::Spendable => VtxoStateKind::Spendable,
139 VtxoState::Locked { .. } => VtxoStateKind::Locked,
140 VtxoState::Spent => VtxoStateKind::Spent,
141 }
142 }
143}
144
145/// A wallet-owned [Vtxo] paired with its current tracked state and a small set of
146/// genesis-derived summaries that the wallet would otherwise have to load the full
147/// exit chain for.
148///
149/// The wallet stores [Vtxo<Full>] on disk but listings, balance computations, coin
150/// selection, and refresh-strategy checks all run against this bare representation
151/// to avoid the per-VTXO memory cost (tens of KB at high exit depths). When an
152/// operation actually needs the exit chain — unilateral exit, server registration,
153/// arkoor send, offboard, counterparty-risk checks — call
154/// [crate::Wallet::get_full_vtxo] or
155/// [crate::persist::BarkPersister::get_full_vtxos] to fetch it from disk.
156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
157pub struct WalletVtxo {
158 /// The underlying [Vtxo] without its genesis chain.
159 #[serde(with = "ark::encode::serde")]
160 pub vtxo: Vtxo<Bare>,
161
162 /// The current tracked state for [`WalletVtxo`].
163 pub state: VtxoState,
164
165 /// Cached `vtxo.exit_depth()` from when the VTXO was inserted into the
166 /// wallet. Genesis is immutable post-creation, so this never drifts.
167 pub exit_depth: u16,
168
169 /// Cached sum of weight units for the unilateral exit transaction chain.
170 ///
171 /// Lets the refresh strategy answer "uneconomical to exit" without loading the genesis.
172 pub exit_tx_weight: Weight,
173}
174
175impl VtxoRef for WalletVtxo {
176 fn vtxo_id(&self) -> ark::VtxoId { self.vtxo.id() }
177 fn as_bare_vtxo(&self) -> Option<std::borrow::Cow<'_, Vtxo<Bare>>> {
178 Some(std::borrow::Cow::Borrowed(&self.vtxo))
179 }
180 fn as_full_vtxo(&self) -> Option<&Vtxo<Full>> { None }
181 fn into_full_vtxo(self) -> Option<Vtxo<Full>> { None }
182}
183
184impl<'a> VtxoRef for &'a WalletVtxo {
185 fn vtxo_id(&self) -> ark::VtxoId { self.vtxo.id() }
186 fn as_bare_vtxo(&self) -> Option<std::borrow::Cow<'_, Vtxo<Bare>>> {
187 Some(std::borrow::Cow::Borrowed(&self.vtxo))
188 }
189 fn as_full_vtxo(&self) -> Option<&Vtxo<Full>> { None }
190 fn into_full_vtxo(self) -> Option<Vtxo<Full>> { None }
191}
192
193impl AsRef<Vtxo<Bare>> for WalletVtxo {
194 fn as_ref(&self) -> &Vtxo<Bare> {
195 &self.vtxo
196 }
197}
198
199impl Deref for WalletVtxo {
200 type Target = Vtxo<Bare>;
201
202 fn deref(&self) -> &Vtxo<Bare> {
203 &self.vtxo
204 }
205}
206
207#[cfg(test)]
208mod test {
209 use super::*;
210
211 #[test]
212 fn convert_serialize() {
213 let states = [
214 VtxoStateKind::Spendable,
215 VtxoStateKind::Spent,
216 VtxoStateKind::Locked,
217 ];
218
219 assert_eq!(
220 serde_json::to_string(&states).unwrap(),
221 serde_json::to_string(&[SPENDABLE, SPENT, LOCKED]).unwrap(),
222 );
223
224 // If a compiler error occurs,
225 // This is a reminder that you should update the test above
226 match VtxoState::Spent {
227 VtxoState::Spendable => {},
228 VtxoState::Spent => {},
229 VtxoState::Locked { .. } => {},
230 }
231 }
232}