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 ark::Vtxo;
21use ark::vtxo::{Bare, Full, VtxoRef};
22use crate::movement::MovementId;
23
24const SPENDABLE: &'static str = "Spendable";
25const LOCKED: &'static str = "Locked";
26const SPENT: &'static str = "Spent";
27
28/// A compact, serialization-friendly representation of a VTXO's state.
29///
30/// Use [VtxoState::kind] to derive it from a richer [VtxoState].
31#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub enum VtxoStateKind {
33	/// The [Vtxo] is available and can be selected as an input for a new offboard/round.
34	Spendable,
35	/// The [Vtxo] is currently locked in an action.
36	Locked,
37	/// The [Vtxo] has been consumed and is no longer part of the wallet's balance.
38	Spent,
39}
40
41impl VtxoStateKind {
42	/// Returns a stable string identifier for this state, suitable for DB rows, logs, and APIs.
43	pub fn as_str(&self) -> &str {
44		match self {
45			VtxoStateKind::Spendable => SPENDABLE,
46			VtxoStateKind::Locked => LOCKED,
47			VtxoStateKind::Spent => SPENT,
48		}
49	}
50
51	pub fn as_byte(&self) -> u8 {
52		match self {
53			VtxoStateKind::Spendable => 0,
54			VtxoStateKind::Locked { .. } => 1,
55			VtxoStateKind::Spent => 2,
56		}
57	}
58
59	/// List of all existing states
60	pub const ALL: &[VtxoStateKind] = &[
61		VtxoStateKind::Spendable,
62		VtxoStateKind::Locked,
63		VtxoStateKind::Spent,
64	];
65
66	/// List of the different states considered unspent
67	pub const UNSPENT_STATES: &[VtxoStateKind] = &[
68		VtxoStateKind::Spendable,
69		VtxoStateKind::Locked,
70	];
71}
72
73impl fmt::Display for VtxoStateKind {
74	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75	    f.write_str(self.as_str())
76	}
77}
78
79impl fmt::Debug for VtxoStateKind {
80	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81	    f.write_str(self.as_str())
82	}
83}
84
85/// Rich [Vtxo] state carrying additional context needed at runtime.
86#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[serde(tag = "type", rename_all = "kebab-case")]
88pub enum VtxoState {
89	/// The [Vtxo] is available and can be spent in a future round.
90	Spendable,
91	/// The [Vtxo] is currently locked in an action.
92	Locked {
93		/// The ID of the associated [Movement](crate::movement::Movement) that locked this VTXO.
94		movement_id: Option<MovementId>,
95	},
96	/// The [Vtxo] has been consumed.
97	Spent,
98}
99
100impl VtxoState {
101	/// Returns the compact [VtxoStateKind] discriminator for this rich state.
102	pub fn kind(&self) -> VtxoStateKind {
103		match self {
104			VtxoState::Spendable => VtxoStateKind::Spendable,
105			VtxoState::Locked { .. } => VtxoStateKind::Locked,
106			VtxoState::Spent => VtxoStateKind::Spent,
107		}
108	}
109}
110
111/// A wallet-owned [Vtxo] paired with its current tracked state.
112#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
113pub struct WalletVtxo {
114	/// The underlying [Vtxo].
115	#[serde(with = "ark::encode::serde")]
116	pub vtxo: Vtxo<Full>,
117	/// The current tracked state for [WalletVtxo::into_vtxo].
118	pub state: VtxoState,
119}
120
121impl VtxoRef for WalletVtxo {
122	fn vtxo_id(&self) -> ark::VtxoId { self.vtxo.id() }
123	fn as_bare_vtxo(&self) -> Option<std::borrow::Cow<'_, Vtxo<Bare>>> {
124		Some(std::borrow::Cow::Owned(self.vtxo.to_bare()))
125	}
126	fn as_full_vtxo(&self) -> Option<&Vtxo<Full>> { Some(&self.vtxo) }
127	fn into_full_vtxo(self) -> Option<Vtxo<Full>> { Some(self.vtxo) }
128}
129
130impl<'a> VtxoRef for &'a WalletVtxo {
131	fn vtxo_id(&self) -> ark::VtxoId { self.vtxo.id() }
132	fn as_bare_vtxo(&self) -> Option<std::borrow::Cow<'_, Vtxo<Bare>>> {
133		Some(std::borrow::Cow::Owned(self.vtxo.to_bare()))
134	}
135	fn as_full_vtxo(&self) -> Option<&Vtxo<Full>> { Some(&self.vtxo) }
136	fn into_full_vtxo(self) -> Option<Vtxo<Full>> { Some(self.vtxo.clone()) }
137}
138
139impl AsRef<Vtxo<Full>> for WalletVtxo {
140	fn as_ref(&self) -> &Vtxo<Full> {
141		&self.vtxo
142	}
143}
144
145impl Deref for WalletVtxo {
146	type Target = Vtxo<Full>;
147
148	fn deref(&self) -> &Vtxo<Full> {
149		&self.vtxo
150	}
151}
152
153#[cfg(test)]
154mod test {
155	use super::*;
156
157	#[test]
158	fn convert_serialize() {
159		let states = [
160			VtxoStateKind::Spendable,
161			VtxoStateKind::Spent,
162			VtxoStateKind::Locked,
163		];
164
165		assert_eq!(
166			serde_json::to_string(&states).unwrap(),
167			serde_json::to_string(&[SPENDABLE, SPENT, LOCKED]).unwrap(),
168		);
169
170		// If a compiler error occurs,
171		// This is a reminder that you should update the test above
172		match VtxoState::Spent {
173			VtxoState::Spendable => {},
174			VtxoState::Spent => {},
175			VtxoState::Locked { .. } => {},
176		}
177	}
178}