implement sqlite database
This commit is contained in:
parent
5a6a2cefdd
commit
c06d9f1d33
1
.github/workflows/cont_integration.yml
vendored
1
.github/workflows/cont_integration.yml
vendored
@ -26,6 +26,7 @@ jobs:
|
|||||||
- verify
|
- verify
|
||||||
- async-interface
|
- async-interface
|
||||||
- use-esplora-reqwest
|
- use-esplora-reqwest
|
||||||
|
- sqlite
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Update toolchain
|
- name: Update toolchain
|
||||||
run: rustup update
|
run: rustup update
|
||||||
- name: Build docs
|
- 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
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
@ -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.
|
- Added `flush` method to the `Database` trait to explicitly flush to disk latest changes on the db.
|
||||||
- Add support for proxies in `EsploraBlockchain`
|
- 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]
|
## [v0.10.0] - [v0.9.0]
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ rand = "^0.7"
|
|||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
sled = { version = "0.34", optional = true }
|
sled = { version = "0.34", optional = true }
|
||||||
electrum-client = { version = "0.8", optional = true }
|
electrum-client = { version = "0.8", optional = true }
|
||||||
|
rusqlite = { version = "0.25.3", optional = true }
|
||||||
reqwest = { version = "0.11", optional = true, features = ["json"] }
|
reqwest = { version = "0.11", optional = true, features = ["json"] }
|
||||||
ureq = { version = "2.1", features = ["json"], optional = true }
|
ureq = { version = "2.1", features = ["json"], optional = true }
|
||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
@ -55,6 +56,7 @@ minimal = []
|
|||||||
compiler = ["miniscript/compiler"]
|
compiler = ["miniscript/compiler"]
|
||||||
verify = ["bitcoinconsensus"]
|
verify = ["bitcoinconsensus"]
|
||||||
default = ["key-value-db", "electrum"]
|
default = ["key-value-db", "electrum"]
|
||||||
|
sqlite = ["rusqlite"]
|
||||||
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
|
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
|
||||||
key-value-db = ["sled"]
|
key-value-db = ["sled"]
|
||||||
all-keys = ["keys-bip39"]
|
all-keys = ["keys-bip39"]
|
||||||
|
@ -65,6 +65,8 @@ macro_rules! impl_inner_method {
|
|||||||
$enum_name::Memory(inner) => inner.$name( $($args, )* ),
|
$enum_name::Memory(inner) => inner.$name( $($args, )* ),
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
$enum_name::Sled(inner) => inner.$name( $($args, )* ),
|
$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")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))]
|
||||||
/// Simple key-value embedded database based on [`sled`]
|
/// Simple key-value embedded database based on [`sled`]
|
||||||
Sled(sled::Tree),
|
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!(memory::MemoryDatabase, AnyDatabase, Memory,);
|
||||||
impl_from!(sled::Tree, AnyDatabase, Sled, #[cfg(feature = "key-value-db")]);
|
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
|
/// Type that contains any of the [`BatchDatabase::Batch`] types defined by the library
|
||||||
pub enum AnyBatch {
|
pub enum AnyBatch {
|
||||||
@ -95,6 +102,10 @@ pub enum AnyBatch {
|
|||||||
#[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))]
|
||||||
/// Simple key-value embedded database based on [`sled`]
|
/// Simple key-value embedded database based on [`sled`]
|
||||||
Sled(<sled::Tree as BatchDatabase>::Batch),
|
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!(
|
impl_from!(
|
||||||
@ -103,6 +114,7 @@ impl_from!(
|
|||||||
Memory,
|
Memory,
|
||||||
);
|
);
|
||||||
impl_from!(<sled::Tree as BatchDatabase>::Batch, AnyBatch, Sled, #[cfg(feature = "key-value-db")]);
|
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 {
|
impl BatchOperations for AnyDatabase {
|
||||||
fn set_script_pubkey(
|
fn set_script_pubkey(
|
||||||
@ -300,19 +312,25 @@ impl BatchDatabase for AnyDatabase {
|
|||||||
AnyDatabase::Memory(inner) => inner.begin_batch().into(),
|
AnyDatabase::Memory(inner) => inner.begin_batch().into(),
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
AnyDatabase::Sled(inner) => inner.begin_batch().into(),
|
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> {
|
fn commit_batch(&mut self, batch: Self::Batch) -> Result<(), Error> {
|
||||||
match self {
|
match self {
|
||||||
AnyDatabase::Memory(db) => match batch {
|
AnyDatabase::Memory(db) => match batch {
|
||||||
AnyBatch::Memory(batch) => db.commit_batch(batch),
|
AnyBatch::Memory(batch) => db.commit_batch(batch),
|
||||||
#[cfg(feature = "key-value-db")]
|
_ => unimplemented!("Other batch shouldn't be used with Memory db."),
|
||||||
_ => unimplemented!("Sled batch shouldn't be used with Memory db."),
|
|
||||||
},
|
},
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
AnyDatabase::Sled(db) => match batch {
|
AnyDatabase::Sled(db) => match batch {
|
||||||
AnyBatch::Sled(batch) => db.commit_batch(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
|
/// 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`]
|
/// 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")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "key-value-db")))]
|
||||||
/// Simple key-value embedded database based on [`sled`]
|
/// Simple key-value embedded database based on [`sled`]
|
||||||
Sled(SledDbConfiguration),
|
Sled(SledDbConfiguration),
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "sqlite")))]
|
||||||
|
/// Sqlite embedded database using [`rusqlite`]
|
||||||
|
Sqlite(SqliteDbConfiguration),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigurableDatabase for AnyDatabase {
|
impl ConfigurableDatabase for AnyDatabase {
|
||||||
@ -362,9 +401,14 @@ impl ConfigurableDatabase for AnyDatabase {
|
|||||||
}
|
}
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
AnyDatabaseConfig::Sled(inner) => AnyDatabase::Sled(sled::Tree::from_config(inner)?),
|
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!((), AnyDatabaseConfig, Memory,);
|
||||||
impl_from!(SledDbConfiguration, AnyDatabaseConfig, Sled, #[cfg(feature = "key-value-db")]);
|
impl_from!(SledDbConfiguration, AnyDatabaseConfig, Sled, #[cfg(feature = "key-value-db")]);
|
||||||
|
impl_from!(SqliteDbConfiguration, AnyDatabaseConfig, Sqlite, #[cfg(feature = "sqlite")]);
|
||||||
|
@ -36,6 +36,11 @@ pub use any::{AnyDatabase, AnyDatabaseConfig};
|
|||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
pub(crate) mod keyvalue;
|
pub(crate) mod keyvalue;
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
pub(crate) mod sqlite;
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
pub use sqlite::SqliteDatabase;
|
||||||
|
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub use memory::MemoryDatabase;
|
pub use memory::MemoryDatabase;
|
||||||
|
|
||||||
|
968
src/database/sqlite.rs
Normal file
968
src/database/sqlite.rs
Normal file
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -140,6 +140,9 @@ pub enum Error {
|
|||||||
#[cfg(feature = "rpc")]
|
#[cfg(feature = "rpc")]
|
||||||
/// Rpc client error
|
/// Rpc client error
|
||||||
Rpc(core_rpc::Error),
|
Rpc(core_rpc::Error),
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
/// Rusqlite client error
|
||||||
|
Rusqlite(rusqlite::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
@ -194,6 +197,8 @@ impl_error!(electrum_client::Error, Electrum);
|
|||||||
impl_error!(sled::Error, Sled);
|
impl_error!(sled::Error, Sled);
|
||||||
#[cfg(feature = "rpc")]
|
#[cfg(feature = "rpc")]
|
||||||
impl_error!(core_rpc::Error, Rpc);
|
impl_error!(core_rpc::Error, Rpc);
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
impl_error!(rusqlite::Error, Rusqlite);
|
||||||
|
|
||||||
#[cfg(feature = "compact_filters")]
|
#[cfg(feature = "compact_filters")]
|
||||||
impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
|
impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {
|
||||||
|
@ -244,6 +244,9 @@ pub extern crate electrum_client;
|
|||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
pub extern crate sled;
|
pub extern crate sled;
|
||||||
|
|
||||||
|
#[cfg(feature = "sqlite")]
|
||||||
|
pub extern crate rusqlite;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub(crate) mod error;
|
pub(crate) mod error;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user