]> Untitled Git - bdk/commitdiff
implement sqlite database
authorJohn Cantrell <johncantrell97@gmail.com>
Fri, 18 Jun 2021 17:45:16 +0000 (13:45 -0400)
committerJohn Cantrell <johncantrell97@gmail.com>
Fri, 24 Sep 2021 00:54:08 +0000 (20:54 -0400)
.github/workflows/cont_integration.yml
.github/workflows/nightly_docs.yml
CHANGELOG.md
Cargo.toml
src/database/any.rs
src/database/mod.rs
src/database/sqlite.rs [new file with mode: 0644]
src/error.rs
src/lib.rs

index 233c98ee59acc8212afcb33973ab1ede07c722f2..992b7ab3240467cf588fb906cb3e887f8136ef3b 100644 (file)
@@ -26,6 +26,7 @@ jobs:
           - verify
           - async-interface
           - use-esplora-reqwest
+          - sqlite
     steps:
       - name: checkout
         uses: actions/checkout@v2
index e6a49e2eb80ee1ecbb6bba0b1726b797b5823e16..88f72ba1912b99a0454e772be5b5a5e2d8db4054 100644 (file)
@@ -24,7 +24,7 @@ jobs:
       - name: Update toolchain
         run: rustup update
       - name: Build docs
-        run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys -- --cfg docsrs -Dwarnings
+        run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys,sqlite -- --cfg docsrs -Dwarnings
       - name: Upload artifact
         uses: actions/upload-artifact@v2
         with:
index 4a4b347cdbdd412a7bf1a1bf8ba8e82b27a475c8..6ab9af73b5dfe4185e67c964c9c770ba68bd7bbf 100644 (file)
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Added `flush` method to the `Database` trait to explicitly flush to disk latest changes on the db.
 - Add support for proxies in `EsploraBlockchain`
+- Added `SqliteDatabase` that implements `Database` backed by a sqlite database using `rusqlite` crate.
 
 ## [v0.10.0] - [v0.9.0]
 
index b2b248bd87a7aac3ea9c22e71e6ace7213d9e0bb..ce4e40d521d7fb7a26809a7218c69e762961bed3 100644 (file)
@@ -23,6 +23,7 @@ rand = "^0.7"
 # Optional dependencies
 sled = { version = "0.34", optional = true }
 electrum-client = { version = "0.8", optional = true }
+rusqlite = { version = "0.25.3", optional = true }
 reqwest = { version = "0.11", optional = true, features = ["json"] }
 ureq = { version = "2.1", features = ["json"], optional = true }
 futures = { version = "0.3", optional = true }
@@ -55,6 +56,7 @@ minimal = []
 compiler = ["miniscript/compiler"]
 verify = ["bitcoinconsensus"]
 default = ["key-value-db", "electrum"]
+sqlite = ["rusqlite"]
 compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
 key-value-db = ["sled"]
 all-keys = ["keys-bip39"]
index dbdd2d09e7d56af85544a58861b6f863157d6b78..707d40fc42a34ac128b75639a9b80882fcbd3837 100644 (file)
@@ -65,6 +65,8 @@ macro_rules! impl_inner_method {
             $enum_name::Memory(inner) => inner.$name( $($args, )* ),
             #[cfg(feature = "key-value-db")]
             $enum_name::Sled(inner) => inner.$name( $($args, )* ),
+            #[cfg(feature = "sqlite")]
+            $enum_name::Sqlite(inner) => inner.$name( $($args, )* ),
         }
     }
 }
