bark/persist/
mod.rs

1//! Persistence abstractions for Bark wallets.
2//!
3//! This module defines the [BarkPersister] trait and related data models used by the
4//! wallet to store and retrieve state. Implementors can provide their own storage backends
5//! (e.g., SQLite, PostgreSQL, in-memory, mobile key/value stores) by implementing the
6//! [BarkPersister] trait.
7//!
8//! Design goals
9//! - Clear separation between wallet logic and storage.
10//! - Transactional semantics where appropriate (round state transitions, movement recording).
11//! - Portability across different platforms and environments.
12//!
13//! Typical usage
14//! - Applications construct a concrete persister (for example, a SQLite-backed client) and
15//!   pass it to the [crate::Wallet]. The [crate::Wallet] only depends on this trait for reads/writes.
16//! - Custom wallet implementations can reuse this trait to remain compatible with Bark
17//!   storage expectations without depending on a specific database.
18//! - A default rusqlite implementation is provided by [sqlite::SqliteClient].
19
20pub mod models;
21pub mod sqlite;
22
23
24use std::fmt;
25
26use bitcoin::{Amount, Transaction, Txid};
27use bitcoin::secp256k1::PublicKey;
28use chrono::DateTime;
29use lightning_invoice::Bolt11Invoice;
30#[cfg(feature = "onchain_bdk")]
31use bdk_wallet::ChangeSet;
32
33use ark::{Vtxo, VtxoId};
34use ark::lightning::{Invoice, PaymentHash, Preimage};
35use bitcoin_ext::BlockDelta;
36
37use crate::WalletProperties;
38use crate::exit::ExitTxOrigin;
39use crate::movement::{Movement, MovementId, MovementStatus, MovementSubsystem, PaymentMethod};
40use crate::persist::models::{LightningReceive, LightningSend, PendingBoard, StoredExit};
41use crate::round::RoundState;
42use crate::vtxo::{VtxoState, VtxoStateKind, WalletVtxo};
43
44/// Identifier for a stored [RoundState].
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct RoundStateId(pub u32);
47
48impl fmt::Display for RoundStateId {
49	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50	    fmt::Display::fmt(&self.0, f)
51	}
52}
53
54pub struct StoredRoundState {
55	pub id: RoundStateId,
56	pub state: RoundState,
57}
58
59/// Storage interface for Bark wallets.
60///
61/// Implement this trait to plug a custom persistence backend. The wallet uses it to:
62/// - Initialize and read wallet properties and configuration.
63/// - Record movements (spends/receives), recipients, and enforce [Vtxo] state transitions.
64/// - Manage round lifecycles (attempts, pending confirmation, confirmations/cancellations).
65/// - Persist ephemeral protocol artifacts (e.g., secret nonces) transactionally.
66/// - Track Lightning receives and preimage revelation.
67/// - Track exit-related data and associated child transactions.
68/// - Persist the last synchronized Ark block height.
69///
70/// Feature integration:
71/// - With the `onchain_bdk` feature, methods are provided to initialize and persist a BDK
72///   wallet ChangeSet in the same storage.
73///
74/// Notes for implementors:
75/// - Ensure that operations that change multiple records (e.g., registering a movement,
76///   storing round state transitions) are executed transactionally.
77/// - Enforce state integrity by verifying allowed_old_states before updating a [Vtxo] state.
78/// - If your backend is not thread-safe, prefer a short-lived connection per call or use
79///   an internal pool with checked-out connections per operation.
80/// - Return precise errors so callers can surface actionable diagnostics.
81#[async_trait]
82pub trait BarkPersister: Send + Sync + 'static {
83	/// Initialize a wallet in storage with the provided properties.
84	///
85	/// Call exactly once per wallet database. Subsequent calls should fail to prevent
86	/// accidental re-initialization.
87	///
88	/// Parameters:
89	/// - properties: WalletProperties to persist (e.g., network, descriptors, metadata).
90	///
91	/// Returns:
92	/// - `Ok(())` on success.
93	///
94	/// Errors:
95	/// - Returns an error if the wallet is already initialized or if persistence fails.
96	async fn init_wallet(&self, properties: &WalletProperties) -> anyhow::Result<()>;
97
98	/// Initialize the onchain BDK wallet and return any previously stored ChangeSet.
99	///
100	/// Must be called before storing any new BDK changesets to bootstrap the BDK state.
101	///
102	/// Feature: only available with `onchain_bdk`.
103	///
104	/// Returns:
105	/// - `Ok(ChangeSet)` containing the previously persisted BDK state (possibly empty).
106	///
107	/// Errors:
108	/// - Returns an error if the BDK state cannot be created or loaded.
109	#[cfg(feature = "onchain_bdk")]
110	async fn initialize_bdk_wallet(&self) -> anyhow::Result<ChangeSet>;
111
112	/// Persist an incremental BDK ChangeSet.
113	///
114	/// The changeset should be applied atomically. Callers typically obtain the changeset
115	/// from a BDK wallet instance after mutating wallet state (e.g., sync).
116	///
117	/// Feature: only available with `onchain_bdk`.
118	///
119	/// Parameters:
120	/// - changeset: The BDK ChangeSet to persist.
121	///
122	/// Errors:
123	/// - Returns an error if the changeset cannot be written.
124	#[cfg(feature = "onchain_bdk")]
125	async fn store_bdk_wallet_changeset(&self, changeset: &ChangeSet) -> anyhow::Result<()>;
126
127	/// Read wallet properties from storage.
128	///
129	/// Returns:
130	/// - `Ok(Some(WalletProperties))` if the wallet has been initialized.
131	/// - `Ok(None)` if no wallet exists yet.
132	///
133	/// Errors:
134	/// - Returns an error on I/O or deserialization failures.
135	async fn read_properties(&self) -> anyhow::Result<Option<WalletProperties>>;
136
137	/// Check whether a recipient identifier already exists.
138	///
139	/// Useful to avoid storing duplicate recipients for the same logical payee or duplicated
140	/// lightning invoice payments (unsafe)
141	///
142	/// Parameters:
143	/// - recipient: A recipient identifier (e.g., invoice).
144	///
145	/// Returns:
146	/// - `Ok(true)` if the recipient exists,
147	/// - `Ok(false)` otherwise.
148	///
149	/// Errors:
150	/// - Returns an error if the lookup fails.
151	async fn check_recipient_exists(&self, recipient: &PaymentMethod) -> anyhow::Result<bool>;
152
153	/// Creates a new movement in the given state, ready to be updated.
154	///
155	/// Parameters:
156	/// - status: The desired status for the new movement.
157	/// - subsystem: The subsystem that created the movement.
158	/// - time: The time the movement should be marked as created.
159	///
160	/// Returns:
161	/// - `Ok(MovementId)` of the newly created movement.
162	///
163	/// Errors:
164	/// - Returns an error if the movement is unable to be created.
165	async fn create_new_movement(&self,
166		status: MovementStatus,
167		subsystem: &MovementSubsystem,
168		time: DateTime<chrono::Local>,
169	) -> anyhow::Result<MovementId>;
170
171	/// Persists the given movement state.
172	///
173	/// Parameters:
174	/// - movement: The movement and its associated data to be persisted.
175	///
176	/// Errors:
177	/// - Returns an error if updating the movement fails for any reason.
178	async fn update_movement(&self, movement: &Movement) -> anyhow::Result<()>;
179
180	/// Gets the movement with the given [MovementId].
181	///
182	/// Parameters:
183	/// - movement_id: The ID of the movement to retrieve.
184	///
185	/// Returns:
186	/// - `Ok(Movement)` if the movement exists.
187	///
188	/// Errors:
189	/// - If the movement does not exist.
190	/// - If retrieving the movement fails.
191	async fn get_movement_by_id(&self, movement_id: MovementId) -> anyhow::Result<Movement>;
192
193	/// Gets every stored movement.
194	///
195	/// Returns:
196	/// - `Ok(Vec<Movement>)` containing all movements, empty if none exist.
197	///
198	/// Errors:
199	/// - If retrieving the movements fails.
200	async fn get_all_movements(&self) -> anyhow::Result<Vec<Movement>>;
201
202	/// Store a pending board.
203	///
204	/// Parameters:
205	/// - vtxo: The [Vtxo] to store.
206	/// - funding_txid: The funding [Txid].
207	/// - movement_id: The [MovementId] associated with this board.
208	///
209	/// Errors:
210	/// - Returns an error if the board cannot be stored.
211	async fn store_pending_board(
212		&self,
213		vtxo: &Vtxo,
214		funding_tx: &Transaction,
215		movement_id: MovementId,
216	) -> anyhow::Result<()>;
217
218	/// Remove a pending board.
219	///
220	/// Parameters:
221	/// - vtxo_id: The [VtxoId] to remove.
222	///
223	/// Errors:
224	/// - Returns an error if the board cannot be removed.
225	async fn remove_pending_board(&self, vtxo_id: &VtxoId) -> anyhow::Result<()>;
226
227	/// Get the [VtxoId] for each pending board.
228	///
229	/// Returns:
230	/// - `Ok(Vec<VtxoId>)` possibly empty.
231	///
232	/// Errors:
233	/// - Returns an error if the query fails.
234	async fn get_all_pending_board_ids(&self) -> anyhow::Result<Vec<VtxoId>>;
235
236	/// Get the [PendingBoard] associated with the given [VtxoId].
237	///
238	/// Returns:
239	/// - `Ok(Some(PendingBoard))` if a matching board exists
240	/// - `Ok(None)` if no matching board exists
241	///
242	/// Errors:
243	/// - Returns an error if the query fails.
244	async fn get_pending_board_by_vtxo_id(&self, vtxo_id: VtxoId) -> anyhow::Result<Option<PendingBoard>>;
245
246	/// Store a new ongoing round state and lock the VTXOs in round
247	///
248	/// Parameters:
249	/// - `round_state`: the state to store
250	///
251	/// Returns:
252	/// - `RoundStateId`: the storaged ID of the new state
253	///
254	/// Errors:
255	/// - returns an error of the new round state could not be stored or the VTXOs
256	///   couldn't be marked as locked
257	async fn store_round_state_lock_vtxos(&self, round_state: &RoundState) -> anyhow::Result<RoundStateId>;
258
259	/// Update an existing stored pending round state
260	///
261	/// Parameters:
262	/// - `round_state`: the round state to update
263	///
264	/// Errors:
265	/// - returns an error of the existing round state could not be found or updated
266	async fn update_round_state(&self, round_state: &StoredRoundState) -> anyhow::Result<()>;
267
268	/// Remove a pending round state from the db
269	///
270	/// Parameters:
271	/// - `round_state`: the round state to remove
272	///
273	/// Errors:
274	/// - returns an error of the existing round state could not be found or removed
275	async fn remove_round_state(&self, round_state: &StoredRoundState) -> anyhow::Result<()>;
276
277	/// Load all pending round states from the db
278	///
279	/// Returns:
280	/// - `Vec<StoredRoundState>`: unordered vector with all stored round states
281	///
282	/// Errors:
283	/// - returns an error of the states could not be succesfully retrieved
284	async fn load_round_states(&self) -> anyhow::Result<Vec<StoredRoundState>>;
285
286	/// Stores VTXOs with their initial state.
287	///
288	/// This operation is idempotent: if a VTXO already exists (same `id`), the
289	/// implementation should succeed without modifying the existing VTXO or its
290	/// state. This allows safe retries during crash recovery scenarios.
291	///
292	/// # Parameters
293	/// - `vtxos`: Slice of VTXO and state pairs to store.
294	///
295	/// # Behavior
296	/// - For each VTXO that does not exist: inserts the VTXO and its initial state.
297	/// - For each VTXO that already exists: no-op for that VTXO.
298	///
299	/// # Errors
300	/// - Returns an error if the storage operation fails.
301	async fn store_vtxos(
302		&self,
303		vtxos: &[(&Vtxo, &VtxoState)],
304	) -> anyhow::Result<()>;
305
306	/// Fetch a wallet [Vtxo] with its current state by ID.
307	///
308	/// Parameters:
309	/// - id: [VtxoId] to look up.
310	///
311	/// Returns:
312	/// - `Ok(Some(WalletVtxo))` if found,
313	/// - `Ok(None)` otherwise.
314	///
315	/// Errors:
316	/// - Returns an error if the lookup fails.
317	async fn get_wallet_vtxo(&self, id: VtxoId) -> anyhow::Result<Option<WalletVtxo>>;
318
319	/// Fetch all wallet VTXOs in the database.
320	///
321	/// Returns:
322	/// - `Ok(Vec<WalletVtxo>)` possibly empty.
323	///
324	/// Errors:
325	/// - Returns an error if the query fails.
326	async fn get_all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>>;
327
328	/// Fetch all wallet VTXOs whose state matches any of the provided kinds.
329	///
330	/// Parameters:
331	/// - state: Slice of `VtxoStateKind` filters.
332	///
333	/// Returns:
334	/// - `Ok(Vec<WalletVtxo>)` possibly empty.
335	///
336	/// Errors:
337	/// - Returns an error if the query fails.
338	async fn get_vtxos_by_state(&self, state: &[VtxoStateKind]) -> anyhow::Result<Vec<WalletVtxo>>;
339
340	/// Remove a [Vtxo] by ID.
341	///
342	/// Parameters:
343	/// - id: `VtxoId` to remove.
344	///
345	/// Returns:
346	/// - `Ok(Some(Vtxo))` with the removed [Vtxo] data if it existed,
347	/// - `Ok(None)` otherwise.
348	///
349	/// Errors:
350	/// - Returns an error if the delete operation fails.
351	async fn remove_vtxo(&self, id: VtxoId) -> anyhow::Result<Option<Vtxo>>;
352
353	/// Check whether a [Vtxo] is already marked spent.
354	///
355	/// Parameters:
356	/// - id: VtxoId to check.
357	///
358	/// Returns:
359	/// - `Ok(true)` if spent,
360	/// - `Ok(false)` if not found or not spent.
361	///
362	/// Errors:
363	/// - Returns an error if the lookup fails.
364	async fn has_spent_vtxo(&self, id: VtxoId) -> anyhow::Result<bool>;
365
366	/// Store a newly derived/assigned [Vtxo] public key index mapping.
367	///
368	/// Parameters:
369	/// - index: Derivation index.
370	/// - public_key: PublicKey at that index.
371	///
372	/// Errors:
373	/// - Returns an error if the mapping cannot be stored.
374	async fn store_vtxo_key(&self, index: u32, public_key: PublicKey) -> anyhow::Result<()>;
375
376	/// Get the last revealed/used [Vtxo] key index.
377	///
378	/// Returns:
379	/// - `Ok(Some(u32))` if a key was stored
380	/// - `Ok(None)` otherwise.
381	///
382	/// Errors:
383	/// - Returns an error if the query fails.
384	async fn get_last_vtxo_key_index(&self) -> anyhow::Result<Option<u32>>;
385
386	/// Retrieves the derivation index of the provided [PublicKey] from the database
387	///
388	/// Returns:
389	/// - `Ok(Some(u32))` if the key was stored.
390	/// - `Ok(None)` if the key was not stored.
391	///
392	/// Errors:
393	/// - Returns an error if the query fails.
394	async fn get_public_key_idx(&self, public_key: &PublicKey) -> anyhow::Result<Option<u32>>;
395
396	/// Store a new pending lightning send.
397	///
398	/// Parameters:
399	/// - invoice: The invoice of the pending lightning send.
400	/// - amount: The amount of the pending lightning send.
401	/// - vtxos: The vtxos of the pending lightning send.
402	///
403	/// Errors:
404	/// - Returns an error if the pending lightning send cannot be stored.
405	async fn store_new_pending_lightning_send(
406		&self,
407		invoice: &Invoice,
408		amount: &Amount,
409		vtxos: &[VtxoId],
410		movement_id: MovementId,
411	) -> anyhow::Result<LightningSend>;
412
413	/// Get all pending lightning sends.
414	///
415	/// Returns:
416	/// - `Ok(Vec<LightningSend>)` possibly empty.
417	///
418	/// Errors:
419	/// - Returns an error if the query fails.
420	async fn get_all_pending_lightning_send(&self) -> anyhow::Result<Vec<LightningSend>>;
421
422	/// Mark a lightning send as finished.
423	///
424	/// Parameters:
425	/// - payment_hash: The [PaymentHash] of the lightning send to update.
426	/// - preimage: The [Preimage] of the successful lightning send.
427	///
428	/// Errors:
429	/// - Returns an error if the lightning send cannot be updated.
430	async fn finish_lightning_send(
431		&self,
432		payment_hash: PaymentHash,
433		preimage: Option<Preimage>,
434	) -> anyhow::Result<()>;
435
436	/// Remove a lightning send.
437	///
438	/// Parameters:
439	/// - payment_hash: The [PaymentHash] of the lightning send to remove.
440	///
441	/// Errors:
442	/// - Returns an error if the lightning send cannot be removed.
443	async fn remove_lightning_send(&self, payment_hash: PaymentHash) -> anyhow::Result<()>;
444
445	/// Get a lightning send by payment hash
446	///
447	/// Parameters:
448	/// - payment_hash: The [PaymentHash] of the lightning send to get.
449	///
450	/// Errors:
451	/// - Returns an error if the lookup fails.
452	async fn get_lightning_send(&self, payment_hash: PaymentHash) -> anyhow::Result<Option<LightningSend>>;
453
454	/// Store an incoming Lightning receive record.
455	///
456	/// Parameters:
457	/// - payment_hash: Unique payment hash.
458	/// - preimage: Payment preimage (kept until disclosure).
459	/// - invoice: The associated BOLT11 invoice.
460	/// - htlc_recv_cltv_delta: The CLTV delta for the HTLC VTXO.
461	///
462	/// Errors:
463	/// - Returns an error if the receive cannot be stored.
464	async fn store_lightning_receive(
465		&self,
466		payment_hash: PaymentHash,
467		preimage: Preimage,
468		invoice: &Bolt11Invoice,
469		htlc_recv_cltv_delta: BlockDelta,
470	) -> anyhow::Result<()>;
471
472	/// Returns a list of all pending lightning receives
473	///
474	/// Returns:
475	/// - `Ok(Vec<LightningReceive>)` possibly empty.
476	///
477	/// Errors:
478	/// - Returns an error if the query fails.
479	async fn get_all_pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>>;
480
481	/// Mark a Lightning receive preimage as revealed (e.g., after settlement).
482	///
483	/// Parameters:
484	/// - payment_hash: The payment hash identifying the receive.
485	///
486	/// Errors:
487	/// - Returns an error if the update fails or the receive does not exist.
488	async fn set_preimage_revealed(&self, payment_hash: PaymentHash) -> anyhow::Result<()>;
489
490	/// Set the VTXO IDs and [MovementId] for a [LightningReceive].
491	///
492	/// Parameters:
493	/// - payment_hash: The payment hash identifying the receive.
494	/// - htlc_vtxo_ids: The VTXO IDs to set.
495	/// - movement_id: The movement ID associated with the invoice.
496	///
497	/// Errors:
498	/// - Returns an error if the update fails or the receive does not exist.
499	async fn update_lightning_receive(
500		&self,
501		payment_hash: PaymentHash,
502		htlc_vtxo_ids: &[VtxoId],
503		movement_id: MovementId,
504	) -> anyhow::Result<()>;
505
506	/// Fetch a Lightning receive by its payment hash.
507	///
508	/// Parameters:
509	/// - payment_hash: The payment hash to look up.
510	///
511	/// Returns:
512	/// - `Ok(Some(LightningReceive))` if found,
513	/// - `Ok(None)` otherwise.
514	///
515	/// Errors:
516	/// - Returns an error if the lookup fails.
517	async fn fetch_lightning_receive_by_payment_hash(
518		&self,
519		payment_hash: PaymentHash,
520	) -> anyhow::Result<Option<LightningReceive>>;
521
522	/// Remove a Lightning receive by its payment hash.
523	///
524	/// Parameters:
525	/// - payment_hash: The payment hash of the record to remove.
526	///
527	/// Errors:
528	/// - Returns an error if the removal fails.
529	async fn finish_pending_lightning_receive(
530		&self,
531		payment_hash: PaymentHash,
532	) -> anyhow::Result<()>;
533
534	/// Store an entry indicating a [Vtxo] is being exited.
535	///
536	/// Parameters:
537	/// - exit: StoredExit describing the exit operation.
538	///
539	/// Errors:
540	/// - Returns an error if the entry cannot be stored.
541	async fn store_exit_vtxo_entry(&self, exit: &StoredExit) -> anyhow::Result<()>;
542
543	/// Remove an exit entry for a given [Vtxo] ID.
544	///
545	/// Parameters:
546	/// - id: VtxoId to remove from exit tracking.
547	///
548	/// Errors:
549	/// - Returns an error if the removal fails.
550	async fn remove_exit_vtxo_entry(&self, id: &VtxoId) -> anyhow::Result<()>;
551
552	/// List all VTXOs currently tracked as being exited.
553	///
554	/// Returns:
555	/// - `Ok(Vec<StoredExit>)` possibly empty.
556	///
557	/// Errors:
558	/// - Returns an error if the query fails.
559	async fn get_exit_vtxo_entries(&self) -> anyhow::Result<Vec<StoredExit>>;
560
561	/// Store a child transaction related to an exit transaction.
562	///
563	/// Parameters:
564	/// - exit_txid: The parent exit transaction ID.
565	/// - child_tx: The child bitcoin Transaction to store.
566	/// - origin: Metadata describing where the child came from (ExitTxOrigin).
567	///
568	/// Errors:
569	/// - Returns an error if the transaction cannot be stored.
570	async fn store_exit_child_tx(
571		&self,
572		exit_txid: Txid,
573		child_tx: &Transaction,
574		origin: ExitTxOrigin,
575	) -> anyhow::Result<()>;
576
577	/// Retrieve a stored child transaction for a given exit transaction ID.
578	///
579	/// Parameters:
580	/// - exit_txid: The parent exit transaction ID.
581	///
582	/// Returns:
583	/// - `Ok(Some((Transaction, ExitTxOrigin)))` if found,
584	/// - `Ok(None)` otherwise.
585	///
586	/// Errors:
587	/// - Returns an error if the lookup fails.
588	async fn get_exit_child_tx(
589		&self,
590		exit_txid: Txid,
591	) -> anyhow::Result<Option<(Transaction, ExitTxOrigin)>>;
592
593	/// Updates the state of the VTXO corresponding to the given [VtxoId], provided that their
594	/// current state is one of the given `allowed_states`.
595	///
596	/// # Parameters
597	/// - `vtxo_id`: The ID of the [Vtxo] to update.
598	/// - `state`: The new state to be set for the specified [Vtxo].
599	/// - `allowed_states`: An iterable collection of allowed states ([VtxoStateKind]) that the
600	///   [Vtxo] must currently be in for their state to be updated to the new `state`.
601	///
602	/// # Returns
603	/// - `Ok(WalletVtxo)` if the state update is successful.
604	/// - `Err(anyhow::Error)` if the VTXO fails to meet the required conditions,
605	///    or if another error occurs during the operation.
606	///
607	/// # Errors
608	/// - Returns an error if the current state is not within the `allowed_states`.
609	/// - Returns an error for any other issues encountered during the operation.
610	async fn update_vtxo_state_checked(
611		&self,
612		vtxo_id: VtxoId,
613		new_state: VtxoState,
614		allowed_old_states: &[VtxoStateKind],
615	) -> anyhow::Result<WalletVtxo>;
616}