forked from bitcoindevkit/bdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add bdk_sqlite_store crate implementing PersistBackend backed b…
…y a SQLite database
- Loading branch information
1 parent
bb0a951
commit 9e8916c
Showing
10 changed files
with
1,229 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,4 @@ Cargo.lock | |
|
||
# Example persisted files. | ||
*.db | ||
*.sqlite* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[package] | ||
name = "bdk_sqlite_store" | ||
version = "0.1.0" | ||
edition = "2021" | ||
license = "MIT OR Apache-2.0" | ||
repository = "https://github.com/bitcoindevkit/bdk" | ||
documentation = "https://docs.rs/bdk_sqlite_store" | ||
description = "A simple SQLite based implementation of Persist for Bitcoin Dev Kit." | ||
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"] | ||
authors = ["Bitcoin Dev Kit Developers"] | ||
readme = "README.md" | ||
|
||
[dependencies] | ||
anyhow = { version = "1", default-features = false } | ||
bdk_chain = { path = "../chain", version = "0.13.0", features = [ "serde", "miniscript" ] } | ||
bdk_persist = { path = "../persist", version = "0.1.0" } | ||
rusqlite = { version = "0.31.0", features = ["bundled"]} | ||
serde = { version = "1", features = ["derive"] } | ||
serde_json = "1" | ||
|
||
# optional | ||
bdk = { path = "../bdk", optional = true } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# BDK SQLite Store | ||
|
||
This is a simple [SQLite] relational database schema backed implementation of | ||
[`PersistBackend`](`bdk_chain::PersistBackend`). | ||
|
||
The main structure is [`Store`](`store::Store`) which works with any `bdk_chain` based changeset to persist data into a SQLite database file. To use `Store` with [`bdk`]'s `Wallet` enable the `bdk` feature. | ||
|
||
[`bdk`]: https://docs.rs/bdk/latest | ||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest | ||
[SQLite]: https://www.sqlite.org/index.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
-- schema version control | ||
CREATE TABLE version (version INTEGER) STRICT; | ||
INSERT INTO version VALUES (1); | ||
|
||
-- network is the valid network for all other table data | ||
CREATE TABLE network | ||
( | ||
name TEXT UNIQUE NOT NULL | ||
) STRICT; | ||
|
||
-- keychain is the json serialized keychain structure as JSONB, | ||
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum, | ||
-- descriptor is the complete descriptor string, | ||
-- last active index is a u32 | ||
CREATE TABLE keychain | ||
( | ||
keychain BLOB PRIMARY KEY NOT NULL, | ||
descriptor TEXT UNIQUE NOT NULL, | ||
descriptor_id BLOB UNIQUE NOT NULL, | ||
last_revealed INTEGER | ||
) STRICT; | ||
|
||
-- hash is block hash hex string, | ||
-- block height is a u32, | ||
-- previous block hash hex string | ||
CREATE TABLE block | ||
( | ||
hash TEXT PRIMARY KEY NOT NULL, | ||
height INTEGER NOT NULL | ||
) STRICT; | ||
|
||
-- txid is transaction hash hex string (reversed) | ||
-- whole_tx is a consensus encoded transaction, | ||
-- last seen is a u64 unix epoch seconds | ||
CREATE TABLE tx | ||
( | ||
txid TEXT PRIMARY KEY NOT NULL, | ||
whole_tx BLOB NOT NULL, | ||
last_seen INTEGER | ||
) STRICT; | ||
|
||
-- Outpoint txid hash hex string (reversed) | ||
-- Outpoint vout | ||
-- TxOut value as SATs | ||
-- TxOut script consensus encoded | ||
CREATE TABLE txout | ||
( | ||
txid TEXT NOT NULL, | ||
vout INTEGER NOT NULL, | ||
value INTEGER NOT NULL, | ||
script BLOB NOT NULL, | ||
PRIMARY KEY (txid, vout) | ||
) STRICT; | ||
|
||
-- set of block "anchors" and txids | ||
-- block hash hex string | ||
-- block height u32 | ||
-- confirmation height u32 | ||
-- confirmation time unix epoch seconds | ||
-- txid is transaction hash hex string (reversed) | ||
CREATE TABLE anchor | ||
( | ||
block_hash TEXT NOT NULL REFERENCES block (hash), | ||
confirmation_height INTEGER, | ||
confirmation_time INTEGER, | ||
txid TEXT NOT NULL REFERENCES tx (txid) | ||
) STRICT; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
#![doc = include_str!("../README.md")] | ||
|
||
pub mod persist; | ||
mod schema; | ||
pub mod store; | ||
#[cfg(feature = "bdk")] | ||
#[cfg_attr(docsrs, doc(cfg(feature = "wallet")))] | ||
pub mod wallet; | ||
|
||
use bdk_chain::bitcoin::Network; | ||
use bdk_chain::{indexed_tx_graph, keychain, local_chain, Anchor, Append}; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
/// Change set representing changes to [`local_chain::ChangeSet`] and [`indexed_tx_graph::ChangeSet`]. | ||
/// | ||
/// This structure is used to persist data with the SQLite based [`store::Store`] provided by this crate. | ||
#[derive(Clone, Debug, PartialEq)] | ||
pub struct ChangeSet<K: Ord + for<'de> Deserialize<'de> + Serialize, A: Anchor> { | ||
pub network: Option<Network>, | ||
pub chain: local_chain::ChangeSet, | ||
pub tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>, | ||
} | ||
|
||
impl<K, A> Append for ChangeSet<K, A> | ||
where | ||
K: Ord + for<'de> Deserialize<'de> + Serialize, | ||
A: Anchor, | ||
{ | ||
fn append(&mut self, mut other: Self) { | ||
assert_eq!(self.network, other.network); | ||
self.chain.append(&mut other.chain); | ||
self.tx_graph.append(other.tx_graph); | ||
} | ||
|
||
fn is_empty(&self) -> bool { | ||
self.chain.is_empty() && self.tx_graph.is_empty() | ||
} | ||
} | ||
|
||
/// Error that occurs while reading or writing change sets with the SQLite database. | ||
#[derive(Debug)] | ||
pub enum Error { | ||
/// Invalid network, cannot change the one already stored in the database. | ||
Network { expected: Network, given: Network }, | ||
/// SQLite error. | ||
Sqlite(rusqlite::Error), | ||
} | ||
|
||
impl core::fmt::Display for Error { | ||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
match self { | ||
Self::Network { expected, given } => write!( | ||
f, | ||
"network error trying to read or write change set, expected {}, given {}", | ||
expected, given | ||
), | ||
Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e), | ||
} | ||
} | ||
} | ||
|
||
impl std::error::Error for Error {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
use crate::store::{ReadWrite, Store}; | ||
use crate::ChangeSet; | ||
use anyhow::anyhow; | ||
use bdk_chain::{BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor}; | ||
use bdk_persist::PersistBackend; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
impl<K> PersistBackend<ChangeSet<K, ConfirmationTimeHeightAnchor>> | ||
for Store<K, ConfirmationTimeHeightAnchor> | ||
where | ||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send, | ||
{ | ||
fn write_changes( | ||
&mut self, | ||
changeset: &ChangeSet<K, ConfirmationTimeHeightAnchor>, | ||
) -> anyhow::Result<()> { | ||
self.write(changeset) | ||
.map_err(|e| anyhow!(e).context("unable to write changes to sqlite database")) | ||
} | ||
|
||
fn load_from_persistence( | ||
&mut self, | ||
) -> anyhow::Result<Option<ChangeSet<K, ConfirmationTimeHeightAnchor>>> { | ||
self.read() | ||
.map_err(|e| anyhow!(e).context("unable to read changes from sqlite database")) | ||
} | ||
} | ||
|
||
impl<K> PersistBackend<ChangeSet<K, ConfirmationHeightAnchor>> | ||
for Store<K, ConfirmationHeightAnchor> | ||
where | ||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send, | ||
{ | ||
fn write_changes( | ||
&mut self, | ||
changeset: &ChangeSet<K, ConfirmationHeightAnchor>, | ||
) -> anyhow::Result<()> { | ||
self.write(changeset) | ||
.map_err(|e| anyhow!(e).context("unable to write changes to sqlite database")) | ||
} | ||
|
||
fn load_from_persistence( | ||
&mut self, | ||
) -> anyhow::Result<Option<ChangeSet<K, ConfirmationHeightAnchor>>> { | ||
self.read() | ||
.map_err(|e| anyhow!(e).context("unable to read changes from sqlite database")) | ||
} | ||
} | ||
|
||
impl<K> PersistBackend<ChangeSet<K, BlockId>> for Store<K, BlockId> | ||
where | ||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send, | ||
{ | ||
fn write_changes(&mut self, changeset: &ChangeSet<K, BlockId>) -> anyhow::Result<()> { | ||
self.write(changeset) | ||
.map_err(|e| anyhow!(e).context("unable to write changes to sqlite database")) | ||
} | ||
|
||
fn load_from_persistence(&mut self) -> anyhow::Result<Option<ChangeSet<K, BlockId>>> { | ||
self.read() | ||
.map_err(|e| anyhow!(e).context("unable to read changes from sqlite database")) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
use rusqlite::{named_params, Connection, Error}; | ||
|
||
const SCHEMA_0: &str = include_str!("../schema/schema_0.sql"); | ||
const MIGRATIONS: &[&str] = &[SCHEMA_0]; | ||
|
||
pub(crate) trait Schema { | ||
/// Migrate sqlite db schema to latest version. | ||
fn migrate(conn: &mut Connection) -> Result<(), Error> { | ||
let stmts = &MIGRATIONS | ||
.iter() | ||
.flat_map(|stmt| { | ||
// remove comment lines | ||
let s = stmt | ||
.split('\n') | ||
.filter(|l| !l.starts_with("--") && !l.is_empty()) | ||
.collect::<Vec<_>>() | ||
.join(" "); | ||
// split into statements | ||
s.split(';') | ||
// remove extra spaces | ||
.map(|s| { | ||
s.trim() | ||
.split(' ') | ||
.filter(|s| !s.is_empty()) | ||
.collect::<Vec<_>>() | ||
.join(" ") | ||
}) | ||
.collect::<Vec<_>>() | ||
}) | ||
// remove empty statements | ||
.filter(|s| !s.is_empty()) | ||
.collect::<Vec<String>>(); | ||
|
||
let version = Self::get_schema_version(conn)?; | ||
let stmts = &stmts[(version as usize)..]; | ||
|
||
// begin transaction, all migration statements and new schema version commit or rollback | ||
let tx = conn.transaction()?; | ||
|
||
// execute every statement and return `Some` new schema version | ||
// if execution fails, return `Error::Rusqlite` | ||
// if no statements executed returns `None` | ||
let new_version = stmts | ||
.iter() | ||
.enumerate() | ||
.map(|version_stmt| { | ||
tx.execute(version_stmt.1.as_str(), []) | ||
// map result value to next migration version | ||
.map(|_| version_stmt.0 as i32 + version + 1) | ||
}) | ||
.last() | ||
.transpose()?; | ||
|
||
// if `Some` new statement version, set new schema version | ||
if let Some(version) = new_version { | ||
Self::set_schema_version(&tx, version)?; | ||
} | ||
|
||
// commit transaction | ||
tx.commit()?; | ||
Ok(()) | ||
} | ||
|
||
fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> { | ||
let statement = conn.prepare_cached("SELECT version FROM version"); | ||
match statement { | ||
Err(Error::SqliteFailure(e, Some(msg))) => { | ||
if msg == "no such table: version" { | ||
Ok(0) | ||
} else { | ||
Err(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), | ||
} | ||
} | ||
|
||
fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> { | ||
conn.execute( | ||
"UPDATE version SET version=:version", | ||
named_params! {":version": version}, | ||
) | ||
} | ||
} |
Oops, something went wrong.