diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c5058d6..4be0c63 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -13,10 +13,10 @@ env: CARGO_TERM_COLOR: auto PGPASSWORD: password DATABASE_TEST_URL: postgres://postgres:password@localhost:5432/testdb - + DATABASE_URL: postgres://postgres:password@localhost:5432/testdb jobs: - wallet-is-persisted: - name: Test wallet persistence + all-tests: + name: Run all wallet tests runs-on: ubuntu-latest services: postgres: @@ -35,141 +35,59 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - - name: Create database + + - name: Install dependencies run: | sudo apt-get install libpq-dev -y - psql -h localhost -p 5432 -U postgres -d postgres -c 'create user testuser' - psql -h localhost -p 5432 -U postgres -d postgres -c 'create database testdb with owner = testuser' - - name: Test wallet_is_persisted - run: cargo test wallet_is_persisted -- --show-output + # Install sqlx-cli + cargo install sqlx-cli --no-default-features --features native-tls,postgres - test-three-wallets: - name: Test three wallets list transactions - runs-on: ubuntu-latest - services: - postgres: - image: postgres:14 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - POSTGRES_DB: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Create database + - name: Create and prepare database run: | - sudo apt-get install libpq-dev -y psql -h localhost -p 5432 -U postgres -d postgres -c 'create user testuser' psql -h localhost -p 5432 -U postgres -d postgres -c 'create database testdb with owner = testuser' - - name: Test test_three_wallets_list_transactions - run: cargo test test_three_wallets_list_transactions -- --show-output - wallet-load-checks: - name: Test wallet load checks - runs-on: ubuntu-latest - services: - postgres: - image: postgres:14 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - POSTGRES_DB: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Create database + - name: Run all wallet tests run: | - sudo apt-get install libpq-dev -y - psql -h localhost -p 5432 -U postgres -d postgres -c 'create user testuser' - psql -h localhost -p 5432 -U postgres -d postgres -c 'create database testdb with owner = testuser' - - name: Test wallet_load_checks - run: cargo test wallet_load_checks -- --show-output + # Test 1: wallet_is_persisted + sqlx migrate run --source migrations/postgres + cargo test wallet_is_persisted -- --show-output + sqlx migrate revert --all + + # Test 2: test_three_wallets_list_transactions + sqlx migrate run --source migrations/postgres + cargo test test_three_wallets_list_transactions -- --show-output + sqlx migrate revert --all + + # Test 3: wallet_load_checks + sqlx migrate run --source migrations/postgres + cargo test wallet_load_checks -- --show-output + sqlx migrate revert --all + + # Test 4: single_descriptor_wallet_persist_and_recover + sqlx migrate run --source migrations/postgres + cargo test single_descriptor_wallet_persist_and_recover -- --show-output + sqlx migrate revert --all + + # Test 5: two_wallets_load + sqlx migrate run --source migrations/postgres + cargo test two_wallets_load -- --show-output + sqlx migrate revert --all - single-descriptor-wallet: - name: Test single descriptor wallet - runs-on: ubuntu-latest - services: - postgres: - image: postgres:14 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - POSTGRES_DB: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Create database + - name: Check fmt and run clippy run: | - sudo apt-get install libpq-dev -y - psql -h localhost -p 5432 -U postgres -d postgres -c 'create user testuser' - psql -h localhost -p 5432 -U postgres -d postgres -c 'create database testdb with owner = testuser' - - name: Test single_descriptor_wallet_persist_and_recover - run: cargo test single_descriptor_wallet_persist_and_recover -- --show-output + cargo fmt --all -- --check + cargo clippy --all-targets -- -Dwarnings - two-wallets-load: - name: Test two wallets load - runs-on: ubuntu-latest - services: - postgres: - image: postgres:14 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - POSTGRES_DB: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Create database + - name: Security Audit run: | - sudo apt-get install libpq-dev -y - psql -h localhost -p 5432 -U postgres -d postgres -c 'create user testuser' - psql -h localhost -p 5432 -U postgres -d postgres -c 'create database testdb with owner = testuser' - - name: Test two_wallets_load - run: cargo test two_wallets_load -- --show-output + cargo install cargo-audit + cargo audit - fmt-clippy: - name: Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - uses: Swatinem/rust-cache@v2 - - name: Check fmt - run: cargo fmt --all -- --check - - name: Clippy - run: cargo clippy --all-targets -- -Dwarnings \ No newline at end of file + - name: Check Unused Dependencies + run: | + cargo install cargo-machete + cargo machete diff --git a/Cargo.toml b/Cargo.toml index e181ee1..cb14c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,17 +7,16 @@ edition = "2021" bdk_wallet = { version = "1.2.0", features = ["test-utils"] } serde = { version = "1.0.208", features = ["derive"] } serde_json = "1.0.125" -sqlx = { version = "0.8.1", default-features = false, features = ["runtime-tokio", "tls-rustls-ring","derive", "postgres", "sqlite", "json", "chrono", "uuid", "sqlx-macros", "migrate"] } -thiserror = "1" -tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } +sqlx = { version = "0.8.5", default-features = false, features = ["runtime-tokio","migrate", "tls-rustls-ring", "derive", "postgres", "sqlite", "json", "chrono", "uuid"] } +thiserror = "2" +tokio = { version = "1.44.0", features = ["macros", "rt-multi-thread"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde_json", "json"] } -sqlx-postgres-tester = "0.1.1" [dev-dependencies] assert_matches = "1.5.0" -anyhow = "1.0.89" -bdk_electrum = { version = "0.20.1"} +anyhow = "1.0.98" +bdk_electrum = { version = "0.21.0"} rustls = "0.23.14" [[example]] diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..5420758 --- /dev/null +++ b/Justfile @@ -0,0 +1,25 @@ +# PostgreSQL commands +start-postgres: + docker run -d --name postgres \ + -p 5432:5432 \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=password \ + -e POSTGRES_DB=mydatabase \ + postgres:15 + +test-postgres: + PGPASSWORD=password psql -h localhost -p 5432 -U postgres -d mydatabase -c "SELECT 1" + +stop-postgres: + docker stop postgres && docker rm postgres + +# DATABASE_URL: postgres://postgres:password@localhost:5432/mydatabase +example: + cargo run --example bdk_sqlx_postgres + +# Database migration +run-migrations: + sqlx migrate run --source migrations/postgres + +undo-migrations: + sqlx migrate revert --all diff --git a/audit.toml b/audit.toml new file mode 100644 index 0000000..0335a34 --- /dev/null +++ b/audit.toml @@ -0,0 +1,2 @@ +[advisories] +ignore = ["RUSTSEC-2023-0071"] \ No newline at end of file diff --git a/examples/bdk_sqlx_postgres.rs b/examples/bdk_sqlx_postgres.rs index 0961e0b..1225102 100644 --- a/examples/bdk_sqlx_postgres.rs +++ b/examples/bdk_sqlx_postgres.rs @@ -3,8 +3,9 @@ use std::collections::HashSet; use std::io::Write; use bdk_electrum::{electrum_client, BdkElectrumClient}; +use bdk_sqlx::pg_store_builder::PgStoreBuilder; use bdk_sqlx::sqlx::Postgres; -use bdk_sqlx::{PgStoreBuilder, Store}; +use bdk_sqlx::Store; use bdk_wallet::bitcoin::secp256k1::Secp256k1; use bdk_wallet::bitcoin::Network; use bdk_wallet::{KeychainKind, PersistedWallet, Wallet}; @@ -12,7 +13,6 @@ use rustls::crypto::ring::default_provider; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; - // Create and persist a BDK wallet to postgres. // wallet 1 @@ -59,8 +59,8 @@ async fn main() -> anyhow::Result<()> { let mut store = PgStoreBuilder::new(wallet_name.clone()) .network(NETWORK) - .migrate(true) - .build_with_url(&url) + .url(&url) + .build() .await?; let mut wallet = match Wallet::load().load_wallet_async(&mut store).await? { @@ -90,8 +90,8 @@ async fn main() -> anyhow::Result<()> { let mut store = PgStoreBuilder::new(wallet_name.clone()) .network(NETWORK) - .migrate(true) - .build_with_url(&url) + .url(&url) + .build() .await?; let mut wallet = match Wallet::load().load_wallet_async(&mut store).await? { diff --git a/src/lib.rs b/src/lib.rs index ae613ac..ae4b1ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,18 +5,57 @@ mod postgres; mod sqlite; +/// Builder for Store +pub mod pg_store_builder; #[cfg(test)] mod test; -use std::future::Future; -use std::pin::Pin; - use bdk_wallet::bitcoin; use bdk_wallet::bitcoin::Network; use bdk_wallet::chain::miniscript; pub use sqlx; +use sqlx::Database; use sqlx::Pool; -use sqlx::{Database, PgPool}; +use std::future::Future; +use std::pin::Pin; +use std::sync::OnceLock; +use tracing::warn; + +/// Result type for bdk-sqlx +pub type Result = core::result::Result; + +/// Thread-safe storage for the network configuration that's shared across all Store instances. +/// This ensures consistent network validation across multiple threads. +static NETWORK: OnceLock = OnceLock::new(); + +/// Retrieves the current global network configuration for validation operations. +/// +/// Returns the current network configuration or an error if not initialized. +fn get_network() -> Result { + NETWORK + .get() + .copied() + .ok_or_else(|| BdkSqlxError::GetNetworkFailure) +} + +/// Sets the global network configuration to ensure consistent validation across threads. +/// +/// Returns an error if the network is already initialized with a different network. +fn initialize_network(network: Network) -> Result<()> { + match NETWORK.get() { + Some(current) if *current == network => { + warn!("initialize_network called more than once"); + Ok(()) + } + Some(current) => Err(BdkSqlxError::DuplicateInitNetwork { + current: *current, + network, + }), + None => NETWORK + .set(network) + .map_err(BdkSqlxError::SetNetworkFailure), + } +} /// Crate error #[derive(Debug, thiserror::Error)] @@ -33,9 +72,6 @@ pub enum BdkSqlxError { /// sqlx error #[error("sqlx error: {0}")] Sqlx(#[from] sqlx::Error), - /// migrate error - #[error("migrate error: {0}")] - Migrate(#[from] sqlx::migrate::MigrateError), /// Network confusion #[error("Invalid Network expected {expected}, got {got}")] InvalidNetwork { @@ -84,12 +120,4 @@ pub struct Store { wallet_name: String, } -/// Build a new instance of the PgStoreBuilder -pub struct PgStoreBuilder { - wallet_name: String, - pool: Option, - migrate: bool, - network: Option, -} - -type FutureResult<'a, T, E> = Pin> + Send + 'a>>; +type FutureResult<'a, T, E> = Pin> + Send + 'a>>; diff --git a/src/pg_store_builder.rs b/src/pg_store_builder.rs new file mode 100644 index 0000000..2ccff53 --- /dev/null +++ b/src/pg_store_builder.rs @@ -0,0 +1,118 @@ +use crate::{initialize_network, BdkSqlxError, Store}; +use bdk_wallet::bitcoin::Network; +use sqlx::{PgPool, Postgres}; + +/// Builder for creating a Postgres-backed Store instance +pub struct PgStoreBuilder { + wallet_name: String, + pool: Option, + network: Option, + url: Option, +} + +impl PgStoreBuilder { + /// Creates a new builder for a [`Store`] with the given wallet name. + /// + /// # Required fields + /// Before building, you must set: + /// - `network` - The Bitcoin network to use + /// - Either provide a connection pool with `pool()` or a database URL with `url()` + /// + /// # Example + /// ``` + /// + /// + /// async fn example() -> Result<(), bdk_sqlx::BdkSqlxError> { + /// use bdk_wallet::bitcoin::Network; + /// use sqlx::PgPool; + /// use bdk_sqlx::pg_store_builder::PgStoreBuilder; + /// + /// // Build with a URL + /// let store = PgStoreBuilder::new("bdk_wallet_name".to_string()) + /// .network(Network::Testnet) + /// .url("postgres://username:password@localhost/database".to_string()) + /// .build() + /// .await?; + /// + /// // Or build with an existing pool + /// let pool = PgPool::connect("postgres://username:password@localhost/database").await?; + /// let store = PgStoreBuilder::new("another_wallet".to_string()) + /// .network(Network::Testnet) + /// .pool(pool) + /// .build() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + #[tracing::instrument] + pub fn new(wallet_name: String) -> Self { + Self { + wallet_name, + pool: None, + network: None, + url: None, + } + } + + /// Sets the database connection pool for the [`Store`]. + /// + /// Either a pool or a URL must be provided before building. + pub fn pool(mut self, pool: PgPool) -> Self { + self.pool = Some(pool); + self + } + + /// Sets the Bitcoin network for the [`Store`]. + /// + /// The network is required to build a valid [`Store`]. + pub fn network(mut self, network: Network) -> Self { + self.network = Some(network); + self + } + + /// Sets the Postgres connection URL for the [`Store`]. + /// + /// Either a URL or a pool must be provided before building. + /// + /// # Example + /// ``` + /// # use bdk_sqlx::pg_store_builder::PgStoreBuilder; + /// let builder = PgStoreBuilder::new("wallet".to_string()) + /// .url("postgres://username:password@localhost/database".to_string()); + /// ``` + pub fn url(mut self, url: impl Into) -> Self { + self.url = Some(url.into()); + self + } + + /// Builds the [`Store`] with the configured options. + /// + /// # Errors + /// + /// Returns an error if: + /// - No network has been specified (`MissingNetwork`) + /// - Neither pool nor URL has been specified (`MissingPool`) + /// - Database connection fails + /// - Network initialization fails + pub async fn build(self) -> crate::Result> { + if self + .network + .and_then(|n| initialize_network(n).ok()) + .is_none() + { + return Err(BdkSqlxError::MissingNetwork); + } + + // Get or create the connection pool + let pool = match (self.pool, self.url) { + (Some(pool), _) => pool, + (_, Some(url)) => PgPool::connect(&url).await?, + (None, None) => return Err(BdkSqlxError::MissingPool), + }; + + Ok(Store { + pool, + wallet_name: self.wallet_name, + }) + } +} diff --git a/src/postgres.rs b/src/postgres.rs index d4acec8..df2b74c 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -3,10 +3,7 @@ #![warn(missing_docs)] // Standard library imports -use std::{ - str::FromStr, - sync::{Arc, OnceLock}, -}; +use std::{str::FromStr, sync::Arc}; // Third party crates use bdk_chain::{ local_chain, tx_graph, Anchor, ConfirmationBlockTime, DescriptorExt, DescriptorId, Merge, @@ -25,48 +22,13 @@ use bdk_wallet::{ }; use serde_json::json; use sqlx::{ - postgres::{PgPool, PgRow, Postgres}, + postgres::{PgRow, Postgres}, FromRow, Pool, Row, Transaction, }; use tracing::{info, trace, warn}; // First party imports -use super::{BdkSqlxError, FutureResult, PgStoreBuilder, Store}; - -type Result = core::result::Result; - -/// Thread-safe storage for the network configuration that's shared across all Store instances. -/// This ensures consistent network validation across multiple threads. -static NETWORK: OnceLock = OnceLock::new(); - -/// Retrieves the current global network configuration for validation operations. -/// -/// Returns the current network configuration or an error if not initialized. -fn get_network() -> Result { - NETWORK - .get() - .copied() - .ok_or_else(|| BdkSqlxError::GetNetworkFailure) -} - -/// Sets the global network configuration to ensure consistent validation across threads. -/// -/// Returns an error if the network is already initialized with a different network. -fn initialize_network(network: Network) -> Result<()> { - match NETWORK.get() { - Some(current) if *current == network => { - warn!("initialize_network called more than once"); - Ok(()) - } - Some(current) => Err(BdkSqlxError::DuplicateInitNetwork { - current: *current, - network, - }), - None => NETWORK - .set(network) - .map_err(BdkSqlxError::SetNetworkFailure), - } -} +use super::{get_network, BdkSqlxError, FutureResult, Store}; impl AsyncWalletPersister for Store { type Error = BdkSqlxError; @@ -93,229 +55,18 @@ impl AsyncWalletPersister for Store { } } -impl PgStoreBuilder { - /// Creates a new builder for a [`Store`] with the given wallet name. - /// - /// # Required fields - /// Before building, you must set: - /// - `network` - The Bitcoin network to use - /// - Either provide a connection pool with `pool()` or a database URL with `build_with_url()` - /// - /// # Example - /// ``` - /// # async fn example() -> Result<(), bdk_sqlx::BdkSqlxError> { - /// use bdk_wallet::bitcoin::Network; - /// use bdk_sqlx::PgStoreBuilder; - /// - /// let store = PgStoreBuilder::new("bdk_wallet_name".to_string()) - /// .network(Network::Testnet) - /// .migrate(true) - /// .build_with_url("postgres://username:password@localhost/database") - /// .await?; - /// # Ok(()) - /// # } - /// ``` - #[tracing::instrument] - pub fn new(wallet_name: String) -> Self { - Self { - wallet_name, - pool: None, - migrate: false, - network: None, - } - } - - /// Sets the database connection pool for the [`Store`]. - /// - /// The pool is required to build a valid [`Store`]. If not provided, - /// the build operation will fail with a MissingPool error. - pub fn pool(mut self, pool: Pool) -> Self { - self.pool = Some(pool); - self - } - - /// Sets whether database migrations should be run during [`Store`] initialization. - /// - /// When set to true, the necessary database schema and tables will be created - /// if they don't already exist. - pub fn migrate(mut self, migrate: bool) -> Self { - self.migrate = migrate; - self - } - - /// Sets the Bitcoin network for the [`Store`]. - /// - /// The network is required to build a valid [`Store`]. If not provided, - /// the build operation will fail with a MissingNetwork error. - pub fn network(mut self, network: Network) -> Self { - self.network = Some(network); - self - } - - /// Builds the [`Store`] with the configured options. - /// - /// This method creates a new [`Store`] instance using the options that have been - /// set on this builder. It requires both a network and a pool to be specified - /// before building. - /// - /// # Errors - /// - /// Returns an error if: - /// - No network has been specified (MissingNetwork) - /// - No pool has been specified (MissingPool) - /// - Migration fails - /// - Network initialization fails - pub async fn build(self) -> Result> { - let network = self.network.ok_or_else(|| BdkSqlxError::MissingNetwork)?; - - match self.pool { - Some(pool) => { - let store = Store { - pool, - wallet_name: self.wallet_name, - }; - if self.migrate { - store.migrate().await?; - } - - initialize_network(network)?; - - Ok(store) - } - None => Err(BdkSqlxError::MissingPool), - } - } - - /// Builds the [`Store`] with a new connection pool created from the provided URL. - /// - /// This is a convenience method that creates a connection pool from the URL - /// and then builds the [`Store`] using that pool. - /// - /// # Errors - /// - /// Returns an error if: - /// - Database connection fails - /// - Any error that could occur in the build() method - pub async fn build_with_url(self, url: &str) -> Result> { - let pool = PgPool::connect(url).await?; - let store = self.pool(pool).build().await?; - Ok(store) - } -} - -impl Store { - /// Runs Migrations for a [`Store`] without an existing pg connection. - #[tracing::instrument(skip_all)] - pub async fn migrate(&self) -> Result<()> { - trace!("migrating bdk sqlx"); - - let mut tx = self.pool.begin().await?; - - // Create the schema first - let create_schema_query = r#"CREATE SCHEMA IF NOT EXISTS "bdk_wallet""#; - sqlx::query(create_schema_query) - .execute(&mut *tx) - .await - .map_err(|e| BdkSqlxError::QueryError { - table: "create schema bdk_wallet".to_string(), - source: e, - })?; - - // Create the tables one by one - let queries = [ - r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."version" ( - version INTEGER PRIMARY KEY - )"#, - r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."network" ( - wallet_name TEXT PRIMARY KEY, - name TEXT NOT NULL - )"#, - r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."keychain" ( - wallet_name TEXT NOT NULL, - keychainkind TEXT NOT NULL, - descriptor TEXT NOT NULL, - descriptor_id BYTEA NOT NULL, - last_revealed INTEGER DEFAULT 0, - PRIMARY KEY (wallet_name, keychainkind) - )"#, - r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."block" ( - wallet_name TEXT NOT NULL, - hash TEXT NOT NULL, - height INTEGER NOT NULL, - PRIMARY KEY (wallet_name, hash) - )"#, - r#"CREATE INDEX IF NOT EXISTS idx_block_height ON "bdk_wallet"."block" (height)"#, - r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."tx" ( - wallet_name TEXT NOT NULL, - txid TEXT NOT NULL, - whole_tx BYTEA, - last_seen BIGINT, - PRIMARY KEY (wallet_name, txid) - )"#, - r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."txout" ( - wallet_name TEXT NOT NULL, - txid TEXT NOT NULL, - vout INTEGER NOT NULL, - value BIGINT NOT NULL, - script BYTEA NOT NULL, - PRIMARY KEY (wallet_name, txid, vout) - )"#, - r#"CREATE TABLE IF NOT EXISTS "bdk_wallet"."anchor_tx" ( - wallet_name TEXT NOT NULL, - block_hash TEXT NOT NULL, - anchor JSONB NOT NULL, - txid TEXT NOT NULL, - PRIMARY KEY (wallet_name, block_hash, txid), - FOREIGN KEY (wallet_name, block_hash) REFERENCES "bdk_wallet"."block"(wallet_name, hash), - FOREIGN KEY (wallet_name, txid) REFERENCES "bdk_wallet"."tx"(wallet_name, txid) - )"#, - r#"CREATE INDEX IF NOT EXISTS idx_anchor_tx_txid ON "bdk_wallet"."anchor_tx" (txid)"#, - ]; - - // Execute each query separately - for query in &queries { - sqlx::query(query) - .execute(&mut *tx) - .await - .map_err(|e| BdkSqlxError::QueryError { - table: query.to_string(), - source: e, - })?; - } - - // At the end of migration, insert the current version - // After all tables are created but before tx.commit() - sqlx::query( - r#"INSERT INTO "bdk_wallet"."version" (version) - VALUES ($1) - ON CONFLICT (version) DO NOTHING"#, - ) - .bind(1) // Current schema version - .execute(&mut *tx) - .await - .map_err(|e| BdkSqlxError::QueryError { - table: "insert version".to_string(), - source: e, - })?; - - tx.commit().await?; - - Ok(()) - } -} - impl Store { #[tracing::instrument(skip_all)] - pub(crate) async fn read(&self) -> Result { + pub(crate) async fn read(&self) -> crate::Result { trace!("reading"); let mut db_tx = self.pool.begin().await?; let mut changeset = ChangeSet::default(); let sql = r#"SELECT n.name as network, k_int.descriptor as internal_descriptor, k_int.last_revealed as internal_last_revealed, k_ext.descriptor as external_descriptor, k_ext.last_revealed as external_last_revealed - FROM "bdk_wallet"."network" n - LEFT JOIN "bdk_wallet"."keychain" k_int ON n.wallet_name = k_int.wallet_name AND k_int.keychainkind = 'Internal' - LEFT JOIN "bdk_wallet"."keychain" k_ext ON n.wallet_name = k_ext.wallet_name AND k_ext.keychainkind = 'External' + FROM bdk_wallet.network n + LEFT JOIN bdk_wallet.keychain k_int ON n.wallet_name = k_int.wallet_name AND k_int.keychainkind = 'Internal' + LEFT JOIN bdk_wallet.keychain k_ext ON n.wallet_name = k_ext.wallet_name AND k_ext.keychainkind = 'External' WHERE n.wallet_name = $1"#; // Fetch wallet data @@ -341,7 +92,7 @@ impl Store { changeset: &mut ChangeSet, row: PgRow, wallet_name: &str, - ) -> Result<()> { + ) -> crate::Result<()> { trace!("changeset from row"); let network: String = row.get("network"); @@ -382,7 +133,7 @@ impl Store { } #[tracing::instrument(skip_all)] - pub(crate) async fn write(&self, changeset: &ChangeSet) -> Result<()> { + pub(crate) async fn write(&self, changeset: &ChangeSet) -> crate::Result<()> { trace!("changeset write"); if changeset.is_empty() { return Ok(()); @@ -427,7 +178,7 @@ async fn insert_descriptor( wallet_name: &str, descriptor: &ExtendedDescriptor, keychain: KeychainKind, -) -> Result<()> { +) -> crate::Result<()> { trace!("insert descriptor"); let descriptor_str = descriptor.to_string(); @@ -438,7 +189,7 @@ async fn insert_descriptor( }; sqlx::query( - r#"INSERT INTO "bdk_wallet"."keychain" (wallet_name, keychainkind, descriptor, descriptor_id) VALUES ($1, $2, $3, $4)"#, + r#"INSERT INTO "bdk_wallet"."keychain" (wallet_name, keychainkind, descriptor, descriptor_id) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING"#, ) .bind(wallet_name) .bind(keychain) @@ -460,9 +211,9 @@ async fn insert_network( db_tx: &mut Transaction<'_, Postgres>, wallet_name: &str, network: Network, -) -> Result<()> { +) -> crate::Result<()> { trace!("insert network"); - sqlx::query(r#"INSERT INTO "bdk_wallet"."network" (wallet_name, name) VALUES ($1, $2)"#) + sqlx::query(r#"INSERT INTO "bdk_wallet"."network" (wallet_name, name) VALUES ($1, $2) ON CONFLICT DO NOTHING"#) .bind(wallet_name) .bind(network.to_string()) .execute(&mut **db_tx) @@ -482,7 +233,7 @@ async fn update_last_revealed( wallet_name: &str, descriptor_id: DescriptorId, last_revealed: u32, -) -> Result<()> { +) -> crate::Result<()> { trace!("update last revealed"); sqlx::query( @@ -506,7 +257,7 @@ async fn update_last_revealed( pub async fn tx_graph_changeset_from_postgres( db_tx: &mut Transaction<'_, Postgres>, wallet_name: &str, -) -> Result> { +) -> crate::Result> { trace!("tx graph changeset from postgres"); let mut changeset = tx_graph::ChangeSet::default(); @@ -599,7 +350,7 @@ pub async fn tx_graph_changeset_persist_to_postgres( db_tx: &mut Transaction<'_, Postgres>, wallet_name: &str, changeset: &tx_graph::ChangeSet, -) -> Result<()> { +) -> crate::Result<()> { trace!("tx graph changeset from postgres"); for tx in &changeset.txs { sqlx::query( @@ -677,7 +428,7 @@ pub async fn tx_graph_changeset_persist_to_postgres( pub async fn local_chain_changeset_from_postgres( db_tx: &mut Transaction<'_, Postgres>, wallet_name: &str, -) -> Result { +) -> crate::Result { trace!("local chain changeset from postgres"); let mut changeset = local_chain::ChangeSet::default(); @@ -707,7 +458,7 @@ pub async fn local_chain_changeset_persist_to_postgres( db_tx: &mut Transaction<'_, Postgres>, wallet_name: &str, changeset: &local_chain::ChangeSet, -) -> Result<()> { +) -> crate::Result<()> { trace!("local chain changeset to postgres"); for (&height, &hash) in &changeset.blocks { match hash { @@ -747,7 +498,7 @@ pub async fn local_chain_changeset_persist_to_postgres( /// Collects information on all the wallets in the database and dumps it to stdout. #[tracing::instrument] -pub async fn easy_backup(db: Pool) -> Result<()> { +pub async fn easy_backup(db: Pool) -> crate::Result<()> { trace!("Starting easy backup"); let statement = r#"SELECT * FROM "bdk_wallet"."keychain""#; diff --git a/src/sqlite.rs b/src/sqlite.rs index 590054b..286d0a0 100644 --- a/src/sqlite.rs +++ b/src/sqlite.rs @@ -22,7 +22,6 @@ use bdk_wallet::{AsyncWalletPersister, ChangeSet, KeychainKind}; use serde_json::json; use sqlx::sqlite::SqliteRow; use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; -use sqlx::sqlx_macros::migrate; use sqlx::{sqlite::Sqlite, FromRow, Pool, Row, Transaction}; use tracing::info; @@ -54,16 +53,8 @@ impl AsyncWalletPersister for Store { impl Store { /// Construct a new [`Store`] with an existing sqlite connection pool. #[tracing::instrument] - pub async fn new( - pool: Pool, - wallet_name: String, - migrate: bool, - ) -> Result { + pub async fn new(pool: Pool, wallet_name: String) -> Result { info!("new sqlite store"); - if migrate { - info!("migrate"); - migrate!("./migrations/sqlite").run(&pool).await?; - } Ok(Self { pool, wallet_name }) } @@ -77,7 +68,6 @@ impl Store { pub async fn new_with_url( url: Option, wallet_name: String, - migrate: bool, ) -> Result, BdkSqlxError> { info!("new store with url"); let pool = if let Some(url) = url { @@ -92,7 +82,7 @@ impl Store { .connect(":memory:") .await? }; - Self::new(pool, wallet_name, migrate).await + Self::new(pool, wallet_name).await } } diff --git a/src/test.rs b/src/test.rs index 9a4b7fd..f857ce2 100644 --- a/src/test.rs +++ b/src/test.rs @@ -21,7 +21,7 @@ use bitcoin::{ Network::{self, Regtest}, OutPoint, Transaction, TxIn, TxOut, Txid, }; -use sqlx::{Pool, Postgres, Sqlite, SqlitePool}; +use sqlx::{sqlx_macros::migrate, Pool, Postgres, Sqlite, SqlitePool}; use test_utils::{ get_test_tr_single_sig_xprv_and_change_desc, get_test_wpkh, insert_anchor, insert_checkpoint, insert_tx, new_tx, @@ -29,7 +29,8 @@ use test_utils::{ use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; -use crate::{BdkSqlxError, FutureResult, PgStoreBuilder, Store}; +use crate::pg_store_builder::PgStoreBuilder; +use crate::{BdkSqlxError, FutureResult, Store}; pub fn get_test_minisicript_with_change_desc() -> (&'static str, &'static str) { ("wsh(andor(multi(2,[a0d3c79c/48'/1'/79'/2']tpubDEsGdqFaKUVnVNZZw8AixJ8C3yD8o6nN7hsdLfbtVRDTk3PNrQ2pcWNWNbxhdcNSgQP25pUpgRQ7qiVtN3YvSzACKizrvzSwH9SQ2Bjbbwt/0/*,[ea2484f9/48'/1'/79'/2']tpubDFjkswBXoRHKkvmHsxv4xdDqbjg1peX9zJytLeSLbXuwVgYhXgbABzC2r5MAWxqWoaUr7hWGW5TPjA9sNvxa3mX6DrNBdynDsEvwDoXGFpm/0/*,[93f245d7/48'/1'/79'/2']tpubDEVnR72gRgTsqaPFMacV6fCfaSEe56gcDomuGhk9MFeUdEi18riJCokgsZr2x1KKGRM59TJ4AQ6FuNun3khh95ceoH2ytN13nVD7yDLP5LJ/0/*),or_i(and_v(v:pkh([61cdf766/48'/1'/79'/2']tpubDEXETCw2WurhazfW5gW1z4njP6yLXDQmCGfjWGP5k3BuTQ5iZqovMr1zz1zWPhDMRn11hXGpZHodus1LysXnwREsD1ig96M24JhQCpPPpf6/0/*),after(1753228800)),thresh(2,pk([39bf48a9/48'/1'/0'/2']tpubDEr9rVFQbT1keErwxb6GuGy3RM6TEACSkFxBgziUvrDprYuM1Wm7wi6jb1gcaLrSgk6MSkGx84dS2kQQwJKxGRJ59rAvmuKTU7E3saHJLf5/0/*),s:pk([9467fdb3/48'/1'/0'/2']tpubDFEjX5BY88AbWpshPwGscwgKLtcCjeVodMbmhS6D6cbz1eGNUs3546ephbVmbHpxEhbCDrezGmFBArLxBKzPEfBcBdzQuncPm8ww2xa6UUQ/0/*),s:pk([01adf45e/48'/1'/0'/2']tpubDFPYZPeShApyWndvDUtpLSjDHGYK4tTT4BkMyTukGqbP9AXQeQhiWsbwEzyZhxgud9ZPew1FPsoLbWjfnE3veSXLeU4ViofrhVAHNXtjQWE/0/*),snl:after(1739836800))),and_v(v:thresh(2,pkh([39bf48a9/48'/1'/0'/2']tpubDEr9rVFQbT1keErwxb6GuGy3RM6TEACSkFxBgziUvrDprYuM1Wm7wi6jb1gcaLrSgk6MSkGx84dS2kQQwJKxGRJ59rAvmuKTU7E3saHJLf5/2/*),a:pkh([9467fdb3/48'/1'/0'/2']tpubDFEjX5BY88AbWpshPwGscwgKLtcCjeVodMbmhS6D6cbz1eGNUs3546ephbVmbHpxEhbCDrezGmFBArLxBKzPEfBcBdzQuncPm8ww2xa6UUQ/2/*),a:pkh([01adf45e/48'/1'/0'/2']tpubDFPYZPeShApyWndvDUtpLSjDHGYK4tTT4BkMyTukGqbP9AXQeQhiWsbwEzyZhxgud9ZPew1FPsoLbWjfnE3veSXLeU4ViofrhVAHNXtjQWE/2/*)),after(1757116800))))", @@ -103,7 +104,7 @@ impl DropAll for Pool { #[derive(Debug)] enum TestStore { Postgres(Store), - Sqlite(Store), + // Sqlite(Store), } impl AsyncWalletPersister for TestStore { @@ -116,8 +117,14 @@ impl AsyncWalletPersister for TestStore { { info!("initialize test store"); match store { - TestStore::Postgres(store) => Box::pin(store.read()), - TestStore::Sqlite(store) => Box::pin(store.read()), + TestStore::Postgres(store) => { + migrate!("migrations/postgres"); + Box::pin(store.read()) + } + // TestStore::Sqlite(store) => { + // migrate!("migrations/sqlite"); + // Box::pin(store.read()) + // } } } @@ -132,40 +139,11 @@ impl AsyncWalletPersister for TestStore { info!("persist test store"); match store { TestStore::Postgres(store) => Box::pin(store.write(changeset)), - TestStore::Sqlite(store) => Box::pin(store.write(changeset)), + // TestStore::Sqlite(store) => Box::pin(store.write(changeset)), } } } -pub async fn _drop_tables() -> anyhow::Result<()> { - let url = env::var("DATABASE_TEST_URL").expect("DATABASE_TEST_URL must be set for tests"); - let pool = Pool::::connect(&url.clone()).await?; - - let mut tx = pool.begin().await?; - - // Drop tables in reverse order of creation to handle foreign key constraints - let queries = [ - r#"DROP INDEX IF EXISTS "bdk_wallet"."idx_anchor_tx_txid""#, - r#"DROP TABLE IF EXISTS "bdk_wallet"."anchor_tx""#, - r#"DROP TABLE IF EXISTS "bdk_wallet"."txout""#, - r#"DROP TABLE IF EXISTS "bdk_wallet"."tx""#, - r#"DROP INDEX IF EXISTS "bdk_wallet"."idx_block_height""#, - r#"DROP TABLE IF EXISTS "bdk_wallet"."block""#, - r#"DROP TABLE IF EXISTS "bdk_wallet"."keychain""#, - r#"DROP TABLE IF EXISTS "bdk_wallet"."network""#, - r#"DROP SCHEMA IF EXISTS "bdk_wallet" CASCADE"#, - ]; - - // Execute each query separately - for query in &queries { - sqlx::query(query).execute(&mut *tx).await?; - } - - tx.commit().await?; - - Ok(()) -} - async fn create_test_stores(wallet_name: String) -> anyhow::Result> { let mut stores: Vec = Vec::new(); @@ -177,15 +155,15 @@ async fn create_test_stores(wallet_name: String) -> anyhow::Result::new(pool.clone(), wallet_name.clone(), true).await?; - stores.push(TestStore::Sqlite(sqlite_store)); + // let pool = SqlitePool::connect(":memory:").await?; + // let sqlite_store = Store::::new(pool.clone(), wallet_name.clone()).await?; + // stores.push(TestStore::Sqlite(sqlite_store)); Ok(stores) } @@ -437,6 +415,7 @@ async fn test_three_wallets_list_transactions() -> anyhow::Result<()> { let loaded_balance = wallet.balance(); assert_eq!(saved_balance, loaded_balance); } + Ok(()) } @@ -499,6 +478,7 @@ async fn wallet_load_checks() -> anyhow::Result<()> { "unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)"); } } + Ok(()) } @@ -545,6 +525,7 @@ async fn single_descriptor_wallet_persist_and_recover() -> anyhow::Result<()> { ); } } + Ok(()) } @@ -630,5 +611,6 @@ async fn two_wallets_load() -> anyhow::Result<()> { "different wallets should not have same chain tip" ); } + Ok(()) }