@@ -82,10 +84,15 @@ pub enum AnyDatabase {
     #[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))]
     /// Simple key-value embedded database based on [`sled`]
     Sled(sled::Tree),
+    #[cfg(feature = "sqlite")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
+    /// Sqlite embedded database using [`rusqlite`]
+    Sqlite(sqlite::SqliteDatabase),
 }
 
 impl_from!(memory::MemoryDatabase, AnyDatabase, Memory,);
 impl_from!(sled::Tree, AnyDatabase, Sled, #[cfg(feature = "key-value-db")]);
+impl_from!(sqlite::SqliteDatabase, AnyDatabase, Sqlite, #[cfg(feature = "sqlite")]);
 
 /// Type that contains any of the [`BatchDatabase::Batch`] types defined by the library
 pub enum AnyBatch {
@@ -95,6 +102,10 @@ pub enum AnyBatch {
     #[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))]
     /// Simple key-value embedded database based on [`sled`]
     Sled(<sled::Tree as BatchDatabase>::Batch),
+    #[cfg(feature = "sqlite")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
+    /// Sqlite embedded database using [`rusqlite`]
+    Sqlite(<sqlite::SqliteDatabase as BatchDatabase>::Batch),
 }
 
 impl_from!(
@@ -103,6 +114,7 @@ impl_from!(
     Memory,
 );
 impl_from!(<sled::Tree as BatchDatabase>::Batch, AnyBatch, Sled, #[cfg(feature = "key-value-db")]);
+impl_from!(<sqlite::SqliteDatabase as BatchDatabase>::Batch, AnyBatch, Sqlite, #[cfg(feature = "sqlite")]);
 
 impl BatchOperations for AnyDatabase {
     fn set_script_pubkey(
@@ -300,19 +312,25 @@ impl BatchDatabase for AnyDatabase {
             AnyDatabase::Memory(inner) => inner.begin_batch().into(),
             #[cfg(feature = "key-value-db")]
             AnyDatabase::Sled(inner) => inner.begin_batch().into(),
+            #[cfg(feature = "sqlite")]
+            AnyDatabase::Sqlite(inner) => inner.begin_batch().into(),
         }
     }
     fn commit_batch(&mut self, batch: Self::Batch) -> Result<(), Error> {
         match self {
             AnyDatabase::Memory(db) => match batch {
                 AnyBatch::Memory(batch) => db.commit_batch(batch),
-                #[cfg(feature = "key-value-db")]
-                _ => unimplemented!("Sled batch shouldn't be used with Memory db."),
+                _ => unimplemented!("Other batch shouldn't be used with Memory db."),
             },
             #[cfg(feature = "key-value-db")]
             AnyDatabase::Sled(db) => match batch {
                 AnyBatch::Sled(batch) => db.commit_batch(batch),
-                _ => unimplemented!("Memory batch shouldn't be used with Sled db."),
+                _ => unimplemented!("Other batch shouldn't be used with Sled db."),
+            },
+            #[cfg(feature = "sqlite")]
+            AnyDatabase::Sqlite(db) => match batch {
+                AnyBatch::Sqlite(batch) => db.commit_batch(batch),
+                _ => unimplemented!("Other batch shouldn't be used with Sqlite db."),
             },
         }
     }
@@ -337,6 +355,23 @@ impl ConfigurableDatabase for sled::Tree {
     }
 }
 
+/// Configuration type for a [`sqlite::SqliteDatabase`] database
+#[cfg(feature = "sqlite")]
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+pub struct SqliteDbConfiguration {
+    /// Main directory of the db
+    pub path: String,
+}
+
+#[cfg(feature = "sqlite")]
+impl ConfigurableDatabase for sqlite::SqliteDatabase {
+    type Config = SqliteDbConfiguration;
+
+    fn from_config(config: &Self::Config) -> Result<Self, Error> {
+        Ok(sqlite::SqliteDatabase::new(config.path.clone()))
+    }
+}
+
 /// Type that can contain any of the database configurations defined by the library
 ///
 /// This allows storing a single configuration that can be loaded into an [`AnyDatabase`]
@@ -350,6 +385,10 @@ pub enum AnyDatabaseConfig {
     #[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))]
     /// Simple key-value embedded database based on [`sled`]
     Sled(SledDbConfiguration),
+    #[cfg(feature = "sqlite")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
+    /// Sqlite embedded database using [`rusqlite`]
+    Sqlite(SqliteDbConfiguration),
 }
 
 impl ConfigurableDatabase for AnyDatabase {
@@ -362,9 +401,14 @@ impl ConfigurableDatabase for AnyDatabase {
             }
             #[cfg(feature = "key-value-db")]
             AnyDatabaseConfig::Sled(inner) => AnyDatabase::Sled(sled::Tree::from_config(inner)?),
+            #[cfg(feature = "sqlite")]
+            AnyDatabaseConfig::Sqlite(inner) => {
+                AnyDatabase::Sqlite(sqlite::SqliteDatabase::from_config(inner)?)
+            }
         })
     }
 }
 
 impl_from!((), AnyDatabaseConfig, Memory,);
 impl_from!(SledDbConfiguration, AnyDatabaseConfig, Sled, #[cfg(feature = "key-value-db")]);
+impl_from!(SqliteDbConfiguration, AnyDatabaseConfig, Sqlite, #[cfg(feature = "sqlite")]);
index 6dbecc66cd88e8853e618af091b5889d7352e319..4a3936f55d98c83ae5f87f88dfc78ecbba924438 100644 (file)
@@ -36,6 +36,11 @@ pub use any::{AnyDatabase, AnyDatabaseConfig};
 #[cfg(feature = "key-value-db")]
 pub(crate) mod keyvalue;
 
+#[cfg(feature = "sqlite")]
+pub(crate) mod sqlite;
+#[cfg(feature = "sqlite")]
+pub use sqlite::SqliteDatabase;
+
 pub mod memory;
 pub use memory::MemoryDatabase;
 
diff --git a/src/database/sqlite.rs b/src/database/sqlite.rs
new file mode 100644 (file)
index 0000000..e396a0a
--- /dev/null
@@ -0,0 +1,968 @@
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+use bitcoin::consensus::encode::{deserialize, serialize};
+use bitcoin::hash_types::Txid;
+use bitcoin::{OutPoint, Script, Transaction, TxOut};
+
+use crate::database::{BatchDatabase, BatchOperations, Database};
+use crate::error::Error;
+use crate::types::*;
+
+use rusqlite::{named_params, Connection};
+
+static MIGRATIONS: &[&str] = &[
+    "CREATE TABLE version (version INTEGER)",
+    "INSERT INTO version VALUES (1)",
+    "CREATE TABLE script_pubkeys (keychain TEXT, child INTEGER, script BLOB);",
+    "CREATE INDEX idx_keychain_child ON script_pubkeys(keychain, child);",
+    "CREATE INDEX idx_script ON script_pubkeys(script);",
+    "CREATE TABLE utxos (value INTEGER, keychain TEXT, vout INTEGER, txid BLOB, script BLOB);",
+    "CREATE INDEX idx_txid_vout ON utxos(txid, vout);",
+    "CREATE TABLE transactions (txid BLOB, raw_tx BLOB);",
+    "CREATE INDEX idx_txid ON transactions(txid);",
+    "CREATE TABLE transaction_details (txid BLOB, timestamp INTEGER, received INTEGER, sent INTEGER, fee INTEGER, height INTEGER, verified INTEGER DEFAULT 0);",
+    "CREATE INDEX idx_txdetails_txid ON transaction_details(txid);",
+    "CREATE TABLE last_derivation_indices (keychain TEXT, value INTEGER);",
+    "CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);",
+    "CREATE TABLE checksums (keychain TEXT, checksum BLOB);",
+    "CREATE INDEX idx_checksums_keychain ON checksums(keychain);",
+];
+
+/// Sqlite database stored on filesystem
+///
+/// This is a permanent storage solution for devices and platforms that provide a filesystem.
+/// [`crate::database`]
+#[derive(Debug)]
+pub struct SqliteDatabase {
+    /// Path on the local filesystem to store the sqlite file
+    pub path: String,
+    /// A rusqlite connection object to the sqlite database
+    pub connection: Connection,
+}
+
+impl SqliteDatabase {
+    /// Instantiate a new SqliteDatabase instance by creating a connection
+    /// to the database stored at path
+    pub fn new(path: String) -> Self {
+        let connection = get_connection(&path).unwrap();
+        SqliteDatabase { path, connection }
+    }
+    fn insert_script_pubkey(
+        &self,
+        keychain: String,
+        child: u32,
+        script: &[u8],
+    ) -> Result<i64, Error> {
+        let mut statement = self.connection.prepare_cached("INSERT INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
+        statement.execute(named_params! {
+            ":keychain": keychain,
+            ":child": child,
+            ":script": script
+        })?;
+
+        Ok(self.connection.last_insert_rowid())
+    }
+    fn insert_utxo(
+        &self,
+        value: u64,
+        keychain: String,
+        vout: u32,
+        txid: &[u8],
+        script: &[u8],
+    ) -> Result<i64, Error> {
+        let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script) VALUES (:value, :keychain, :vout, :txid, :script)")?;
+        statement.execute(named_params! {
+            ":value": value,
+            ":keychain": keychain,
+            ":vout": vout,
+            ":txid": txid,
+            ":script": script
+        })?;
+
+        Ok(self.connection.last_insert_rowid())
+    }
+    fn insert_transaction(&self, txid: &[u8], raw_tx: &[u8]) -> Result<i64, Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("INSERT INTO transactions (txid, raw_tx) VALUES (:txid, :raw_tx)")?;
+        statement.execute(named_params! {
+            ":txid": txid,
+            ":raw_tx": raw_tx,
+        })?;
+
+        Ok(self.connection.last_insert_rowid())
+    }
+
+    fn update_transaction(&self, txid: &[u8], raw_tx: &[u8]) -> Result<(), Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("UPDATE transactions SET raw_tx=:raw_tx WHERE txid=:txid")?;
+
+        statement.execute(named_params! {
+            ":txid": txid,
+            ":raw_tx": raw_tx,
+        })?;
+
+        Ok(())
+    }
+
+    fn insert_transaction_details(&self, transaction: &TransactionDetails) -> Result<i64, Error> {
+        let (timestamp, height) = match &transaction.confirmation_time {
+            Some(confirmation_time) => (
+                Some(confirmation_time.timestamp),
+                Some(confirmation_time.height),
+            ),
+            None => (None, None),
+        };
+
+        let txid: &[u8] = &transaction.txid;
+
+        let mut statement = self.connection.prepare_cached("INSERT INTO transaction_details (txid, timestamp, received, sent, fee, height, verified) VALUES (:txid, :timestamp, :received, :sent, :fee, :height, :verified)")?;
+
+        statement.execute(named_params! {
+            ":txid": txid,
+            ":timestamp": timestamp,
+            ":received": transaction.received,
+            ":sent": transaction.sent,
+            ":fee": transaction.fee,
+            ":height": height,
+            ":verified": transaction.verified
+        })?;
+
+        Ok(self.connection.last_insert_rowid())
+    }
+
+    fn update_transaction_details(&self, transaction: &TransactionDetails) -> Result<(), Error> {
+        let (timestamp, height) = match &transaction.confirmation_time {
+            Some(confirmation_time) => (
+                Some(confirmation_time.timestamp),
+                Some(confirmation_time.height),
+            ),
+            None => (None, None),
+        };
+
+        let txid: &[u8] = &transaction.txid;
+
+        let mut statement = self.connection.prepare_cached("UPDATE transaction_details SET timestamp=:timestamp, received=:received, sent=:sent, fee=:fee, height=:height, verified=:verified WHERE txid=:txid")?;
+
+        statement.execute(named_params! {
+            ":txid": txid,
+            ":timestamp": timestamp,
+            ":received": transaction.received,
+            ":sent": transaction.sent,
+            ":fee": transaction.fee,
+            ":height": height,
+            ":verified": transaction.verified,
+        })?;
+
+        Ok(())
+    }
+
+    fn insert_last_derivation_index(&self, keychain: String, value: u32) -> Result<i64, Error> {
+        let mut statement = self.connection.prepare_cached(
+            "INSERT INTO last_derivation_indices (keychain, value) VALUES (:keychain, :value)",
+        )?;
+
+        statement.execute(named_params! {
+            ":keychain": keychain,
+            ":value": value,
+        })?;
+
+        Ok(self.connection.last_insert_rowid())
+    }
+
+    fn insert_checksum(&self, keychain: String, checksum: &[u8]) -> Result<i64, Error> {
+        let mut statement = self.connection.prepare_cached(
+            "INSERT INTO checksums (keychain, checksum) VALUES (:keychain, :checksum)",
+        )?;
+        statement.execute(named_params! {
+            ":keychain": keychain,
+            ":checksum": checksum,
+        })?;
+
+        Ok(self.connection.last_insert_rowid())
+    }
+
+    fn update_last_derivation_index(&self, keychain: String, value: u32) -> Result<(), Error> {
+        let mut statement = self.connection.prepare_cached(
+            "INSERT INTO last_derivation_indices (keychain, value) VALUES (:keychain, :value) ON CONFLICT(keychain) DO UPDATE SET value=:value WHERE keychain=:keychain",
+        )?;
+
+        statement.execute(named_params! {
+            ":keychain": keychain,
+            ":value": value,
+        })?;
+
+        Ok(())
+    }
+
+    fn select_script_pubkeys(&self) -> Result<Vec<Script>, Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("SELECT script FROM script_pubkeys")?;
+        let mut scripts: Vec<Script> = vec![];
+        let mut rows = statement.query([])?;
+        while let Some(row) = rows.next()? {
+            let raw_script: Vec<u8> = row.get(0)?;
+            scripts.push(raw_script.into());
+        }
+
+        Ok(scripts)
+    }
+
+    fn select_script_pubkeys_by_keychain(&self, keychain: String) -> Result<Vec<Script>, Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("SELECT script FROM script_pubkeys WHERE keychain=:keychain")?;
+        let mut scripts: Vec<Script> = vec![];
+        let mut rows = statement.query(named_params! {":keychain": keychain})?;
+        while let Some(row) = rows.next()? {
+            let raw_script: Vec<u8> = row.get(0)?;
+            scripts.push(raw_script.into());
+        }
+
+        Ok(scripts)
+    }
+
+    fn select_script_pubkey_by_path(
+        &self,
+        keychain: String,
+        child: u32,
+    ) -> Result<Option<Script>, Error> {
+        let mut statement = self.connection.prepare_cached(
+            "SELECT script FROM script_pubkeys WHERE keychain=:keychain AND child=:child",
+        )?;
+        let mut rows = statement.query(named_params! {":keychain": keychain,":child": child})?;
+
+        match rows.next()? {
+            Some(row) => {
+                let script: Vec<u8> = row.get(0)?;
+                let script: Script = script.into();
+                Ok(Some(script))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn select_script_pubkey_by_script(
+        &self,
+        script: &[u8],
+    ) -> Result<Option<(KeychainKind, u32)>, Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("SELECT keychain, child FROM script_pubkeys WHERE script=:script")?;
+        let mut rows = statement.query(named_params! {":script": script})?;
+        match rows.next()? {
+            Some(row) => {
+                let keychain: String = row.get(0)?;
+                let keychain: KeychainKind = serde_json::from_str(&keychain)?;
+                let child: u32 = row.get(1)?;
+                Ok(Some((keychain, child)))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn select_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("SELECT value, keychain, vout, txid, script FROM utxos")?;
+        let mut utxos: Vec<LocalUtxo> = vec![];
+        let mut rows = statement.query([])?;
+        while let Some(row) = rows.next()? {
+            let value = row.get(0)?;
+            let keychain: String = row.get(1)?;
+            let vout = row.get(2)?;
+            let txid: Vec<u8> = row.get(3)?;
+            let script: Vec<u8> = row.get(4)?;
+
+            let keychain: KeychainKind = serde_json::from_str(&keychain)?;
+
+            utxos.push(LocalUtxo {
+                outpoint: OutPoint::new(deserialize(&txid)?, vout),
+                txout: TxOut {
+                    value,
+                    script_pubkey: script.into(),
+                },
+                keychain,
+            })
+        }
+
+        Ok(utxos)
+    }
+
+    fn select_utxo_by_outpoint(
+        &self,
+        txid: &[u8],
+        vout: u32,
+    ) -> Result<Option<(u64, KeychainKind, Script)>, Error> {
+        let mut statement = self.connection.prepare_cached(
+            "SELECT value, keychain, script FROM utxos WHERE txid=:txid AND vout=:vout",
+        )?;
+        let mut rows = statement.query(named_params! {":txid": txid,":vout": vout})?;
+        match rows.next()? {
+            Some(row) => {
+                let value: u64 = row.get(0)?;
+                let keychain: String = row.get(1)?;
+                let keychain: KeychainKind = serde_json::from_str(&keychain)?;
+                let script: Vec<u8> = row.get(2)?;
+                let script: Script = script.into();
+
+                Ok(Some((value, keychain, script)))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn select_transactions(&self) -> Result<Vec<Transaction>, Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("SELECT raw_tx FROM transactions")?;
+        let mut txs: Vec<Transaction> = vec![];
+        let mut rows = statement.query([])?;
+        while let Some(row) = rows.next()? {
+            let raw_tx: Vec<u8> = row.get(0)?;
+            let tx: Transaction = deserialize(&raw_tx)?;
+            txs.push(tx);
+        }
+        Ok(txs)
+    }
+
+    fn select_transaction_by_txid(&self, txid: &[u8]) -> Result<Option<Transaction>, Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("SELECT raw_tx FROM transactions WHERE txid=:txid")?;
+        let mut rows = statement.query(named_params! {":txid": txid})?;
+        match rows.next()? {
+            Some(row) => {
+                let raw_tx: Vec<u8> = row.get(0)?;
+                let tx: Transaction = deserialize(&raw_tx)?;
+                Ok(Some(tx))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn select_transaction_details_with_raw(&self) -> Result<Vec<TransactionDetails>, Error> {
+        let mut statement = self.connection.prepare_cached("SELECT transaction_details.txid, transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transaction_details.verified, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid = transactions.txid")?;
+        let mut transaction_details: Vec<TransactionDetails> = vec![];
+        let mut rows = statement.query([])?;
+        while let Some(row) = rows.next()? {
+            let txid: Vec<u8> = row.get(0)?;
+            let txid: Txid = deserialize(&txid)?;
+            let timestamp: Option<u64> = row.get(1)?;
+            let received: u64 = row.get(2)?;
+            let sent: u64 = row.get(3)?;
+            let fee: Option<u64> = row.get(4)?;
+            let height: Option<u32> = row.get(5)?;
+            let verified: bool = row.get(6)?;
+            let raw_tx: Option<Vec<u8>> = row.get(7)?;
+            let tx: Option<Transaction> = match raw_tx {
+                Some(raw_tx) => {
+                    let tx: Transaction = deserialize(&raw_tx)?;
+                    Some(tx)
+                }
+                None => None,
+            };
+
+            let confirmation_time = match (timestamp, height) {
+                (Some(timestamp), Some(height)) => Some(ConfirmationTime { timestamp, height }),
+                _ => None,
+            };
+
+            transaction_details.push(TransactionDetails {
+                transaction: tx,
+                txid,
+                received,
+                sent,
+                fee,
+                confirmation_time,
+                verified,
+            });
+        }
+        Ok(transaction_details)
+    }
+
+    fn select_transaction_details(&self) -> Result<Vec<TransactionDetails>, Error> {
+        let mut statement = self.connection.prepare_cached(
+            "SELECT txid, timestamp, received, sent, fee, height, verified FROM transaction_details",
+        )?;
+        let mut transaction_details: Vec<TransactionDetails> = vec![];
+        let mut rows = statement.query([])?;
+        while let Some(row) = rows.next()? {
+            let txid: Vec<u8> = row.get(0)?;
+            let txid: Txid = deserialize(&txid)?;
+            let timestamp: Option<u64> = row.get(1)?;
+            let received: u64 = row.get(2)?;
+            let sent: u64 = row.get(3)?;
+            let fee: Option<u64> = row.get(4)?;
+            let height: Option<u32> = row.get(5)?;
+            let verified: bool = row.get(6)?;
+
+            let confirmation_time = match (timestamp, height) {
+                (Some(timestamp), Some(height)) => Some(ConfirmationTime { timestamp, height }),
+                _ => None,
+            };
+
+            transaction_details.push(TransactionDetails {
+                transaction: None,
+                txid,
+                received,
+                sent,
+                fee,
+                confirmation_time,
+                verified,
+            });
+        }
+        Ok(transaction_details)
+    }
+
+    fn select_transaction_details_by_txid(
+        &self,
+        txid: &[u8],
+    ) -> Result<Option<TransactionDetails>, Error> {
+        let mut statement = self.connection.prepare_cached("SELECT transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transaction_details.verified, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid=transactions.txid AND transaction_details.txid=:txid")?;
+        let mut rows = statement.query(named_params! { ":txid": txid })?;
+
+        match rows.next()? {
+            Some(row) => {
+                let timestamp: Option<u64> = row.get(0)?;
+                let received: u64 = row.get(1)?;
+                let sent: u64 = row.get(2)?;
+                let fee: Option<u64> = row.get(3)?;
+                let height: Option<u32> = row.get(4)?;
+                let verified: bool = row.get(5)?;
+
+                let raw_tx: Option<Vec<u8>> = row.get(6)?;
+                let tx: Option<Transaction> = match raw_tx {
+                    Some(raw_tx) => {
+                        let tx: Transaction = deserialize(&raw_tx)?;
+                        Some(tx)
+                    }
+                    None => None,
+                };
+
+                let confirmation_time = match (timestamp, height) {
+                    (Some(timestamp), Some(height)) => Some(ConfirmationTime { timestamp, height }),
+                    _ => None,
+                };
+
+                Ok(Some(TransactionDetails {
+                    transaction: tx,
+                    txid: deserialize(txid)?,
+                    received,
+                    sent,
+                    fee,
+                    confirmation_time,
+                    verified,
+                }))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn select_last_derivation_index_by_keychain(
+        &self,
+        keychain: String,
+    ) -> Result<Option<u32>, Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("SELECT value FROM last_derivation_indices WHERE keychain=:keychain")?;
+        let mut rows = statement.query(named_params! {":keychain": keychain})?;
+        match rows.next()? {
+            Some(row) => {
+                let value: u32 = row.get(0)?;
+                Ok(Some(value))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn select_checksum_by_keychain(&self, keychain: String) -> Result<Option<Vec<u8>>, Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("SELECT checksum FROM checksums WHERE keychain=:keychain")?;
+        let mut rows = statement.query(named_params! {":keychain": keychain})?;
+
+        match rows.next()? {
+            Some(row) => {
+                let checksum: Vec<u8> = row.get(0)?;
+                Ok(Some(checksum))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn delete_script_pubkey_by_path(&self, keychain: String, child: u32) -> Result<(), Error> {
+        let mut statement = self.connection.prepare_cached(
+            "DELETE FROM script_pubkeys WHERE keychain=:keychain AND child=:child",
+        )?;
+        statement.execute(named_params! {
+            ":keychain": keychain,
+            ":child": child
+        })?;
+
+        Ok(())
+    }
+
+    fn delete_script_pubkey_by_script(&self, script: &[u8]) -> Result<(), Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("DELETE FROM script_pubkeys WHERE script=:script")?;
+        statement.execute(named_params! {
+            ":script": script
+        })?;
+
+        Ok(())
+    }
+
+    fn delete_utxo_by_outpoint(&self, txid: &[u8], vout: u32) -> Result<(), Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("DELETE FROM utxos WHERE txid=:txid AND vout=:vout")?;
+        statement.execute(named_params! {
+            ":txid": txid,
+            ":vout": vout
+        })?;
+
+        Ok(())
+    }
+
+    fn delete_transaction_by_txid(&self, txid: &[u8]) -> Result<(), Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("DELETE FROM transactions WHERE txid=:txid")?;
+        statement.execute(named_params! {":txid": txid})?;
+        Ok(())
+    }
+
+    fn delete_transaction_details_by_txid(&self, txid: &[u8]) -> Result<(), Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("DELETE FROM transaction_details WHERE txid=:txid")?;
+        statement.execute(named_params! {":txid": txid})?;
+        Ok(())
+    }
+
+    fn delete_last_derivation_index_by_keychain(&self, keychain: String) -> Result<(), Error> {
+        let mut statement = self
+            .connection
+            .prepare_cached("DELETE FROM last_derivation_indices WHERE keychain=:keychain")?;
+        statement.execute(named_params! {
+            ":keychain": &keychain
+        })?;
+
+        Ok(())
+    }
+}
+
+impl BatchOperations for SqliteDatabase {
+    fn set_script_pubkey(
+        &mut self,
+        script: &Script,
+        keychain: KeychainKind,
+        child: u32,
+    ) -> Result<(), Error> {
+        let keychain = serde_json::to_string(&keychain)?;
+        self.insert_script_pubkey(keychain, child, script.as_bytes())?;
+        Ok(())
+    }
+
+    fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
+        self.insert_utxo(
+            utxo.txout.value,
+            serde_json::to_string(&utxo.keychain)?,
+            utxo.outpoint.vout,
+            &utxo.outpoint.txid,
+            utxo.txout.script_pubkey.as_bytes(),
+        )?;
+        Ok(())
+    }
+
+    fn set_raw_tx(&mut self, transaction: &Transaction) -> Result<(), Error> {
+        match self.select_transaction_by_txid(&transaction.txid())? {
+            Some(_) => {
+                self.update_transaction(&transaction.txid(), &serialize(transaction))?;
+            }
+            None => {
+                self.insert_transaction(&transaction.txid(), &serialize(transaction))?;
+            }
+        }
+        Ok(())
+    }
+
+    fn set_tx(&mut self, transaction: &TransactionDetails) -> Result<(), Error> {
+        match self.select_transaction_details_by_txid(&transaction.txid)? {
+            Some(_) => {
+                self.update_transaction_details(transaction)?;
+            }
+            None => {
+                self.insert_transaction_details(transaction)?;
+            }
+        }
+
+        if let Some(tx) = &transaction.transaction {
+            self.set_raw_tx(tx)?;
+        }
+
+        Ok(())
+    }
+
+    fn set_last_index(&mut self, keychain: KeychainKind, value: u32) -> Result<(), Error> {
+        self.update_last_derivation_index(serde_json::to_string(&keychain)?, value)?;
+        Ok(())
+    }
+
+    fn del_script_pubkey_from_path(
+        &mut self,
+        keychain: KeychainKind,
+        child: u32,
+    ) -> Result<Option<Script>, Error> {
+        let keychain = serde_json::to_string(&keychain)?;
+        let script = self.select_script_pubkey_by_path(keychain.clone(), child)?;
+        match script {
+            Some(script) => {
+                self.delete_script_pubkey_by_path(keychain, child)?;
+                Ok(Some(script))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn del_path_from_script_pubkey(
+        &mut self,
+        script: &Script,
+    ) -> Result<Option<(KeychainKind, u32)>, Error> {
+        match self.select_script_pubkey_by_script(script.as_bytes())? {
+            Some((keychain, child)) => {
+                self.delete_script_pubkey_by_script(script.as_bytes())?;
+                Ok(Some((keychain, child)))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
+        match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
+            Some((value, keychain, script_pubkey)) => {
+                self.delete_utxo_by_outpoint(&outpoint.txid, outpoint.vout)?;
+                Ok(Some(LocalUtxo {
+                    outpoint: *outpoint,
+                    txout: TxOut {
+                        value,
+                        script_pubkey,
+                    },
+                    keychain,
+                }))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn del_raw_tx(&mut self, txid: &Txid) -> Result<Option<Transaction>, Error> {
+        match self.select_transaction_by_txid(txid)? {
+            Some(tx) => {
+                self.delete_transaction_by_txid(txid)?;
+                Ok(Some(tx))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn del_tx(
+        &mut self,
+        txid: &Txid,
+        include_raw: bool,
+    ) -> Result<Option<TransactionDetails>, Error> {
+        match self.select_transaction_details_by_txid(txid)? {
+            Some(transaction_details) => {
+                self.delete_transaction_details_by_txid(txid)?;
+
+                if include_raw {
+                    self.delete_transaction_by_txid(txid)?;
+                }
+                Ok(Some(transaction_details))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn del_last_index(&mut self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
+        let keychain = serde_json::to_string(&keychain)?;
+        match self.select_last_derivation_index_by_keychain(keychain.clone())? {
+            Some(value) => {
+                self.delete_last_derivation_index_by_keychain(keychain)?;
+
+                Ok(Some(value))
+            }
+            None => Ok(None),
+        }
+    }
+}
+
+impl Database for SqliteDatabase {
+    fn check_descriptor_checksum<B: AsRef<[u8]>>(
+        &mut self,
+        keychain: KeychainKind,
+        bytes: B,
+    ) -> Result<(), Error> {
+        let keychain = serde_json::to_string(&keychain)?;
+
+        match self.select_checksum_by_keychain(keychain.clone())? {
+            Some(checksum) => {
+                if checksum == bytes.as_ref().to_vec() {
+                    Ok(())
+                } else {
+                    Err(Error::ChecksumMismatch)
+                }
+            }
+            None => {
+                self.insert_checksum(keychain, bytes.as_ref())?;
+                Ok(())
+            }
+        }
+    }
+
+    fn iter_script_pubkeys(&self, keychain: Option<KeychainKind>) -> Result<Vec<Script>, Error> {
+        match keychain {
+            Some(keychain) => {
+                let keychain = serde_json::to_string(&keychain)?;
+                self.select_script_pubkeys_by_keychain(keychain)
+            }
+            None => self.select_script_pubkeys(),
+        }
+    }
+
+    fn iter_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
+        self.select_utxos()
+    }
+
+    fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error> {
+        self.select_transactions()
+    }
+
+    fn iter_txs(&self, include_raw: bool) -> Result<Vec<TransactionDetails>, Error> {
+        match include_raw {
+            true => self.select_transaction_details_with_raw(),
+            false => self.select_transaction_details(),
+        }
+    }
+
+    fn get_script_pubkey_from_path(
+        &self,
+        keychain: KeychainKind,
+        child: u32,
+    ) -> Result<Option<Script>, Error> {
+        let keychain = serde_json::to_string(&keychain)?;
+        match self.select_script_pubkey_by_path(keychain, child)? {
+            Some(script) => Ok(Some(script)),
+            None => Ok(None),
+        }
+    }
+
+    fn get_path_from_script_pubkey(
+        &self,
+        script: &Script,
+    ) -> Result<Option<(KeychainKind, u32)>, Error> {
+        match self.select_script_pubkey_by_script(script.as_bytes())? {
+            Some((keychain, child)) => Ok(Some((keychain, child))),
+            None => Ok(None),
+        }
+    }
+
+    fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
+        match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
+            Some((value, keychain, script_pubkey)) => Ok(Some(LocalUtxo {
+                outpoint: *outpoint,
+                txout: TxOut {
+                    value,
+                    script_pubkey,
+                },
+                keychain,
+            })),
+            None => Ok(None),
+        }
+    }
+
+    fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
+        match self.select_transaction_by_txid(txid)? {
+            Some(tx) => Ok(Some(tx)),
+            None => Ok(None),
+        }
+    }
+
+    fn get_tx(&self, txid: &Txid, include_raw: bool) -> Result<Option<TransactionDetails>, Error> {
+        match self.select_transaction_details_by_txid(txid)? {
+            Some(mut transaction_details) => {
+                if !include_raw {
+                    transaction_details.transaction = None;
+                }
+                Ok(Some(transaction_details))
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn get_last_index(&self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
+        let keychain = serde_json::to_string(&keychain)?;
+        let value = self.select_last_derivation_index_by_keychain(keychain)?;
+        Ok(value)
+    }
+
+    fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
+        let keychain_string = serde_json::to_string(&keychain)?;
+        match self.get_last_index(keychain)? {
+            Some(value) => {
+                self.update_last_derivation_index(keychain_string, value + 1)?;
+                Ok(value + 1)
+            }
+            None => {
+                self.insert_last_derivation_index(keychain_string, 0)?;
+                Ok(0)
+            }
+        }
+    }
+
+    fn flush(&mut self) -> Result<(), Error> {
+        Ok(())
+    }
+}
+
+impl BatchDatabase for SqliteDatabase {
+    type Batch = SqliteDatabase;
+
+    fn begin_batch(&self) -> Self::Batch {
+        let db = SqliteDatabase::new(self.path.clone());
+        db.connection.execute("BEGIN TRANSACTION", []).unwrap();
+        db
+    }
+
+    fn commit_batch(&mut self, batch: Self::Batch) -> Result<(), Error> {
+        batch.connection.execute("COMMIT TRANSACTION", [])?;
+        Ok(())
+    }
+}
+
+pub fn get_connection(path: &str) -> Result<Connection, Error> {
+    let connection = Connection::open(path)?;
+    migrate(&connection)?;
+    Ok(connection)
+}
+
+pub fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
+    let statement = conn.prepare_cached("SELECT version FROM version");
+    match statement {
+        Err(rusqlite::Error::SqliteFailure(e, Some(msg))) => {
+            if msg == "no such table: version" {
+                Ok(0)
+            } else {
+                Err(rusqlite::Error::SqliteFailure(e, Some(msg)))
+            }
+        }
+        Ok(mut stmt) => {
+            let mut rows = stmt.query([])?;
+            match rows.next()? {
+                Some(row) => {
+                    let version: i32 = row.get(0)?;
+                    Ok(version)
+                }
+                None => Ok(0),
+            }
+        }
+        _ => Ok(0),
+    }
+}
+
+pub fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
+    conn.execute(
+        "UPDATE version SET version=:version",
+        named_params! {":version": version},
+    )
+}
+
+pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
+    let version = get_schema_version(conn)?;
+    let stmts = &MIGRATIONS[(version as usize)..];
+    let mut i: i32 = version;
+
+    if version == MIGRATIONS.len() as i32 {
+        log::info!("db up to date, no migration needed");
+        return Ok(());
+    }
+
+    for stmt in stmts {
+        let res = conn.execute(stmt, []);
+        if res.is_err() {
+            println!("migration failed on:\n{}\n{:?}", stmt, res);
+            break;
+        }
+
+        i += 1;
+    }
+
+    set_schema_version(conn, i)?;
+
+    Ok(())
+}
+
+#[cfg(test)]
+pub mod test {
+    use crate::database::SqliteDatabase;
+    use std::time::{SystemTime, UNIX_EPOCH};
+
+    fn get_database() -> SqliteDatabase {
+        let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
+        let mut dir = std::env::temp_dir();
+        dir.push(format!("bdk_{}", time.as_nanos()));
+        SqliteDatabase::new(String::from(dir.to_str().unwrap()))
+    }
+
+    #[test]
+    fn test_script_pubkey() {
+        crate::database::test::test_script_pubkey(get_database());
+    }
+
+    #[test]
+    fn test_batch_script_pubkey() {
+        crate::database::test::test_batch_script_pubkey(get_database());
+    }
+
+    #[test]
+    fn test_iter_script_pubkey() {
+        crate::database::test::test_iter_script_pubkey(get_database());
+    }
+
+    #[test]
+    fn test_del_script_pubkey() {
+        crate::database::test::test_del_script_pubkey(get_database());
+    }
+
+    #[test]
+    fn test_utxo() {
+        crate::database::test::test_utxo(get_database());
+    }
+
+    #[test]
+    fn test_raw_tx() {
+        crate::database::test::test_raw_tx(get_database());
+    }
+
+    #[test]
+    fn test_tx() {
+        crate::database::test::test_tx(get_database());
+    }
+
+    #[test]
+    fn test_last_index() {
+        crate::database::test::test_last_index(get_database());
+    }
+}
index ebbd2745b8a51caed0ded4161ef28b09a844bec2..d2875c8197cf8be51703323b91ab40daa93c3eb6 100644 (file)
@@ -140,6 +140,9 @@ pub enum Error {
     #[cfg(feature = "rpc")]
     /// Rpc client error
     Rpc(core_rpc::Error),
+    #[cfg(feature = "sqlite")]
+    /// Rusqlite client error
+    Rusqlite(rusqlite::Error),
 }
 
 impl fmt::Display for Error {
@@ -194,6 +197,8 @@ impl_error!(electrum_client::Error, Electrum);
 impl_error!(sled::Error, Sled);
 #[cfg(feature = "rpc")]
 impl_error!(core_rpc::Error, Rpc);
+#[cfg(feature = "sqlite")]
+impl_error!(rusqlite::Error, Rusqlite);
 
 #[cfg(feature = "compact_filters")]
 impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
index 83bb02384bbaad59dee88789b3b05289cc348905..007311c92000ef94f4e53724bcfe51b236caf982 100644 (file)
@@ -244,6 +244,9 @@ pub extern crate electrum_client;
 #[cfg(feature = "key-value-db")]
 pub extern crate sled;
 
+#[cfg(feature = "sqlite")]
+pub extern crate rusqlite;
+
 #[allow(unused_imports)]
 #[macro_use]
 pub(crate) mod error;