Skip to main content

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