1mod convert;
12mod migrations;
13mod query;
14
15
16use std::path::{Path, PathBuf};
17
18use anyhow::Context;
19use bitcoin::{Amount, Txid};
20use bitcoin::secp256k1::PublicKey;
21use chrono::DateTime;
22use lightning_invoice::Bolt11Invoice;
23use log::debug;
24use rusqlite::Connection;
25
26use ark::{Vtxo, VtxoId};
27use ark::lightning::{PaymentHash, Preimage};
28use ark::vtxo::Full;
29
30use crate::WalletProperties;
31use crate::actions::{WalletActionCheckpoint, WalletActionId};
32use crate::exit::{ExitStateKind, ExitTxOrigin};
33use crate::movement::{Movement, MovementId, MovementStatus, MovementSubsystem, PaymentMethod};
34use crate::movement::update::MovementUpdate;
35use crate::persist::{BarkPersister, RoundStateId, StoredRoundState, Unlocked};
36use crate::persist::models::{PaidInvoice, SettledLightningReceive, StoredExit};
37use crate::round::RoundState;
38use crate::vtxo::{VtxoState, VtxoStateKind, WalletVtxo};
39
40
41pub const DEFAULT_DB_FILE: &str = "db.sqlite";
43
44#[derive(Debug, Clone)]
47pub struct SqliteClient {
48 connection_string: PathBuf,
49}
50
51impl SqliteClient {
52 pub fn open(db_file: impl AsRef<Path>) -> anyhow::Result<SqliteClient> {
54 let path = db_file.as_ref().to_path_buf();
55
56 debug!("Opening database at {}", path.display());
57 let mut conn = rusqlite::Connection::open(&path)
58 .with_context(|| format!("Error connecting to database {}", path.display()))?;
59
60 crate::fs_perms::warn_if_loose(&path, 0o600);
64
65 let migrations = migrations::MigrationContext::new();
66 migrations.do_all_migrations(&mut conn)?;
67
68 Ok( Self { connection_string: path })
69 }
70
71 fn connect(&self) -> anyhow::Result<Connection> {
72 rusqlite::Connection::open(&self.connection_string)
73 .with_context(|| format!("Error connecting to database {}", self.connection_string.display()))
74 }
75}
76
77#[async_trait]
78impl BarkPersister for SqliteClient {
79 async fn init_wallet(&self, properties: &WalletProperties) -> anyhow::Result<()> {
80 let mut conn = self.connect()?;
81 let tx = conn.transaction()?;
82
83 query::set_properties(&tx, properties)?;
84
85 tx.commit()?;
86 Ok(())
87 }
88
89 #[cfg(feature = "onchain-bdk")]
90 async fn initialize_bdk_wallet(&self) -> anyhow::Result<bdk_wallet::ChangeSet> {
91 let mut conn = self.connect()?;
92 Ok(bdk_wallet::WalletPersister::initialize(&mut conn)?)
93 }
94
95 #[cfg(feature = "onchain-bdk")]
96 async fn store_bdk_wallet_changeset(&self, changeset: &bdk_wallet::ChangeSet) -> anyhow::Result<()> {
97 let mut conn = self.connect()?;
98 bdk_wallet::WalletPersister::persist(&mut conn, changeset)?;
99 Ok(())
100 }
101
102 async fn read_properties(&self) -> anyhow::Result<Option<WalletProperties>> {
103 let conn = self.connect()?;
104 Ok(query::fetch_properties(&conn)?)
105 }
106
107 async fn set_server_pubkey(&self, server_pubkey: PublicKey) -> anyhow::Result<()> {
108 let conn = self.connect()?;
109 query::set_server_pubkey(&conn, &server_pubkey)?;
110 Ok(())
111 }
112
113 async fn set_server_mailbox_pubkey(&self, server_mailbox_pubkey: PublicKey) -> anyhow::Result<()> {
114 let conn = self.connect()?;
115 query::set_server_mailbox_pubkey(&conn, &server_mailbox_pubkey)?;
116 Ok(())
117 }
118
119 async fn create_new_movement(&self,
120 status: MovementStatus,
121 subsystem: &MovementSubsystem,
122 time: DateTime<chrono::Local>,
123 action_id: Option<&str>,
124 ) -> anyhow::Result<MovementId> {
125 let mut conn = self.connect()?;
126 let tx = conn.transaction()?;
127 let movement_id = query::create_new_movement(&tx, status, subsystem, time, action_id)?;
128 tx.commit()?;
129 Ok(movement_id)
130 }
131
132 async fn get_or_create_movement_for_action(
133 &self,
134 subsystem: &MovementSubsystem,
135 time: DateTime<chrono::Local>,
136 action_id: &str,
137 update: MovementUpdate,
138 ) -> anyhow::Result<(MovementId, bool)> {
139 let mut conn = self.connect()?;
140 let tx = conn.transaction()?;
141 let result = match query::get_movement_id_by_action(&tx, action_id)? {
142 Some(id) => (id, false),
143 None => {
144 let id = query::create_new_movement(
145 &tx, MovementStatus::Pending, subsystem, time, Some(action_id))?;
146 let mut movement = query::get_movement_by_id(&tx, id)?;
147 update.apply_to(&mut movement, time);
148 query::update_movement(&tx, &movement)?;
149 (id, true)
150 },
151 };
152 tx.commit()?;
153 Ok(result)
154 }
155
156 async fn update_movement(&self, movement: &Movement) -> anyhow::Result<()> {
157 let mut conn = self.connect()?;
158 let tx = conn.transaction()?;
159 query::update_movement(&tx, movement)?;
160 tx.commit()?;
161 Ok(())
162 }
163
164 async fn get_movement_by_id(&self, movement_id: MovementId) -> anyhow::Result<Movement> {
165 let conn = self.connect()?;
166 query::get_movement_by_id(&conn, movement_id)
167 }
168
169 async fn get_all_movements(&self) -> anyhow::Result<Vec<Movement>> {
170 let conn = self.connect()?;
171 query::get_all_movements(&conn)
172 }
173
174 async fn get_movements_by_payment_method(
175 &self,
176 payment_method: &PaymentMethod,
177 ) -> anyhow::Result<Vec<Movement>> {
178 let conn = self.connect()?;
179 query::get_movements_by_payment_method(&conn, payment_method)
180 }
181
182 async fn store_round_state(&self, round_state: &RoundState) -> anyhow::Result<RoundStateId> {
183 let conn = self.connect()?;
184 query::store_round_state(&conn, round_state)
185 }
186
187 async fn update_round_state(&self, state: &StoredRoundState) -> anyhow::Result<()> {
188 let conn = self.connect()?;
189 query::update_round_state(&conn, state)?;
190 Ok(())
191 }
192
193 async fn remove_round_state(&self, round_state: &StoredRoundState) -> anyhow::Result<()> {
194 let conn = self.connect()?;
195 query::remove_round_state(&conn, round_state.id())?;
196 Ok(())
197 }
198
199 async fn get_round_state_by_id(&self, id: RoundStateId) -> anyhow::Result<Option<StoredRoundState<Unlocked>>> {
200 let conn = self.connect()?;
201 query::get_round_state_by_id(&conn, id)
202 }
203
204 async fn get_pending_round_state_ids(&self) -> anyhow::Result<Vec<RoundStateId>> {
205 let conn = self.connect()?;
206 query::get_pending_round_state_ids(&conn)
207 }
208
209 async fn store_vtxos(
210 &self,
211 vtxos: &[(&Vtxo<Full>, &VtxoState)],
212 ) -> anyhow::Result<()> {
213 let mut conn = self.connect()?;
214 let tx = conn.transaction()?;
215
216 for (vtxo, state) in vtxos {
217 query::store_vtxo_with_initial_state(&tx, vtxo, state)?;
218 }
219 tx.commit()?;
220 Ok(())
221 }
222
223 async fn get_wallet_vtxo(&self, id: VtxoId) -> anyhow::Result<Option<WalletVtxo>> {
224 let conn = self.connect()?;
225 query::get_wallet_vtxo_by_id(&conn, id)
226 }
227
228 async fn get_wallet_vtxos(&self, ids: &[VtxoId]) -> anyhow::Result<Vec<WalletVtxo>> {
229 let conn = self.connect()?;
230 query::get_wallet_vtxos_by_ids(&conn, ids)
231 }
232
233 async fn get_all_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
234 let conn = self.connect()?;
235 query::get_all_vtxos(&conn)
236 }
237
238 async fn get_vtxos_by_state(&self, state: &[VtxoStateKind]) -> anyhow::Result<Vec<WalletVtxo>> {
240 let conn = self.connect()?;
241 query::get_vtxos_by_state(&conn, state)
242 }
243
244 async fn has_spent_vtxo(&self, id: VtxoId) -> anyhow::Result<bool> {
245 let conn = self.connect()?;
246 let state : Option<VtxoState> = query::get_vtxo_state(&conn, id)?;
247 let result = state.map(|s| s == VtxoState::Spent).unwrap_or(false);
248 Ok(result)
249 }
250
251 async fn get_full_vtxo(&self, id: VtxoId) -> anyhow::Result<Option<Vtxo<Full>>> {
252 let conn = self.connect()?;
253 query::get_full_vtxo_by_id(&conn, id)
254 }
255
256 async fn get_full_vtxos(&self, ids: &[VtxoId]) -> anyhow::Result<Vec<Vtxo<Full>>> {
257 let conn = self.connect()?;
258 query::get_full_vtxos_by_ids(&conn, ids)
259 }
260
261 async fn remove_vtxo(&self, id: VtxoId) -> anyhow::Result<Option<Vtxo<Full>>> {
262 let mut conn = self.connect()?;
263 let tx = conn.transaction().context("Failed to start transaction")?;
264 let result = query::delete_vtxo(&tx, id);
265 tx.commit().context("Failed to commit transaction")?;
266 result
267 }
268
269 async fn store_vtxo_key(&self, index: u32, public_key: PublicKey) -> anyhow::Result<()> {
270 let conn = self.connect()?;
271 query::store_vtxo_key(&conn, index, public_key)
272 }
273
274 async fn get_last_vtxo_key_index(&self) -> anyhow::Result<Option<u32>> {
275 let conn = self.connect()?;
276 query::get_last_vtxo_key_index(&conn)
277 }
278
279 async fn get_public_key_idx(&self, public_key: &PublicKey) -> anyhow::Result<Option<u32>> {
280 let conn = self.connect()?;
281 query::get_public_key_idx(&conn, public_key)
282 }
283
284 async fn get_mailbox_checkpoint(&self) -> anyhow::Result<u64> {
285 let conn = self.connect()?;
286 query::get_mailbox_checkpoint(&conn)
287 }
288
289 async fn store_mailbox_checkpoint(&self, checkpoint: u64) -> anyhow::Result<()> {
290 let conn = self.connect()?;
291 query::store_mailbox_checkpoint(&conn, checkpoint)?;
292 Ok(())
293 }
294
295 async fn upsert_wallet_action_checkpoint(
296 &self,
297 id: &WalletActionId,
298 checkpoint: &WalletActionCheckpoint,
299 ) -> anyhow::Result<()> {
300 let conn = self.connect()?;
301 query::upsert_wallet_action_checkpoint(&conn, id, checkpoint)
302 }
303
304 async fn get_wallet_action_checkpoint(
305 &self,
306 id: &WalletActionId,
307 ) -> anyhow::Result<Option<WalletActionCheckpoint>> {
308 let conn = self.connect()?;
309 query::get_wallet_action_checkpoint(&conn, id)
310 }
311
312 async fn get_all_wallet_action_checkpoints(
313 &self,
314 ) -> anyhow::Result<Vec<WalletActionCheckpoint>> {
315 let conn = self.connect()?;
316 query::get_all_wallet_action_checkpoints(&conn)
317 }
318
319 async fn remove_wallet_action_checkpoint(
320 &self,
321 id: &WalletActionId,
322 ) -> anyhow::Result<()> {
323 let conn = self.connect()?;
324 query::remove_wallet_action_checkpoint(&conn, id)
325 }
326
327 async fn record_paid_invoice(
328 &self,
329 payment_hash: PaymentHash,
330 preimage: Preimage,
331 ) -> anyhow::Result<()> {
332 let conn = self.connect()?;
333 query::record_paid_invoice(&conn, payment_hash, preimage)
334 }
335
336 async fn get_paid_invoice(
337 &self,
338 payment_hash: PaymentHash,
339 ) -> anyhow::Result<Option<PaidInvoice>> {
340 let conn = self.connect()?;
341 query::get_paid_invoice(&conn, payment_hash)
342 }
343
344 async fn record_settled_lightning_receive(
345 &self,
346 payment_hash: PaymentHash,
347 preimage: Preimage,
348 invoice: &Bolt11Invoice,
349 amount: Amount,
350 ) -> anyhow::Result<()> {
351 let conn = self.connect()?;
352 query::record_settled_lightning_receive(&conn, payment_hash, preimage, invoice, amount)
353 }
354
355 async fn get_settled_lightning_receive(
356 &self,
357 payment_hash: PaymentHash,
358 ) -> anyhow::Result<Option<SettledLightningReceive>> {
359 let conn = self.connect()?;
360 query::get_settled_lightning_receive(&conn, payment_hash)
361 }
362
363 async fn store_exit_vtxo_entry(&self, exit: &StoredExit) -> anyhow::Result<()> {
364 let mut conn = self.connect()?;
365 let tx = conn.transaction()?;
366 query::store_exit_vtxo_entry(&tx, exit)?;
367 tx.commit()?;
368 Ok(())
369 }
370
371 async fn remove_exit_vtxo_entry(&self, id: &VtxoId) -> anyhow::Result<()> {
372 let mut conn = self.connect()?;
373 let tx = conn.transaction()?;
374 query::remove_exit_vtxo_entry(&tx, &id)?;
375 tx.commit()?;
376 Ok(())
377 }
378
379 async fn get_exit_vtxo_entries(&self) -> anyhow::Result<Vec<StoredExit>> {
380 let conn = self.connect()?;
381 query::get_exit_vtxo_entries(&conn)
382 }
383
384 async fn get_exit_vtxo_entries_with_states(
385 &self,
386 states: &[ExitStateKind],
387 ) -> anyhow::Result<Vec<StoredExit>> {
388 let conn = self.connect()?;
389 query::get_exit_vtxo_entries_with_states(&conn, states)
390 }
391
392 async fn get_exit_vtxo_entry(&self, id: &VtxoId) -> anyhow::Result<Option<StoredExit>> {
393 let conn = self.connect()?;
394 query::get_exit_vtxo_entry(&conn, id)
395 }
396
397 async fn store_exit_child_tx(
398 &self,
399 exit_txid: Txid,
400 child_tx: &bitcoin::Transaction,
401 origin: ExitTxOrigin,
402 ) -> anyhow::Result<()> {
403 let mut conn = self.connect()?;
404 let tx = conn.transaction()?;
405 query::store_exit_child_tx(&tx, exit_txid, child_tx, origin)?;
406 tx.commit()?;
407 Ok(())
408 }
409
410 async fn get_exit_child_tx(
411 &self,
412 exit_txid: Txid,
413 ) -> anyhow::Result<Option<(bitcoin::Transaction, ExitTxOrigin)>> {
414 let conn = self.connect()?;
415 query::get_exit_child_tx(&conn, exit_txid)
416 }
417
418 async fn update_vtxo_state_checked(
419 &self,
420 vtxo_id: VtxoId,
421 new_state: VtxoState,
422 allowed_old_states: &[VtxoStateKind]
423 ) -> anyhow::Result<WalletVtxo> {
424 let conn = self.connect()?;
425 query::update_vtxo_state_checked(&conn, vtxo_id, new_state, allowed_old_states)
426 }
427
428 async fn update_vtxo_states_checked(
429 &self,
430 vtxo_ids: &[VtxoId],
431 new_state: VtxoState,
432 allowed_old_states: &[VtxoStateKind],
433 ) -> anyhow::Result<()> {
434 let mut conn = self.connect()?;
435 let tx = conn.transaction()?;
436 query::update_vtxo_states_checked(&tx, vtxo_ids, new_state, allowed_old_states)?;
437 tx.commit()?;
438 Ok(())
439 }
440
441 async fn mark_vtxos_registered(&self, vtxo_ids: &[VtxoId]) -> anyhow::Result<()> {
442 let conn = self.connect()?;
443 query::mark_vtxos_registered(&conn, vtxo_ids)
444 }
445
446 async fn get_unregistered_vtxo_ids(&self) -> anyhow::Result<Vec<VtxoId>> {
447 let conn = self.connect()?;
448 query::get_unregistered_vtxo_ids(&conn)
449 }
450
451}
452
453#[cfg(any(test, doc))]
454pub mod helpers {
455 use std::path::PathBuf;
456 use std::str::FromStr;
457
458 use rusqlite::Connection;
459
460 #[cfg(any(test, feature = "rand"))]
467 pub fn in_memory_db() -> (PathBuf, Connection) {
468 use rand::{distr, RngExt};
469
470 let mut rng = rand::rng();
476 let filename: String = (&mut rng).sample_iter(distr::Alphanumeric)
477 .take(16).map(char::from).collect();
478
479 let connection_string = format!("file:{}?mode=memory&cache=shared", filename);
480 let pathbuf = PathBuf::from_str(&connection_string).unwrap();
481
482 let conn = Connection::open(pathbuf.clone()).unwrap();
483 (pathbuf.clone(), conn)
484 }
485}
486
487#[cfg(test)]
488mod test {
489 use ark::ProtocolEncoding;
490 use ark::test_util::VTXO_VECTORS;
491
492 use crate::{persist::sqlite::helpers::in_memory_db, vtxo::VtxoState};
493
494 use super::*;
495
496 #[tokio::test]
497 async fn test_add_and_retrieve_vtxos() {
498 let vtxo_1 = &VTXO_VECTORS.board_vtxo;
499 let vtxo_2 = &VTXO_VECTORS.arkoor_htlc_out_vtxo;
500 let vtxo_3 = &VTXO_VECTORS.round2_vtxo;
501
502 let (cs, conn) = in_memory_db();
503 let db = SqliteClient::open(cs).unwrap();
504
505 db.store_vtxos(&[
506 (vtxo_1, &VtxoState::Spendable), (vtxo_2, &VtxoState::Spendable)
507 ]).await.unwrap();
508
509 let vtxo_1_db = db.get_wallet_vtxo(vtxo_1.id()).await.expect("No error").expect("A vtxo was found");
512 assert_eq!(vtxo_1_db.vtxo, vtxo_1.to_bare());
513
514 let vtxo_1_full = db.get_full_vtxo(vtxo_1.id()).await.unwrap().unwrap();
516 assert_eq!(vtxo_1_full.serialize(), vtxo_1.serialize());
517
518 assert!(db.get_wallet_vtxo(vtxo_3.id()).await.expect("No error").is_none());
520
521 let vtxos = db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await.unwrap();
523 assert_eq!(vtxos.len(), 2);
524 assert!(vtxos.iter().any(|v| v.vtxo == vtxo_1.to_bare()));
525 assert!(vtxos.iter().any(|v| v.vtxo == vtxo_2.to_bare()));
526 assert!(!vtxos.iter().any(|v| v.vtxo == vtxo_3.to_bare()));
527
528 db.update_vtxo_state_checked(
530 vtxo_1.id(), VtxoState::Spent, &VtxoStateKind::UNSPENT_STATES,
531 ).await.unwrap();
532
533 let vtxos = db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await.unwrap();
534 assert_eq!(vtxos.len(), 1);
535
536 db.store_vtxos(&[(vtxo_3, &VtxoState::Spendable)]).await.unwrap();
538
539 let vtxos = db.get_vtxos_by_state(&[VtxoStateKind::Spendable]).await.unwrap();
540 assert_eq!(vtxos.len(), 2);
541 assert!(vtxos.iter().any(|v| v.vtxo == vtxo_2.to_bare()));
542 assert!(vtxos.iter().any(|v| v.vtxo == vtxo_3.to_bare()));
543
544 conn.close().unwrap();
545 }
546
547 #[tokio::test]
548 #[cfg(feature = "onchain-bdk")]
549 async fn test_create_wallet_then_load() {
550 use bdk_wallet::chain::DescriptorExt;
551
552 let (connection_string, conn) = in_memory_db();
553
554 let db = SqliteClient::open(connection_string).unwrap();
555 let network = bitcoin::Network::Testnet;
556
557 let seed = bip39::Mnemonic::generate(12).unwrap().to_seed("");
558 let xpriv = bitcoin::bip32::Xpriv::new_master(network, &seed).unwrap();
559
560 let desc = format!("tr({}/84'/0'/0'/*)", xpriv);
561
562 let _ = db.initialize_bdk_wallet().await.unwrap();
564 let mut created = bdk_wallet::Wallet::create_single(desc.clone())
565 .network(network)
566 .create_wallet_no_persist()
567 .unwrap();
568 db.store_bdk_wallet_changeset(&created.take_staged().unwrap()).await.unwrap();
569
570 let loaded = {
571 let changeset = db.initialize_bdk_wallet().await.unwrap();
572 bdk_wallet::Wallet::load()
573 .descriptor(bdk_wallet::KeychainKind::External, Some(desc.clone()))
574 .extract_keys()
575 .check_network(network)
576 .load_wallet_no_persist(changeset)
577 .unwrap()
578 };
579
580 assert!(loaded.is_some());
581 assert_eq!(
582 created.public_descriptor(bdk_wallet::KeychainKind::External).descriptor_id(),
583 loaded.unwrap().public_descriptor(bdk_wallet::KeychainKind::External).descriptor_id()
584 );
585
586 conn.close().unwrap();
589 }
590
591 #[tokio::test]
592 async fn differential_bark_persister_suite() {
593 let (cs, _conn) = helpers::in_memory_db();
594 let sqlite = SqliteClient::open(cs).unwrap();
595 let memory = crate::persist::adaptor::StorageAdaptorWrapper::new(
596 crate::persist::adaptor::memory::MemoryStorageAdaptor::new(),
597 );
598 crate::persist::test_suite::run_all(&sqlite, &memory).await;
599 }
600}