bark/persist/
models.rs

1//! Persistence-focused data models.
2//!
3//! This module defines serializable types that mirror core in-memory structures but are tailored
4//! for durable storage and retrieval via a BarkPersister implementation.
5//!
6//! Intent
7//! - Keep storage concerns decoupled from runtime types used by protocol logic.
8//! - Provide stable, serde-friendly representations for database backends.
9//! - Enable forward/backward compatibility when schema migrations occur.
10
11use std::borrow::Cow;
12
13use bitcoin::{Amount, Transaction};
14use bitcoin::secp256k1::Keypair;
15use lightning_invoice::Bolt11Invoice;
16
17use ark::{Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
18use ark::musig::DangerousSecretNonce;
19use ark::tree::signed::{UnlockHash, VtxoTreeSpec};
20use ark::lightning::{Invoice, PaymentHash, Preimage};
21use ark::rounds::RoundSeq;
22use bitcoin_ext::BlockDelta;
23
24use crate::WalletVtxo;
25use crate::exit::{ExitVtxo, ExitState};
26use crate::movement::MovementId;
27use crate::round::{AttemptState, RoundFlowState, RoundParticipation, RoundState};
28
29/// Persisted representation of a pending board.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct PendingBoard {
32	/// This is the [bitcoin::Transaction] that has to
33	/// be confirmed onchain for the board to succeed.
34	pub funding_tx: Transaction,
35	/// The id of VTXOs being boarded.
36	///
37	/// Currently, this is always a vector of length 1
38	pub vtxos: Vec<VtxoId>,
39	/// The amount of the board.
40	pub amount: Amount,
41	/// The [MovementId] associated with this board.
42	pub movement_id: MovementId,
43}
44
45/// Persisted representation of a lightning send.
46///
47/// Created after the HTLCs from client to server are constructed.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct LightningSend {
50	/// The Lightning invoice being paid.
51	pub invoice: Invoice,
52	/// The amount being sent.
53	pub amount: Amount,
54	/// The open HTLCs that are used for this payment.
55	pub htlc_vtxos: Vec<WalletVtxo>,
56	/// The movement associated with this payment.
57	pub movement_id: MovementId,
58	/// The payment preimage, serving as proof of payment.
59	///
60	/// Combined with [`finished_at`](Self::finished_at), determines the payment state:
61	/// - `None` + `finished_at: None` → Pending (in-flight)
62	/// - `None` + `finished_at: Some(_)` → Failed
63	/// - `Some(_)` + `finished_at: Some(_)` → Succeeded
64	pub preimage: Option<Preimage>,
65	/// When the payment reached a terminal state (succeeded or failed).
66	pub finished_at: Option<chrono::DateTime<chrono::Local>>,
67}
68
69/// Persisted representation of an incoming Lightning payment.
70///
71/// Stores the invoice and related cryptographic material (e.g., payment hash and preimage)
72/// and tracks whether the preimage has been revealed.
73///
74/// Note: the record should be removed when the receive is completed or failed.
75#[derive(Debug, Clone)]
76pub struct LightningReceive {
77	pub payment_hash: PaymentHash,
78	pub payment_preimage: Preimage,
79	pub invoice: Bolt11Invoice,
80	pub preimage_revealed_at: Option<chrono::DateTime<chrono::Local>>,
81	pub htlc_vtxos: Option<Vec<WalletVtxo>>,
82	pub htlc_recv_cltv_delta: BlockDelta,
83	pub movement_id: Option<MovementId>,
84	pub finished_at: Option<chrono::DateTime<chrono::Local>>,
85}
86
87/// Persistable view of an [ExitVtxo].
88///
89/// `StoredExit` is a lightweight data transfer object tailored for storage backends. It captures
90/// the VTXO ID, the current state, and the full history of the unilateral exit.
91pub struct StoredExit {
92	/// Identifier of the VTXO being exited.
93	pub vtxo_id: VtxoId,
94	/// Current exit state.
95	pub state: ExitState,
96	/// Historical states for auditability.
97	pub history: Vec<ExitState>,
98}
99
100impl StoredExit {
101	/// Builds a persistable snapshot from an [ExitVtxo].
102	pub fn new(exit: &ExitVtxo) -> Self {
103		Self {
104			vtxo_id: exit.id(),
105			state: exit.state().clone(),
106			history: exit.history().clone(),
107		}
108	}
109}
110
111#[derive(Debug, Clone, Deserialize, Serialize)]
112struct SerdeVtxoRequest<'a> {
113	#[serde(with = "bitcoin::amount::serde::as_sat")]
114	amount: Amount,
115	#[serde(with = "ark::encode::serde")]
116	policy: Cow<'a, VtxoPolicy>,
117}
118
119impl<'a> From<&'a VtxoRequest> for SerdeVtxoRequest<'a> {
120	fn from(v: &'a VtxoRequest) -> Self {
121		Self {
122			amount: v.amount,
123			policy: Cow::Borrowed(&v.policy),
124		}
125	}
126}
127
128impl<'a> From<SerdeVtxoRequest<'a>> for VtxoRequest {
129	fn from(v: SerdeVtxoRequest<'a>) -> Self {
130		VtxoRequest {
131			amount: v.amount,
132			policy: v.policy.into_owned(),
133		}
134	}
135}
136
137/// Model for [RoundParticipation]
138#[derive(Debug, Clone, Serialize, Deserialize)]
139struct SerdeRoundParticipation<'a> {
140	#[serde(with = "ark::encode::serde::cow::vec")]
141	inputs: Cow<'a, [Vtxo]>,
142	outputs: Vec<SerdeVtxoRequest<'a>>,
143}
144
145impl<'a> From<&'a RoundParticipation> for SerdeRoundParticipation<'a> {
146	fn from(v: &'a RoundParticipation) -> Self {
147	    Self {
148			inputs: Cow::Borrowed(&v.inputs),
149			outputs: v.outputs.iter().map(|v| v.into()).collect(),
150		}
151	}
152}
153
154impl<'a> From<SerdeRoundParticipation<'a>> for RoundParticipation {
155	fn from(v: SerdeRoundParticipation<'a>) -> Self {
156		Self {
157			inputs: v.inputs.into_owned(),
158			outputs: v.outputs.into_iter().map(|v| v.into()).collect(),
159		}
160	}
161}
162
163/// Model for [AttemptState]
164#[derive(Debug, Serialize, Deserialize)]
165enum SerdeAttemptState<'a> {
166	AwaitingAttempt,
167	AwaitingUnsignedVtxoTree {
168		cosign_keys: Cow<'a, [Keypair]>,
169		secret_nonces: Cow<'a, [Vec<DangerousSecretNonce>]>,
170		unlock_hash: UnlockHash,
171	},
172	AwaitingFinishedRound {
173		#[serde(with = "bitcoin_ext::serde::encodable::cow")]
174		unsigned_round_tx: Cow<'a, Transaction>,
175		#[serde(with = "ark::encode::serde")]
176		vtxos_spec: Cow<'a, VtxoTreeSpec>,
177		unlock_hash: UnlockHash,
178	},
179}
180
181impl<'a> From<&'a AttemptState> for SerdeAttemptState<'a> {
182	fn from(state: &'a AttemptState) -> Self {
183		match state {
184			AttemptState::AwaitingAttempt => SerdeAttemptState::AwaitingAttempt,
185			AttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
186				SerdeAttemptState::AwaitingUnsignedVtxoTree {
187					cosign_keys: Cow::Borrowed(cosign_keys),
188					secret_nonces: Cow::Borrowed(secret_nonces),
189					unlock_hash: *unlock_hash,
190				}
191			},
192			AttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
193				SerdeAttemptState::AwaitingFinishedRound {
194					unsigned_round_tx: Cow::Borrowed(unsigned_round_tx),
195					vtxos_spec: Cow::Borrowed(vtxos_spec),
196					unlock_hash: *unlock_hash,
197				}
198			},
199		}
200	}
201}
202
203impl<'a> From<SerdeAttemptState<'a>> for AttemptState {
204	fn from(state: SerdeAttemptState<'a>) -> Self {
205		match state {
206			SerdeAttemptState::AwaitingAttempt => AttemptState::AwaitingAttempt,
207			SerdeAttemptState::AwaitingUnsignedVtxoTree { cosign_keys, secret_nonces, unlock_hash } => {
208				AttemptState::AwaitingUnsignedVtxoTree {
209					cosign_keys: cosign_keys.into_owned(),
210					secret_nonces: secret_nonces.into_owned(),
211					unlock_hash: unlock_hash,
212				}
213			},
214			SerdeAttemptState::AwaitingFinishedRound { unsigned_round_tx, vtxos_spec, unlock_hash } => {
215				AttemptState::AwaitingFinishedRound {
216					unsigned_round_tx: unsigned_round_tx.into_owned(),
217					vtxos_spec: vtxos_spec.into_owned(),
218					unlock_hash: unlock_hash,
219				}
220			},
221		}
222	}
223}
224
225/// Model for [RoundFlowState]
226#[derive(Debug, Serialize, Deserialize)]
227enum SerdeRoundFlowState<'a> {
228	/// We don't do flow and we just wait for the round to finish
229	NonInteractivePending {
230		unlock_hash: UnlockHash,
231	},
232
233	/// Waiting for round to happen
234	InteractivePending,
235	/// Interactive part ongoing
236	InteractiveOngoing {
237		round_seq: RoundSeq,
238		attempt_seq: usize,
239		state: SerdeAttemptState<'a>,
240	},
241
242	/// Interactive part finished, waiting for confirmation
243	Finished {
244		funding_tx: Cow<'a, Transaction>,
245		unlock_hash: UnlockHash,
246	},
247
248	/// Failed during round
249	Failed {
250		error: Cow<'a, str>,
251	},
252
253	/// User canceled round
254	Canceled,
255}
256
257impl<'a> From<&'a RoundFlowState> for SerdeRoundFlowState<'a> {
258	fn from(state: &'a RoundFlowState) -> Self {
259		match state {
260			RoundFlowState::NonInteractivePending { unlock_hash } => {
261				SerdeRoundFlowState::NonInteractivePending {
262					unlock_hash: *unlock_hash,
263				}
264			},
265			RoundFlowState::InteractivePending => SerdeRoundFlowState::InteractivePending,
266			RoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
267				SerdeRoundFlowState::InteractiveOngoing {
268					round_seq: *round_seq,
269					attempt_seq: *attempt_seq,
270					state: state.into(),
271				}
272			},
273			RoundFlowState::Finished { funding_tx, unlock_hash } => {
274				SerdeRoundFlowState::Finished {
275					funding_tx: Cow::Borrowed(funding_tx),
276					unlock_hash: *unlock_hash,
277				}
278			},
279			RoundFlowState::Failed { error } => {
280				SerdeRoundFlowState::Failed {
281					error: Cow::Borrowed(error),
282				}
283			},
284			RoundFlowState::Canceled => SerdeRoundFlowState::Canceled,
285		}
286	}
287}
288
289impl<'a> From<SerdeRoundFlowState<'a>> for RoundFlowState {
290	fn from(state: SerdeRoundFlowState<'a>) -> Self {
291		match state {
292			SerdeRoundFlowState::NonInteractivePending { unlock_hash } => {
293				RoundFlowState::NonInteractivePending { unlock_hash }
294			},
295			SerdeRoundFlowState::InteractivePending => RoundFlowState::InteractivePending,
296			SerdeRoundFlowState::InteractiveOngoing { round_seq, attempt_seq, state } => {
297				RoundFlowState::InteractiveOngoing {
298					round_seq: round_seq,
299					attempt_seq: attempt_seq,
300					state: state.into(),
301				}
302			},
303			SerdeRoundFlowState::Finished { funding_tx, unlock_hash } => {
304				RoundFlowState::Finished {
305					funding_tx: funding_tx.into_owned(),
306					unlock_hash,
307				}
308			},
309			SerdeRoundFlowState::Failed { error } => {
310				RoundFlowState::Failed {
311					error: error.into_owned(),
312				}
313			},
314			SerdeRoundFlowState::Canceled => RoundFlowState::Canceled,
315		}
316	}
317}
318
319/// Model for [RoundState]
320#[derive(Debug, Serialize, Deserialize)]
321pub struct SerdeRoundState<'a> {
322	done: bool,
323	participation: SerdeRoundParticipation<'a>,
324	movement_id: Option<MovementId>,
325	flow: SerdeRoundFlowState<'a>,
326	#[serde(with = "ark::encode::serde::cow::vec")]
327	new_vtxos: Cow<'a, [Vtxo]>,
328	sent_forfeit_sigs: bool,
329}
330
331impl<'a> From<&'a RoundState> for SerdeRoundState<'a> {
332	fn from(state: &'a RoundState) -> Self {
333		Self {
334			done: state.done,
335			participation: (&state.participation).into(),
336			movement_id: state.movement_id,
337			flow: (&state.flow).into(),
338			new_vtxos: Cow::Borrowed(&state.new_vtxos),
339			sent_forfeit_sigs: state.sent_forfeit_sigs,
340		}
341	}
342}
343
344impl<'a> From<SerdeRoundState<'a>> for RoundState {
345	fn from(state: SerdeRoundState<'a>) -> Self {
346		Self {
347			done: state.done,
348			participation: state.participation.into(),
349			movement_id: state.movement_id,
350			flow: state.flow.into(),
351			new_vtxos: state.new_vtxos.into_owned(),
352			sent_forfeit_sigs: state.sent_forfeit_sigs,
353		}
354	}
355}
356
357#[cfg(test)]
358mod test {
359	use crate::exit::{ExitState, ExitTxOrigin};
360	use crate::vtxo::VtxoState;
361
362	#[test]
363	/// Each struct stored as JSON in the database should have test to check for backwards compatibility
364	/// Parsing can occur either in convert.rs or this file (query.rs)
365	fn test_serialised_structs() {
366		// Exit state
367		let serialised = r#"{"type":"start","tip_height":119}"#;
368		serde_json::from_str::<ExitState>(serialised).unwrap();
369		let serialised = r#"{"type":"processing","tip_height":119,"transactions":[{"txid":"9fd34b8c556dd9954bda80ba2cf3474a372702ebc31a366639483e78417c6812","status":{"type":"awaiting-input-confirmation","txids":["ddfe11920358d1a1fae970dc80459c60675bf1392896f69b103fc638313751de"]}}]}"#;
370		serde_json::from_str::<ExitState>(serialised).unwrap();
371		let serialised = r#"{"type":"awaiting-delta","tip_height":122,"confirmed_block":"122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f","claimable_height":134}"#;
372		serde_json::from_str::<ExitState>(serialised).unwrap();
373		let serialised = r#"{"type":"claimable","tip_height":134,"claimable_since": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9","last_scanned_block":null}"#;
374		serde_json::from_str::<ExitState>(serialised).unwrap();
375		let serialised = r#"{"type":"claim-in-progress","tip_height":134, "claimable_since": "134:6585896bdda6f08d924bf45cc2b16418af56703b3c50930e4dccbc1728d3800a","claim_txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c"}"#;
376		serde_json::from_str::<ExitState>(serialised).unwrap();
377		let serialised = r#"{"type":"claimed","tip_height":134,"txid":"599347c35870bd36f7acb22b81f9ffa8b911d9b5e94834858aebd3ec09339f4c","block": "122:3cdd30fc942301a74666c481beb82050ccd182050aee3c92d2197e8cad427b8f"}"#;
378		serde_json::from_str::<ExitState>(serialised).unwrap();
379
380		// Exit child tx origins
381		let serialized = r#"{"type":"wallet","confirmed_in":null}"#;
382		serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
383		let serialized = r#"{"type":"wallet","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
384		serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
385		let serialized = r#"{"type":"mempool","fee_rate_kwu":25000,"total_fee":27625}"#;
386		serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
387		let serialized = r#"{"type":"block","confirmed_in": "134:71fe28f4c803a4c46a3a93d0a9937507d7c20b4bd9586ba317d1109e1aebaac9"}"#;
388		serde_json::from_str::<ExitTxOrigin>(serialized).unwrap();
389
390		// Vtxo state
391		let serialised = r#"{"type": "spendable"}"#;
392		serde_json::from_str::<VtxoState>(serialised).unwrap();
393		let serialised = r#"{"type": "spent"}"#;
394		serde_json::from_str::<VtxoState>(serialised).unwrap();
395		let serialised = r#"{"type": "locked", "movement_id": null}"#;
396		serde_json::from_str::<VtxoState>(serialised).unwrap();
397	}
398}