diff --git a/crates/chain/src/rusqlite_impl.rs b/crates/chain/src/rusqlite_impl.rs index 6df4d9f45..cf3d6bc9a 100644 --- a/crates/chain/src/rusqlite_impl.rs +++ b/crates/chain/src/rusqlite_impl.rs @@ -1,4 +1,4 @@ -//! Module for stuff +//! Support for persisting `bdk_chain` structures to SQLite using [`rusqlite`]. use crate::*; use core::str::FromStr; @@ -376,8 +376,15 @@ where "REPLACE INTO {}(txid, block_height, block_hash, anchor) VALUES(:txid, :block_height, :block_hash, jsonb(:anchor))", Self::ANCHORS_TABLE_NAME, ))?; + let mut statement_txid = db_tx.prepare_cached(&format!( + "INSERT OR IGNORE INTO {}(txid) VALUES(:txid)", + Self::TXS_TABLE_NAME, + ))?; for (anchor, txid) in &self.anchors { let anchor_block = anchor.anchor_block(); + statement_txid.execute(named_params! { + ":txid": Impl(*txid) + })?; statement.execute(named_params! { ":txid": Impl(*txid), ":block_height": anchor_block.height, @@ -529,3 +536,70 @@ impl keychain_txout::ChangeSet { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + + use bdk_testenv::{anyhow, hash}; + use bitcoin::{absolute, transaction, TxIn, TxOut}; + + #[test] + fn can_persist_anchors_and_txs_independently() -> anyhow::Result<()> { + type ChangeSet = tx_graph::ChangeSet; + let mut conn = rusqlite::Connection::open_in_memory()?; + + // init tables + { + let db_tx = conn.transaction()?; + ChangeSet::init_sqlite_tables(&db_tx)?; + db_tx.commit()?; + } + + let tx = bitcoin::Transaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![TxOut::NULL], + }; + let tx = Arc::new(tx); + let txid = tx.compute_txid(); + let anchor = BlockId { + height: 21, + hash: hash!("anchor"), + }; + + // First persist the anchor + { + let changeset = ChangeSet { + anchors: [(anchor, txid)].into(), + ..Default::default() + }; + let db_tx = conn.transaction()?; + changeset.persist_to_sqlite(&db_tx)?; + db_tx.commit()?; + } + + // Now persist the tx + { + let changeset = ChangeSet { + txs: [tx.clone()].into(), + ..Default::default() + }; + let db_tx = conn.transaction()?; + changeset.persist_to_sqlite(&db_tx)?; + db_tx.commit()?; + } + + // Loading changeset from sqlite should succeed + { + let db_tx = conn.transaction()?; + let changeset = ChangeSet::from_sqlite(&db_tx)?; + db_tx.commit()?; + assert!(changeset.txs.contains(&tx)); + assert!(changeset.anchors.contains(&(anchor, txid))); + } + + Ok(()) + } +} diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 08be91c7a..66fc08fcc 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1055,6 +1055,37 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch assert!(graph.txs_with_no_anchor_or_last_seen().next().is_none()); } +#[test] +fn insert_anchor_without_tx() { + let mut graph = TxGraph::::default(); + + let tx = new_tx(21); + let txid = tx.compute_txid(); + + let anchor = BlockId { + height: 100, + hash: hash!("A"), + }; + + // insert anchor with no corresponding tx + let mut changeset = graph.insert_anchor(txid, anchor); + assert!(changeset.anchors.contains(&(anchor, txid))); + // recover from changeset + let mut recovered = TxGraph::default(); + recovered.apply_changeset(changeset.clone()); + assert_eq!(recovered, graph); + + // now insert tx + let tx = Arc::new(tx); + let graph_changeset = graph.insert_tx(tx.clone()); + assert!(graph_changeset.txs.contains(&tx)); + changeset.merge(graph_changeset); + // recover from changeset again + let mut recovered = TxGraph::default(); + recovered.apply_changeset(changeset); + assert_eq!(recovered, graph); +} + #[test] /// The `map_anchors` allow a caller to pass a function to reconstruct the [`TxGraph`] with any [`Anchor`], /// even though the function is non-deterministic.