diff --git a/src/lib.rs b/src/lib.rs index 4f848a4..0ef114a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ pub enum BdkSqlxError { } /// Manages a pool of database connections. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Store { pub(crate) pool: Pool, wallet_name: String, diff --git a/src/postgres.rs b/src/postgres.rs index b222822..6f3ffa6 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -121,8 +121,6 @@ impl Store { .fetch_optional(&mut *tx) .await?; - dbg!(&row); - if let Some(row) = row { Self::changeset_from_row(&mut tx, &mut changeset, row).await?; } @@ -465,40 +463,6 @@ pub async fn local_chain_changeset_persist_to_postgres( Ok(()) } -/// Drops all tables. -#[tracing::instrument] -pub async fn drop_all(db: Pool) -> Result<(), BdkSqlxError> { - info!("Dropping all tables"); - - let drop_statements = vec![ - "DROP TABLE IF EXISTS _sqlx_migrations", - "DROP TABLE IF EXISTS vault_addresses", - "DROP TABLE IF EXISTS used_anchorwatch_keys", - "DROP TABLE IF EXISTS anchorwatch_keys", - "DROP TABLE IF EXISTS psbts", - "DROP TABLE IF EXISTS whitelist_update", - "DROP TABLE IF EXISTS vault_parameters", - "DROP TABLE IF EXISTS users", - "DROP TABLE IF EXISTS version", - "DROP TABLE IF EXISTS anchor_tx", - "DROP TABLE IF EXISTS txout", - "DROP TABLE IF EXISTS tx", - "DROP TABLE IF EXISTS block", - "DROP TABLE IF EXISTS keychain", - "DROP TABLE IF EXISTS network", - ]; - - let mut tx = db.begin().await?; - - for statement in drop_statements { - sqlx::query(statement).execute(&mut *tx).await?; - } - - tx.commit().await?; - - Ok(()) -} - /// Collects information on all the wallets in the database and dumps it to stdout. #[tracing::instrument] pub async fn easy_backup(db: Pool) -> Result<(), BdkSqlxError> { diff --git a/src/test.rs b/src/test.rs index 76668fb..227480b 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,9 +1,8 @@ #![allow(unused)] -use crate::{drop_all, Store}; +use crate::{BdkSqlxError, FutureResult, Store}; use assert_matches::assert_matches; use bdk_chain::bitcoin::constants::ChainHash; use bdk_chain::bitcoin::hashes::Hash; -use crate::{postgres::drop_all, Store}; use bdk_chain::bitcoin::secp256k1::Secp256k1; use bdk_chain::bitcoin::Network::Signet; use bdk_chain::bitcoin::{BlockHash, Network}; @@ -13,19 +12,22 @@ use bdk_electrum::{electrum_client, BdkElectrumClient}; use bdk_testenv::bitcoincore_rpc::RpcApi; use bdk_testenv::TestEnv; use bdk_wallet::{ + descriptor, descriptor::ExtendedDescriptor, - wallet_name_from_descriptor, + wallet_name_from_descriptor, AsyncWalletPersister, ChangeSet, KeychainKind::{self, *}, LoadError, LoadMismatch, LoadWithPersistError, Wallet, }; use better_panic::Settings; +use sqlx::{Database, PgPool, Pool, Postgres, Sqlite, SqlitePool}; use std::collections::HashSet; -use sqlx::{PgPool, Postgres, Sqlite, SqlitePool}; use std::env; use std::io::Write; -use std::time::Duration; +use std::ops::Deref; use std::sync::Arc; use std::sync::Once; +use std::time::Duration; +use tracing::info; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; @@ -56,7 +58,7 @@ fn parse_descriptor(s: &str) -> ExtendedDescriptor { static INIT: Once = Once::new(); // This must only be called once. -pub fn initialize() { +fn initialize() { INIT.call_once(|| { tracing_subscriber::registry() .with(EnvFilter::new( @@ -68,9 +70,107 @@ pub fn initialize() { }); } +trait DropAll { + async fn drop_all(&self) -> anyhow::Result<()>; +} + +impl DropAll for Pool { + /// Drops all tables. + /// + /// Clean up (optional, depending on your test database strategy) + /// You might want to delete the test wallet from the database here. + #[tracing::instrument] + async fn drop_all(&self) -> anyhow::Result<()> { + let drop_statements = vec![ + "DROP TABLE IF EXISTS _sqlx_migrations", + "DROP TABLE IF EXISTS vault_addresses", + "DROP TABLE IF EXISTS used_anchorwatch_keys", + "DROP TABLE IF EXISTS anchorwatch_keys", + "DROP TABLE IF EXISTS psbts", + "DROP TABLE IF EXISTS whitelist_update", + "DROP TABLE IF EXISTS vault_parameters", + "DROP TABLE IF EXISTS users", + "DROP TABLE IF EXISTS version", + "DROP TABLE IF EXISTS anchor_tx", + "DROP TABLE IF EXISTS txout", + "DROP TABLE IF EXISTS tx", + "DROP TABLE IF EXISTS block", + "DROP TABLE IF EXISTS keychain", + "DROP TABLE IF EXISTS network", + ]; + + let mut tx = self.begin().await?; + + for statement in drop_statements { + sqlx::query(statement).execute(&mut *tx).await?; + } + + tx.commit().await?; + + Ok(()) + } +} + +#[derive(Debug)] +enum TestStore { + Postgres(Store), + Sqlite(Store), +} + +impl AsyncWalletPersister for TestStore { + type Error = BdkSqlxError; + + #[tracing::instrument] + fn initialize<'a>(store: &'a mut Self) -> FutureResult<'a, ChangeSet, Self::Error> + where + Self: 'a, + { + info!("initialize test store"); + match store { + TestStore::Postgres(store) => Box::pin(store.migrate_and_read()), + TestStore::Sqlite(store) => Box::pin(store.migrate_and_read()), + } + } + + #[tracing::instrument] + fn persist<'a>( + store: &'a mut Self, + changeset: &'a ChangeSet, + ) -> FutureResult<'a, (), Self::Error> + where + Self: 'a, + { + info!("persist test store"); + match store { + TestStore::Postgres(store) => Box::pin(store.write(changeset)), + TestStore::Sqlite(store) => Box::pin(store.write(changeset)), + } + } +} + +async fn create_test_stores(wallet_name: String) -> anyhow::Result> { + let mut stores: Vec = Vec::new(); + + // Set up postgres database URL (you might want to use a test-specific database) + let url = env::var("DATABASE_TEST_URL").expect("DATABASE_TEST_URL must be set for tests"); + let pool = Pool::::connect(&url.clone()).await?; + // Drop all before creating new store for testing + pool.drop_all().await?; + let postgres_store = + Store::::new_with_url(url.clone(), Some(wallet_name.clone())).await?; + stores.push(TestStore::Postgres(postgres_store)); + + // Setup sqlite in-memory database + let pool = SqlitePool::connect(":memory:").await?; + let sqlite_store = Store::::new(pool.clone(), Some(wallet_name.clone()), true).await?; + stores.push(TestStore::Sqlite(sqlite_store)); + + Ok(stores) +} + #[tracing::instrument] #[tokio::test] -async fn wallet_is_persisted_postgres() -> anyhow::Result<()> { +async fn wallet_is_persisted() -> anyhow::Result<()> { Settings::debug() .most_recent_first(false) .lineno_suffix(true) @@ -78,12 +178,6 @@ async fn wallet_is_persisted_postgres() -> anyhow::Result<()> { initialize(); - // Set up the database URL (you might want to use a test-specific database) - let url = env::var("DATABASE_TEST_URL").expect("DATABASE_TEST_URL must be set for tests"); - let pg = PgPool::connect(&url.clone()).await?; - drop_all(pg).await?; - println!("tables dropped"); - // Define descriptors (you may need to adjust these based on your exact requirements) let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); // Generate a unique name for this test wallet @@ -94,55 +188,51 @@ async fn wallet_is_persisted_postgres() -> anyhow::Result<()> { &Secp256k1::new(), )?; - // Create a new wallet - let mut store = Store::::new_with_url(url.clone(), Some(wallet_name.clone())).await?; - let mut wallet = Wallet::create(external_desc, internal_desc) - .network(NETWORK) - .create_wallet_async(&mut store) - .await?; - - let external_addr0 = wallet.reveal_next_address(External); - for keychain in [External, Internal] { - let _ = wallet.reveal_addresses_to(keychain, 2); - } - - assert!(wallet.persist_async(&mut store).await?); - let wallet_spk_index = wallet.spk_index(); - - { - // Recover the wallet - let mut store = Store::::new_with_url(url.clone(), Some(wallet_name)).await?; - let wallet = Wallet::load() - .descriptor(External, Some(external_desc)) - .descriptor(Internal, Some(internal_desc)) - .load_wallet_async(&mut store) - .await? - .expect("wallet must exist"); - - assert_eq!(wallet.network(), NETWORK); - assert_eq!( - wallet.spk_index().keychains().collect::>(), - wallet_spk_index.keychains().collect::>() - ); - assert_eq!( - wallet.spk_index().last_revealed_indices(), - wallet_spk_index.last_revealed_indices() - ); + let stores = create_test_stores(wallet_name).await?; + for mut store in stores { + // Create a new wallet + let mut wallet = Wallet::create(external_desc, internal_desc) + .network(NETWORK) + .create_wallet_async(&mut store) + .await?; + + let external_addr0 = wallet.reveal_next_address(External); + for keychain in [External, Internal] { + let _ = wallet.reveal_addresses_to(keychain, 2); + } - let recovered_addr = wallet.peek_address(External, 0); - assert_eq!(recovered_addr, external_addr0, "failed to recover address"); + assert!(wallet.persist_async(&mut store).await?); + let wallet_spk_index = wallet.spk_index(); - assert_eq!( - wallet.public_descriptor(External).to_string(), - "tr(tpubD6NzVbkrYhZ4WgCeJid2Zds24zATB58r1q1qTLMuApUxZUxzETADNTeP6SvZKSsXs4qhvFAC21GFjXHwgxAcDtZqzzj8JMpsFDgqyjSJHGa/0/*)#celxt6vn".to_string(), - ); + { + // Recover the wallet + let wallet = Wallet::load() + .descriptor(External, Some(external_desc)) + .descriptor(Internal, Some(internal_desc)) + .load_wallet_async(&mut store) + .await? + .expect("wallet must exist"); + + assert_eq!(wallet.network(), NETWORK); + assert_eq!( + wallet.spk_index().keychains().collect::>(), + wallet_spk_index.keychains().collect::>() + ); + assert_eq!( + wallet.spk_index().last_revealed_indices(), + wallet_spk_index.last_revealed_indices() + ); + + let recovered_addr = wallet.peek_address(External, 0); + assert_eq!(recovered_addr, external_addr0, "failed to recover address"); + + assert_eq!( + wallet.public_descriptor(External).to_string(), + "tr(tpubD6NzVbkrYhZ4WgCeJid2Zds24zATB58r1q1qTLMuApUxZUxzETADNTeP6SvZKSsXs4qhvFAC21GFjXHwgxAcDtZqzzj8JMpsFDgqyjSJHGa/0/*)#celxt6vn".to_string(), + ); + } } - // Clean up (optional, depending on your test database strategy) - // You might want to delete the test wallet from the database here - let db = PgPool::connect(&url).await?; - drop_all(db).await.expect("hope its not mainnet"); - Ok(()) } @@ -154,19 +244,6 @@ async fn wallet_load_checks() -> anyhow::Result<()> { .lineno_suffix(true) .install(); - // Set up the database URL (you might want to use a test-specific database) - let url = env::var("DATABASE_TEST_URL").expect("DATABASE_TEST_URL must be set for tests"); - - let pg = PgPool::connect(&url.clone()).await?; - match drop_all(pg).await { - Ok(_) => { - dbg!("tables dropped") - } - Err(_) => { - dbg!("Error dropping tables") - } - }; - // Define descriptors (you may need to adjust these based on your exact requirements) let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); let parsed_ext = parse_descriptor(external_desc); @@ -179,52 +256,49 @@ async fn wallet_load_checks() -> anyhow::Result<()> { &Secp256k1::new(), )?; - // Create a new wallet - let mut store = Store::new_with_url(url.clone(), Some(wallet_name)).await?; - let mut wallet = Wallet::create(external_desc, internal_desc) - .network(NETWORK) - .create_wallet_async(&mut store) - .await?; - - { - assert_matches!( - Wallet::load() - .descriptor(External, Some(internal_desc)) - .load_wallet_async(&mut store) - .await, - Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( - LoadMismatch::Descriptor { keychain, loaded, expected } - ))) - if keychain == External && loaded == Some(parsed_ext.clone()) && expected == Some(parsed_int), - "should error on wrong external descriptor" - ); - } - { - assert_matches!( - Wallet::load() - .descriptor(External, Option::<&str>::None) - .load_wallet_async(&mut store) - .await, - Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( - LoadMismatch::Descriptor { keychain, loaded, expected } - ))) - if keychain == External && loaded == Some(parsed_ext) && expected.is_none(), - "external descriptor check should error when expected is none" - ); - } - { - let mainnet_hash = BlockHash::from_byte_array(ChainHash::BITCOIN.to_bytes()); - assert_matches!( - Wallet::load().check_genesis_hash(mainnet_hash).load_wallet_async(&mut store).await - , Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Genesis { .. }))), - "unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)"); + let stores = create_test_stores(wallet_name).await?; + for mut store in stores { + // Create a new wallet + let mut wallet = Wallet::create(external_desc, internal_desc) + .network(NETWORK) + .create_wallet_async(&mut store) + .await?; + + { + assert_matches!( + Wallet::load() + .descriptor(External, Some(internal_desc)) + .load_wallet_async(&mut store) + .await, + Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( + LoadMismatch::Descriptor { keychain, loaded, expected } + ))) + if keychain == External && loaded == Some(parsed_ext.clone()) && expected == Some(parsed_int.clone()), + "should error on wrong external descriptor" + ); + } + { + assert_matches!( + Wallet::load() + .descriptor(External, Option::<&str>::None) + .load_wallet_async(&mut store) + .await, + Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( + LoadMismatch::Descriptor { keychain, loaded, expected } + ))) + if keychain == External && loaded == Some(parsed_ext.clone()) && expected.is_none(), + "external descriptor check should error when expected is none" + ); + } + { + let mainnet_hash = BlockHash::from_byte_array(ChainHash::BITCOIN.to_bytes()); + assert_matches!( + Wallet::load().check_genesis_hash(mainnet_hash).load_wallet_async(&mut store).await + , Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Genesis { .. }))), + "unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)"); + } } - // Clean up (optional, depending on your test database strategy) - // You might want to delete the test wallet from the database here - let db = PgPool::connect(&url).await?; - drop_all(db).await.expect("hope its not mainnet"); - Ok(()) } @@ -235,18 +309,6 @@ async fn single_descriptor_wallet_persist_and_recover() -> anyhow::Result<()> { .most_recent_first(false) .lineno_suffix(true) .install(); - // Set up the database URL (you might want to use a test-specific database) - let url = env::var("DATABASE_TEST_URL").expect("DATABASE_TEST_URL must be set for tests"); - - let pg = PgPool::connect(&url.clone()).await?; - match drop_all(pg).await { - Ok(_) => { - dbg!("tables dropped") - } - Err(_) => { - dbg!("Error dropping tables") - } - }; // Define descriptors let desc = get_test_tr_single_sig_xprv(); @@ -254,42 +316,40 @@ async fn single_descriptor_wallet_persist_and_recover() -> anyhow::Result<()> { // Generate a unique name for this test wallet let wallet_name = wallet_name_from_descriptor(desc, Some(desc), NETWORK, &Secp256k1::new())?; - // Create a new wallet - let mut store = Store::new_with_url(url.clone(), Some(wallet_name)).await?; - let mut wallet = Wallet::create_single(desc) - .network(NETWORK) - .create_wallet_async(&mut store) - .await?; - - let _ = wallet.reveal_addresses_to(External, 2); - assert!(wallet.persist_async(&mut store).await?); - - { - // Recover the wallet - let secp = wallet.secp_ctx(); - let wallet = Wallet::load().load_wallet_async(&mut store).await?.unwrap(); - assert_eq!(wallet.derivation_index(External), Some(2)); - } - { - // should error on wrong internal params - let desc = get_test_wpkh(); - let exp_desc = parse_descriptor(desc); - let err = Wallet::load() - .descriptor(Internal, Some(desc)) - .load_wallet_async(&mut store) - .await; - assert_matches!( - err, - Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Descriptor { keychain, loaded, expected }))) - if keychain == Internal && loaded.is_none() && expected == Some(exp_desc), - "single descriptor wallet should refuse change descriptor param" - ); + let stores = create_test_stores(wallet_name).await?; + for mut store in stores { + // Create a new wallet + let mut wallet = Wallet::create_single(desc) + .network(NETWORK) + .create_wallet_async(&mut store) + .await?; + + let _ = wallet.reveal_addresses_to(External, 2); + assert!(wallet.persist_async(&mut store).await?); + + { + // Recover the wallet + let secp = wallet.secp_ctx(); + let wallet = Wallet::load().load_wallet_async(&mut store).await?.unwrap(); + assert_eq!(wallet.derivation_index(External), Some(2)); + } + { + // should error on wrong internal params + let desc = get_test_wpkh(); + let exp_desc = parse_descriptor(desc); + let err = Wallet::load() + .descriptor(Internal, Some(desc)) + .load_wallet_async(&mut store) + .await; + assert_matches!( + err, + Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Descriptor { keychain, loaded, expected }))) + if keychain == Internal && loaded.is_none() && expected == Some(exp_desc), + "single descriptor wallet should refuse change descriptor param" + ); + } } - // Clean up (optional, depending on your test database strategy) - // You might want to delete the test wallet from the database here - let db = PgPool::connect(&url).await?; - drop_all(db).await.expect("hope its not mainnet"); Ok(()) } @@ -300,18 +360,6 @@ async fn two_wallets_load() -> anyhow::Result<()> { .most_recent_first(false) .lineno_suffix(true) .install(); - // Set up the database URL (you might want to use a test-specific database) - let url = env::var("DATABASE_TEST_URL").expect("DATABASE_TEST_URL must be set for tests"); - - let pg = PgPool::connect(&url.clone()).await?; - match drop_all(pg).await { - Ok(_) => { - dbg!("tables dropped") - } - Err(_) => { - dbg!("Error dropping tables") - } - }; // Define descriptors let (external_desc_wallet_1, internal_desc_wallet_1) = @@ -333,195 +381,110 @@ async fn two_wallets_load() -> anyhow::Result<()> { &Secp256k1::new(), )?; - // Create wallets - let mut store_1 = Store::new_with_url(url.clone(), Some(wallet_1_name)).await?; - let mut store_2 = Store::new_with_url(url.clone(), Some(wallet_2_name)).await?; - - let mut wallet_1 = Wallet::create(external_desc_wallet_1, internal_desc_wallet_1) - .network(NETWORK) - .create_wallet_async(&mut store_1) - .await?; - let _ = wallet_1.reveal_next_address(External); - let _ = wallet_1.reveal_next_address(Internal); - assert!(wallet_1.persist_async(&mut store_1).await?); - - // for wallet 2 we reveal an extra internal address and insert a new checkpoint - // to check that loading returns the correct data for each wallet - let mut wallet_2 = Wallet::create(external_desc_wallet_2, internal_desc_wallet_2) - .network(NETWORK) - .create_wallet_async(&mut store_2) - .await?; - let _ = wallet_2.reveal_next_address(External); - let _ = wallet_2.reveal_addresses_to(Internal, 2); - let block = BlockId { - height: 100, - hash: BlockHash::all_zeros(), - }; - let _ = wallet_2.insert_checkpoint(block).unwrap(); - assert!(wallet_2.persist_async(&mut store_2).await?); - - // Recover the wallet_1 - let wallet_1 = Wallet::load() - .load_wallet_async(&mut store_1) - .await? - .unwrap(); - - // Recover the wallet_2 - let wallet_2 = Wallet::load() - .load_wallet_async(&mut store_2) - .await? - .unwrap(); - - assert_eq!( - wallet_1.derivation_index(External), - wallet_2.derivation_index(External) - ); - // FIXME: see https://github.com/bitcoindevkit/bdk-sqlx/pull/10 - // assert_ne!( - // wallet_1.derivation_index(Internal), - // wallet_2.derivation_index(Internal), - // "different wallets should not have same derivation index" - // ); - // assert_ne!( - // wallet_1.latest_checkpoint(), - // wallet_2.latest_checkpoint(), - // "different wallets should not have same chain tip" - // ); - - // Clean up (optional, depending on your test database strategy) - // You might want to delete the test wallet from the database here - let db = PgPool::connect(&url).await?; - drop_all(db).await.expect("hope its not mainnet"); - Ok(()) -} - -#[tracing::instrument] -#[tokio::test] -async fn sync_with_electrum() -> anyhow::Result<()> { - Settings::debug() - .most_recent_first(false) - .lineno_suffix(true) - .install(); - - // Set up the database URL (you might want to use a test-specific database) - let url = env::var("DATABASE_TEST_URL").expect("DATABASE_TEST_URL must be set for tests"); - - let pg = PgPool::connect(&url.clone()).await?; - match drop_all(pg).await { - Ok(_) => { - dbg!("tables dropped") - } - Err(_) => { - dbg!("Error dropping tables") - } - }; + let mut stores1 = create_test_stores(wallet_1_name).await?; + let mut stores2 = create_test_stores(wallet_2_name).await?; + + for _ in 0..stores1.len() { + let mut store_1 = stores1.pop().unwrap(); + let mut store_2 = stores2.pop().unwrap(); + + let mut wallet_1 = Wallet::create(external_desc_wallet_1, internal_desc_wallet_1) + .network(NETWORK) + .create_wallet_async(&mut store_1) + .await?; + let _ = wallet_1.reveal_next_address(External); + let _ = wallet_1.reveal_next_address(Internal); + assert!(wallet_1.persist_async(&mut store_1).await?); + + // for wallet 2 we reveal an extra internal address and insert a new checkpoint + // to check that loading returns the correct data for each wallet + let mut wallet_2 = Wallet::create(external_desc_wallet_2, internal_desc_wallet_2) + .network(NETWORK) + .create_wallet_async(&mut store_2) + .await?; + let _ = wallet_2.reveal_next_address(External); + let _ = wallet_2.reveal_addresses_to(Internal, 2); + let block = BlockId { + height: 100, + hash: BlockHash::all_zeros(), + }; + let _ = wallet_2.insert_checkpoint(block).unwrap(); + assert!(wallet_2.persist_async(&mut store_2).await?); + + // Recover the wallet_1 + let wallet_1 = Wallet::load() + .load_wallet_async(&mut store_1) + .await? + .unwrap(); - // Define descriptors (you may need to adjust these based on your exact requirements) - let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); - // Generate a unique name for this test wallet - let wallet_name = wallet_name_from_descriptor( - external_desc, - Some(internal_desc), - Network::Regtest, - &Secp256k1::new(), - )?; + // Recover the wallet_2 + let wallet_2 = Wallet::load() + .load_wallet_async(&mut store_2) + .await? + .unwrap(); - let mut store = Store::new_with_url(url.clone(), Some(wallet_name)).await?; - let mut wallet = Wallet::create(external_desc, internal_desc) - .network(Network::Regtest) - .create_wallet_async(&mut store) - .await?; - - // mine blocks and sync with electrum - let env = TestEnv::new()?; - let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; - let client = BdkElectrumClient::new(electrum_client); - let _hashes = env.mine_blocks(9, None)?; - env.wait_until_electrum_sees_block(Duration::from_secs(10))?; - let new_tip_height: u32 = env.rpc_client().get_block_count()?.try_into().unwrap(); - assert_eq!(new_tip_height, 10); - - let request = wallet.start_full_scan(); - let update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; - wallet.apply_update(update)?; - assert!(wallet.persist_async(&mut store).await?); - - // Recover the wallet - let wallet = Wallet::load().load_wallet_async(&mut store).await?.unwrap(); - assert_eq!(wallet.latest_checkpoint().height(), new_tip_height); - - let db = PgPool::connect(&url).await?; - drop_all(db).await.expect("hope its not mainnet"); + assert_eq!( + wallet_1.derivation_index(External), + wallet_2.derivation_index(External) + ); + // FIXME: see https://github.com/bitcoindevkit/bdk-sqlx/pull/10 + // assert_ne!( + // wallet_1.derivation_index(Internal), + // wallet_2.derivation_index(Internal), + // "different wallets should not have same derivation index" + // ); + // assert_ne!( + // wallet_1.latest_checkpoint(), + // wallet_2.latest_checkpoint(), + // "different wallets should not have same chain tip" + // ); + } Ok(()) } -// This test uses the in-memory sqlite DB so tables don't need to be dropped at the end. #[tracing::instrument] #[tokio::test] -async fn wallet_is_persisted_sqlite() -> anyhow::Result<()> { +async fn sync_with_electrum() -> anyhow::Result<()> { Settings::debug() .most_recent_first(false) .lineno_suffix(true) .install(); - initialize(); - - // Set up the database URL (you might want to use a test-specific database) - let pool = SqlitePool::connect(":memory:").await?; - // Define descriptors (you may need to adjust these based on your exact requirements) let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); // Generate a unique name for this test wallet let wallet_name = wallet_name_from_descriptor( external_desc, Some(internal_desc), - NETWORK, + Network::Regtest, &Secp256k1::new(), )?; - // Create a new wallet, use sqlite in memory DB - let mut store = Store::::new(pool.clone(), Some(wallet_name.clone()), true).await?; - let mut wallet = Wallet::create(external_desc, internal_desc) - .network(NETWORK) - .create_wallet_async(&mut store) - .await?; - - let external_addr0 = wallet.reveal_next_address(KeychainKind::External); - for keychain in [External, Internal] { - let _ = wallet.reveal_addresses_to(keychain, 2); - } - - assert!(wallet.persist_async(&mut store).await?); - let wallet_spk_index = wallet.spk_index(); + let stores = create_test_stores(wallet_name).await?; + for mut store in stores { + let mut wallet = Wallet::create(external_desc, internal_desc) + .network(Network::Regtest) + .create_wallet_async(&mut store) + .await?; + + // mine blocks and sync with electrum + let env = TestEnv::new()?; + let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?; + let client = BdkElectrumClient::new(electrum_client); + let _hashes = env.mine_blocks(9, None)?; + env.wait_until_electrum_sees_block(Duration::from_secs(10))?; + let new_tip_height: u32 = env.rpc_client().get_block_count()?.try_into().unwrap(); + assert_eq!(new_tip_height, 10); + + let request = wallet.start_full_scan(); + let update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; + wallet.apply_update(update)?; + assert!(wallet.persist_async(&mut store).await?); - { // Recover the wallet - let mut store = Store::::new(pool, Some(wallet_name), false).await?; - let wallet = Wallet::load() - .descriptor(KeychainKind::External, Some(external_desc)) - .descriptor(KeychainKind::Internal, Some(internal_desc)) - .load_wallet_async(&mut store) - .await? - .expect("wallet must exist"); - - assert_eq!(wallet.network(), NETWORK); - assert_eq!( - wallet.spk_index().keychains().collect::>(), - wallet_spk_index.keychains().collect::>() - ); - assert_eq!( - wallet.spk_index().last_revealed_indices(), - wallet_spk_index.last_revealed_indices() - ); - - let recovered_addr = wallet.peek_address(KeychainKind::External, 0); - assert_eq!(recovered_addr, external_addr0, "failed to recover address"); - - assert_eq!( - wallet.public_descriptor(KeychainKind::External).to_string(), - "tr(tpubD6NzVbkrYhZ4WgCeJid2Zds24zATB58r1q1qTLMuApUxZUxzETADNTeP6SvZKSsXs4qhvFAC21GFjXHwgxAcDtZqzzj8JMpsFDgqyjSJHGa/0/*)#celxt6vn".to_string(), - ); + let wallet = Wallet::load().load_wallet_async(&mut store).await?.unwrap(); + assert_eq!(wallet.latest_checkpoint().height(), new_tip_height); } Ok(())