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}