Skip to main content

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}