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