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