feat(sqlite): add bdk_sqlite crate implementing PersistBackend backed by a SQLite database

This commit is contained in:
Steve Myers
2023-09-19 17:29:23 -05:00
parent b8aa76cd05
commit 475c5024ec
13 changed files with 1196 additions and 153 deletions

View File

@@ -45,6 +45,7 @@ dev-getrandom-wasm = ["getrandom/js"]
lazy_static = "1.4"
assert_matches = "1.5.0"
tempfile = "3"
bdk_sqlite = { path = "../sqlite" }
bdk_file_store = { path = "../file_store" }
anyhow = "1"

View File

@@ -466,12 +466,13 @@ impl Wallet {
/// # use bdk_wallet::descriptor::Descriptor;
/// # use bitcoin::key::Secp256k1;
/// # use bdk_wallet::KeychainKind;
/// # use bdk_file_store::Store;
/// # use bdk_sqlite::{Store, rusqlite::Connection};
/// #
/// # fn main() -> Result<(), anyhow::Error> {
/// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
/// # let file_path = temp_dir.path().join("store.db");
/// # let db: Store<bdk_wallet::wallet::ChangeSet> = Store::create_new(&[], &file_path).expect("must create db");
/// # let conn = Connection::open(file_path).expect("must open connection");
/// # let db = Store::new(conn).expect("must create db");
/// let secp = Secp256k1::new();
///
/// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap();

View File

@@ -1,9 +1,12 @@
use std::path::Path;
use std::str::FromStr;
use assert_matches::assert_matches;
use bdk_chain::collections::BTreeMap;
use bdk_chain::COINBASE_MATURITY;
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_persist::PersistBackend;
use bdk_sqlite::rusqlite::Connection;
use bdk_wallet::descriptor::{calc_checksum, IntoWalletDescriptor};
use bdk_wallet::psbt::PsbtUtils;
use bdk_wallet::signer::{SignOptions, SignerError};
@@ -69,166 +72,198 @@ const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
#[test]
fn load_recovers_wallet() {
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join("store.db");
// create new wallet
let wallet_spk_index = {
let db = bdk_file_store::Store::create_new(DB_MAGIC, &file_path).expect("must create db");
let mut wallet = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet)
.expect("must init wallet");
wallet.reveal_next_address(KeychainKind::External).unwrap();
wallet.spk_index().clone()
};
// recover wallet
fn load_recovers_wallet() -> anyhow::Result<()> {
fn run<B, FN, FR>(filename: &str, create_new: FN, recover: FR) -> anyhow::Result<()>
where
B: PersistBackend<bdk_wallet::wallet::ChangeSet> + Send + Sync + 'static,
FN: Fn(&Path) -> anyhow::Result<B>,
FR: Fn(&Path) -> anyhow::Result<B>,
{
let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db");
let wallet = Wallet::load(db).expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
assert_eq!(
wallet.spk_index().keychains().collect::<Vec<_>>(),
wallet_spk_index.keychains().collect::<Vec<_>>()
);
assert_eq!(
wallet.spk_index().last_revealed_indices(),
wallet_spk_index.last_revealed_indices()
);
let secp = Secp256k1::new();
assert_eq!(
*wallet.get_descriptor_for_keychain(KeychainKind::External),
get_test_tr_single_sig_xprv()
.into_wallet_descriptor(&secp, wallet.network())
.unwrap()
.0
);
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename);
// create new wallet
let wallet_spk_index = {
let db = create_new(&file_path).expect("must create db");
let mut wallet = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet)
.expect("must init wallet");
wallet.reveal_next_address(KeychainKind::External).unwrap();
wallet.spk_index().clone()
};
// recover wallet
{
let db = recover(&file_path).expect("must recover db");
let wallet = Wallet::load(db).expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
assert_eq!(
wallet.spk_index().keychains().collect::<Vec<_>>(),
wallet_spk_index.keychains().collect::<Vec<_>>()
);
assert_eq!(
wallet.spk_index().last_revealed_indices(),
wallet_spk_index.last_revealed_indices()
);
let secp = Secp256k1::new();
assert_eq!(
*wallet.get_descriptor_for_keychain(KeychainKind::External),
get_test_tr_single_sig_xprv()
.into_wallet_descriptor(&secp, wallet.network())
.unwrap()
.0
);
}
// `new` can only be called on empty db
{
let db = recover(&file_path).expect("must recover db");
let result = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet);
assert!(matches!(result, Err(NewError::NonEmptyDatabase)));
}
Ok(())
}
// `new` can only be called on empty db
{
let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db");
let result = Wallet::new(get_test_tr_single_sig_xprv(), None, db, Network::Testnet);
assert!(matches!(result, Err(NewError::NonEmptyDatabase)));
}
run(
"store.db",
|path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?),
|path| Ok(bdk_file_store::Store::open(DB_MAGIC, path)?),
)?;
run(
"store.sqlite",
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
)?;
Ok(())
}
#[test]
fn new_or_load() {
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join("store.db");
// init wallet when non-existent
let wallet_keychains: BTreeMap<_, _> = {
let db = bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path)
.expect("must create db");
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
.expect("must init wallet");
wallet.keychains().map(|(k, v)| (*k, v.clone())).collect()
};
// wrong network
fn new_or_load() -> anyhow::Result<()> {
fn run<B, F>(filename: &str, new_or_load: F) -> anyhow::Result<()>
where
B: PersistBackend<bdk_wallet::wallet::ChangeSet> + Send + Sync + 'static,
F: Fn(&Path) -> anyhow::Result<B>,
{
let db =
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
let err = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Bitcoin)
.expect_err("wrong network");
assert!(
matches!(
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename);
// init wallet when non-existent
let wallet_keychains: BTreeMap<_, _> = {
let db = new_or_load(&file_path).expect("must create db");
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
.expect("must init wallet");
wallet.keychains().map(|(k, v)| (*k, v.clone())).collect()
};
// wrong network
{
let db = new_or_load(&file_path).expect("must create db");
let err = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Bitcoin)
.expect_err("wrong network");
assert!(
matches!(
err,
bdk_wallet::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch {
got: Some(Network::Testnet),
expected: Network::Bitcoin
}
),
"err: {}",
err,
bdk_wallet::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch {
got: Some(Network::Testnet),
expected: Network::Bitcoin
}
),
"err: {}",
err,
);
}
);
}
// wrong genesis hash
{
let exp_blockhash = BlockHash::all_zeros();
let got_blockhash =
bitcoin::blockdata::constants::genesis_block(Network::Testnet).block_hash();
// wrong genesis hash
{
let exp_blockhash = BlockHash::all_zeros();
let got_blockhash =
bitcoin::blockdata::constants::genesis_block(Network::Testnet).block_hash();
let db =
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
let err = Wallet::new_or_load_with_genesis_hash(
get_test_wpkh(),
None,
db,
Network::Testnet,
exp_blockhash,
)
.expect_err("wrong genesis hash");
assert!(
matches!(
let db = new_or_load(&file_path).expect("must open db");
let err = Wallet::new_or_load_with_genesis_hash(
get_test_wpkh(),
None,
db,
Network::Testnet,
exp_blockhash,
)
.expect_err("wrong genesis hash");
assert!(
matches!(
err,
bdk_wallet::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected }
if got == Some(got_blockhash) && expected == exp_blockhash
),
"err: {}",
err,
bdk_wallet::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected }
if got == Some(got_blockhash) && expected == exp_blockhash
),
"err: {}",
err,
);
}
);
}
// wrong external descriptor
{
let exp_descriptor = get_test_tr_single_sig();
let got_descriptor = get_test_wpkh()
.into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
.unwrap()
.0;
// wrong external descriptor
{
let exp_descriptor = get_test_tr_single_sig();
let got_descriptor = get_test_wpkh()
.into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
.unwrap()
.0;
let db =
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
let err = Wallet::new_or_load(exp_descriptor, None, db, Network::Testnet)
.expect_err("wrong external descriptor");
assert!(
matches!(
let db = new_or_load(&file_path).expect("must open db");
let err = Wallet::new_or_load(exp_descriptor, None, db, Network::Testnet)
.expect_err("wrong external descriptor");
assert!(
matches!(
err,
bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
if got == &Some(got_descriptor) && keychain == KeychainKind::External
),
"err: {}",
err,
bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
if got == &Some(got_descriptor) && keychain == KeychainKind::External
),
"err: {}",
err,
);
}
);
}
// wrong internal descriptor
{
let exp_descriptor = Some(get_test_tr_single_sig());
let got_descriptor = None;
// wrong internal descriptor
{
let exp_descriptor = Some(get_test_tr_single_sig());
let got_descriptor = None;
let db =
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
let err = Wallet::new_or_load(get_test_wpkh(), exp_descriptor, db, Network::Testnet)
.expect_err("wrong internal descriptor");
assert!(
matches!(
let db = new_or_load(&file_path).expect("must open db");
let err = Wallet::new_or_load(get_test_wpkh(), exp_descriptor, db, Network::Testnet)
.expect_err("wrong internal descriptor");
assert!(
matches!(
err,
bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
if got == &got_descriptor && keychain == KeychainKind::Internal
),
"err: {}",
err,
bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
if got == &got_descriptor && keychain == KeychainKind::Internal
),
"err: {}",
err,
);
);
}
// all parameters match
{
let db = new_or_load(&file_path).expect("must open db");
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
.expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
assert!(wallet
.keychains()
.map(|(k, v)| (*k, v.clone()))
.eq(wallet_keychains));
}
Ok(())
}
// all parameters match
{
let db =
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
.expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
assert!(wallet
.keychains()
.map(|(k, v)| (*k, v.clone()))
.eq(wallet_keychains));
}
run("store.db", |path| {
Ok(bdk_file_store::Store::open_or_create_new(DB_MAGIC, path)?)
})?;
run("store.sqlite", |path| {
Ok(bdk_sqlite::Store::new(Connection::open(path)?)?)
})?;
Ok(())
}
#[test]