--- /dev/null
+// 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());
+ }
+}