From e19eb79e0c28aa7bc277c861d8848c664f011015 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 18:48:17 -0500 Subject: [PATCH 01/17] fixup/ci: audit and machete --- .github/workflows/rust.yml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c5058d6..9ff6caf 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -172,4 +172,26 @@ jobs: - name: Check fmt run: cargo fmt --all -- --check - name: Clippy - run: cargo clippy --all-targets -- -Dwarnings \ No newline at end of file + run: cargo clippy --all-targets -- -Dwarnings + + security-audit: + name: Security Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install cargo-audit + run: cargo install cargo-audit + - name: Run cargo-audit + run: cargo audit + + unused-dependencies: + name: Check Unused Dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install cargo-machete + run: cargo install cargo-machete + - name: Run cargo-machete + run: cargo machete \ No newline at end of file From 8168dbadacf2325a65906d45cf8bfad72b308c34 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 19:03:31 -0500 Subject: [PATCH 02/17] fixup: update deps and justfile --- Cargo.toml | 11 +++++------ Justfile | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 Justfile diff --git a/Cargo.toml b/Cargo.toml index e181ee1..05f0485 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", "tls-rustls-ring", "derive", "postgres", "sqlite", "json", "chrono", "uuid", "migrate"] } +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..2961618 --- /dev/null +++ b/Justfile @@ -0,0 +1,18 @@ +# 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 \ No newline at end of file From 4131982144ec6ecfb2664bd58d1da01a383832aa Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 19:11:30 -0500 Subject: [PATCH 03/17] fixup/postgres/sqlite: removed migration --- .github/workflows/rust.yml | 2 +- Cargo.toml | 2 +- examples/bdk_sqlx_postgres.rs | 2 - src/lib.rs | 4 -- src/postgres.rs | 113 ---------------------------------- src/sqlite.rs | 9 +-- src/test.rs | 3 +- 7 files changed, 4 insertions(+), 131 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9ff6caf..dab5a0a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -183,7 +183,7 @@ jobs: - name: Install cargo-audit run: cargo install cargo-audit - name: Run cargo-audit - run: cargo audit + run: cargo audit --ignore RUSTSEC-2023-0071 unused-dependencies: name: Check Unused Dependencies diff --git a/Cargo.toml b/Cargo.toml index 05f0485..fcc1367 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ 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.5", default-features = false, features = ["runtime-tokio", "tls-rustls-ring", "derive", "postgres", "sqlite", "json", "chrono", "uuid", "migrate"] } +sqlx = { version = "0.8.5", default-features = false, features = ["runtime-tokio", "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" diff --git a/examples/bdk_sqlx_postgres.rs b/examples/bdk_sqlx_postgres.rs index 0961e0b..9ef60f0 100644 --- a/examples/bdk_sqlx_postgres.rs +++ b/examples/bdk_sqlx_postgres.rs @@ -59,7 +59,6 @@ async fn main() -> anyhow::Result<()> { let mut store = PgStoreBuilder::new(wallet_name.clone()) .network(NETWORK) - .migrate(true) .build_with_url(&url) .await?; @@ -90,7 +89,6 @@ async fn main() -> anyhow::Result<()> { let mut store = PgStoreBuilder::new(wallet_name.clone()) .network(NETWORK) - .migrate(true) .build_with_url(&url) .await?; diff --git a/src/lib.rs b/src/lib.rs index ae613ac..d3618b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,9 +33,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 { @@ -88,7 +85,6 @@ pub struct Store { pub struct PgStoreBuilder { wallet_name: String, pool: Option, - migrate: bool, network: Option, } diff --git a/src/postgres.rs b/src/postgres.rs index d4acec8..d3a5334 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -120,7 +120,6 @@ impl PgStoreBuilder { Self { wallet_name, pool: None, - migrate: false, network: None, } } @@ -134,15 +133,6 @@ impl PgStoreBuilder { 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, @@ -174,9 +164,6 @@ impl PgStoreBuilder { pool, wallet_name: self.wallet_name, }; - if self.migrate { - store.migrate().await?; - } initialize_network(network)?; @@ -203,106 +190,6 @@ impl PgStoreBuilder { } } -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)] diff --git a/src/sqlite.rs b/src/sqlite.rs index 590054b..795451a 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; @@ -57,13 +56,8 @@ impl Store { pub async fn new( pool: Pool, wallet_name: String, - migrate: bool, ) -> Result { info!("new sqlite store"); - if migrate { - info!("migrate"); - migrate!("./migrations/sqlite").run(&pool).await?; - } Ok(Self { pool, wallet_name }) } @@ -77,7 +71,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 +85,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..c43df6d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -177,14 +177,13 @@ async fn create_test_stores(wallet_name: String) -> anyhow::Result::new(pool.clone(), wallet_name.clone(), true).await?; + let sqlite_store = Store::::new(pool.clone(), wallet_name.clone()).await?; stores.push(TestStore::Sqlite(sqlite_store)); Ok(stores) From 1ac4a7576ca62b9dc5855bd9be93cc873e41bf5b Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 19:14:27 -0500 Subject: [PATCH 04/17] fixup: audit file --- .github/workflows/rust.yml | 2 +- audit.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 audit.toml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index dab5a0a..9ff6caf 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -183,7 +183,7 @@ jobs: - name: Install cargo-audit run: cargo install cargo-audit - name: Run cargo-audit - run: cargo audit --ignore RUSTSEC-2023-0071 + run: cargo audit unused-dependencies: name: Check Unused Dependencies 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 From dd4904b7c9c4e41aad443ad62c2cb82c0dfcdc0e Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 19:58:57 -0500 Subject: [PATCH 05/17] fixup: refactor --- .github/workflows/rust.yml | 5 ++ Justfile | 6 +- examples/bdk_sqlx_postgres.rs | 10 ++- src/lib.rs | 52 ++++++++--- src/pg_store_builder.rs | 118 +++++++++++++++++++++++++ src/postgres.rs | 157 +++------------------------------- src/sqlite.rs | 5 +- src/test.rs | 39 ++++++--- 8 files changed, 214 insertions(+), 178 deletions(-) create mode 100644 src/pg_store_builder.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9ff6caf..ad80de3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -39,6 +39,8 @@ jobs: - name: Create database run: | sudo apt-get install libpq-dev -y + # Install sqlx-cli + cargo install sqlx-cli --no-default-features --features native-tls,postgres 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 @@ -68,6 +70,9 @@ jobs: - name: Create database run: | sudo apt-get install libpq-dev -y + # Install sqlx-cli + cargo install sqlx-cli --no-default-features --features native-tls,postgres + 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 diff --git a/Justfile b/Justfile index 2961618..b9cf9d4 100644 --- a/Justfile +++ b/Justfile @@ -15,4 +15,8 @@ stop-postgres: # DATABASE_URL: postgres://postgres:password@localhost:5432/mydatabase example: - cargo run --example bdk_sqlx_postgres \ No newline at end of file + cargo run --example bdk_sqlx_postgres + +# Database migration +run-migrations: + sqlx migrate run --source migrations/postgres diff --git a/examples/bdk_sqlx_postgres.rs b/examples/bdk_sqlx_postgres.rs index 9ef60f0..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,7 +59,8 @@ async fn main() -> anyhow::Result<()> { let mut store = PgStoreBuilder::new(wallet_name.clone()) .network(NETWORK) - .build_with_url(&url) + .url(&url) + .build() .await?; let mut wallet = match Wallet::load().load_wallet_async(&mut store).await? { @@ -89,7 +90,8 @@ async fn main() -> anyhow::Result<()> { let mut store = PgStoreBuilder::new(wallet_name.clone()) .network(NETWORK) - .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 d3618b5..a5c442b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,18 +5,55 @@ mod postgres; mod sqlite; +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::Pool; use sqlx::{Database, PgPool}; +use std::future::Future; +use std::pin::Pin; +use std::sync::OnceLock; +use tracing::warn; + +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)] @@ -81,11 +118,4 @@ pub struct Store { wallet_name: String, } -/// Build a new instance of the PgStoreBuilder -pub struct PgStoreBuilder { - wallet_name: String, - pool: Option, - 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..bcb6481 --- /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> { + // Gets network or sets it to regtest by default + let network = self.network.unwrap_or(Network::Regtest); + + // Initialize global network + initialize_network(network)?; + + // Get or create the connection pool + let pool = match (self.pool, self.url) { + (Some(pool), _) => pool, + (None, Some(url)) => PgPool::connect(&url).await?, + (None, None) => return Err(BdkSqlxError::MissingPool), + }; + + Ok(Store { + pool, + wallet_name: self.wallet_name, + }) + } + + // Removed redundant build_with_url method +} diff --git a/src/postgres.rs b/src/postgres.rs index d3a5334..2a2197b 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -31,42 +31,7 @@ use sqlx::{ 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,107 +58,9 @@ 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, - 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 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, - }; - - 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 { #[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(); @@ -228,7 +95,7 @@ impl Store { changeset: &mut ChangeSet, row: PgRow, wallet_name: &str, - ) -> Result<()> { + ) -> crate::Result<()> { trace!("changeset from row"); let network: String = row.get("network"); @@ -269,7 +136,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(()); @@ -314,7 +181,7 @@ async fn insert_descriptor( wallet_name: &str, descriptor: &ExtendedDescriptor, keychain: KeychainKind, -) -> Result<()> { +) -> crate::Result<()> { trace!("insert descriptor"); let descriptor_str = descriptor.to_string(); @@ -347,7 +214,7 @@ 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)"#) .bind(wallet_name) @@ -369,7 +236,7 @@ async fn update_last_revealed( wallet_name: &str, descriptor_id: DescriptorId, last_revealed: u32, -) -> Result<()> { +) -> crate::Result<()> { trace!("update last revealed"); sqlx::query( @@ -393,7 +260,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(); @@ -486,7 +353,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( @@ -564,7 +431,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(); @@ -594,7 +461,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 { @@ -634,7 +501,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 795451a..286d0a0 100644 --- a/src/sqlite.rs +++ b/src/sqlite.rs @@ -53,10 +53,7 @@ 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, - ) -> Result { + pub async fn new(pool: Pool, wallet_name: String) -> Result { info!("new sqlite store"); Ok(Self { pool, wallet_name }) } diff --git a/src/test.rs b/src/test.rs index c43df6d..6cc3087 100644 --- a/src/test.rs +++ b/src/test.rs @@ -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))))", @@ -137,23 +138,20 @@ impl AsyncWalletPersister for TestStore { } } -pub async fn _drop_tables() -> anyhow::Result<()> { +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 + // Truncate 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"#, + r#"TRUNCATE TABLE "bdk_wallet"."anchor_tx" CASCADE"#, + r#"TRUNCATE TABLE "bdk_wallet"."txout" CASCADE"#, + r#"TRUNCATE TABLE "bdk_wallet"."tx" CASCADE"#, + r#"TRUNCATE TABLE "bdk_wallet"."block" CASCADE"#, + r#"TRUNCATE TABLE "bdk_wallet"."keychain" CASCADE"#, + r#"TRUNCATE TABLE "bdk_wallet"."network" CASCADE"#, ]; // Execute each query separately @@ -177,7 +175,8 @@ async fn create_test_stores(wallet_name: String) -> anyhow::Result anyhow::Result<()> { } } + drop_tables().await?; + Ok(()) } @@ -436,6 +437,9 @@ async fn test_three_wallets_list_transactions() -> anyhow::Result<()> { let loaded_balance = wallet.balance(); assert_eq!(saved_balance, loaded_balance); } + + drop_tables().await?; + Ok(()) } @@ -498,6 +502,9 @@ async fn wallet_load_checks() -> anyhow::Result<()> { "unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)"); } } + + drop_tables().await?; + Ok(()) } @@ -544,6 +551,9 @@ async fn single_descriptor_wallet_persist_and_recover() -> anyhow::Result<()> { ); } } + + drop_tables().await?; + Ok(()) } @@ -629,5 +639,8 @@ async fn two_wallets_load() -> anyhow::Result<()> { "different wallets should not have same chain tip" ); } + + drop_tables().await?; + Ok(()) } From 3e3a5a3c473d2d60c0b0c2ab377f8d2d48530714 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 20:03:01 -0500 Subject: [PATCH 06/17] ci --- .github/workflows/rust.yml | 189 ++++++++----------------------------- 1 file changed, 39 insertions(+), 150 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ad80de3..820a2b3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,8 +15,8 @@ env: DATABASE_TEST_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,168 +35,57 @@ 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 # Install sqlx-cli cargo install sqlx-cli --no-default-features --features native-tls,postgres - 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 + cargo install cargo-audit + cargo install cargo-machete - 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 - # Install sqlx-cli - cargo install sqlx-cli --no-default-features --features native-tls,postgres - 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 - - 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 - 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 + # 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 - 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: 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 two_wallets_load - run: cargo test two_wallets_load -- --show-output - - 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 + cargo fmt --all -- --check + cargo clippy --all-targets -- -Dwarnings - security-audit: - name: Security Audit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - name: Install cargo-audit - run: cargo install cargo-audit - - name: Run cargo-audit + - name: Security Audit run: cargo audit - unused-dependencies: - name: Check Unused Dependencies - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - name: Install cargo-machete - run: cargo install cargo-machete - - name: Run cargo-machete + - name: Check Unused Dependencies run: cargo machete \ No newline at end of file From 786e83f5b28b4fc1df37931df865f4336aa32d02 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 20:09:08 -0500 Subject: [PATCH 07/17] sqlx cli extra env variable --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 820a2b3..bbe35c8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -13,7 +13,7 @@ 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: all-tests: name: Run all wallet tests From 5321397f2772c2ef1b4e463f85590785f8d4dcd0 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 20:19:06 -0500 Subject: [PATCH 08/17] fixup: ci --- Justfile | 3 +++ src/lib.rs | 4 +++- src/pg_store_builder.rs | 16 ++++++++-------- src/postgres.rs | 7 ++----- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Justfile b/Justfile index b9cf9d4..5420758 100644 --- a/Justfile +++ b/Justfile @@ -20,3 +20,6 @@ example: # Database migration run-migrations: sqlx migrate run --source migrations/postgres + +undo-migrations: + sqlx migrate revert --all diff --git a/src/lib.rs b/src/lib.rs index a5c442b..ae4b1ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ mod postgres; mod sqlite; +/// Builder for Store pub mod pg_store_builder; #[cfg(test)] mod test; @@ -13,13 +14,14 @@ 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. diff --git a/src/pg_store_builder.rs b/src/pg_store_builder.rs index bcb6481..2ccff53 100644 --- a/src/pg_store_builder.rs +++ b/src/pg_store_builder.rs @@ -95,16 +95,18 @@ impl PgStoreBuilder { /// - Database connection fails /// - Network initialization fails pub async fn build(self) -> crate::Result> { - // Gets network or sets it to regtest by default - let network = self.network.unwrap_or(Network::Regtest); - - // Initialize global network - initialize_network(network)?; + 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, - (None, Some(url)) => PgPool::connect(&url).await?, + (_, Some(url)) => PgPool::connect(&url).await?, (None, None) => return Err(BdkSqlxError::MissingPool), }; @@ -113,6 +115,4 @@ impl PgStoreBuilder { wallet_name: self.wallet_name, }) } - - // Removed redundant build_with_url method } diff --git a/src/postgres.rs b/src/postgres.rs index 2a2197b..66b3c26 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,7 +22,7 @@ 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}; From ff7d56817f7f861ea42c4665f0ccea18501a0966 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 20:33:34 -0500 Subject: [PATCH 09/17] fixup: sql --- src/postgres.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/postgres.rs b/src/postgres.rs index 66b3c26..315039a 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -64,7 +64,7 @@ impl Store { 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 + FROM "bdk_wallet"."network", 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"#; From 75a71f79a04d3c38ac2e70717eb402bf499ec9c0 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 20:41:07 -0500 Subject: [PATCH 10/17] fixup: sql --- src/postgres.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/postgres.rs b/src/postgres.rs index 315039a..63d7044 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -64,7 +64,7 @@ impl Store { 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", + FROM "bdk_wallet"."network" 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"#; From a8dc3d495a077d46be360a28070c4d159ad9fb12 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 20:48:58 -0500 Subject: [PATCH 11/17] ci2 --- src/postgres.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/postgres.rs b/src/postgres.rs index 63d7044..66b3c26 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -64,7 +64,7 @@ impl Store { 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" + 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"#; From 79307eb92148cb9c8eeb0c268deda8c6976da2b9 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Mon, 21 Apr 2025 21:02:08 -0500 Subject: [PATCH 12/17] migrate --- Cargo.toml | 2 +- src/test.rs | 48 +++++++++--------------------------------------- 2 files changed, 10 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fcc1367..cb14c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ 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.5", default-features = false, features = ["runtime-tokio", "tls-rustls-ring", "derive", "postgres", "sqlite", "json", "chrono", "uuid"] } +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" diff --git a/src/test.rs b/src/test.rs index 6cc3087..6a8a38d 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, @@ -117,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()) + } } } @@ -138,32 +144,6 @@ impl AsyncWalletPersister for TestStore { } } -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?; - - // Truncate tables in reverse order of creation to handle foreign key constraints - let queries = [ - r#"TRUNCATE TABLE "bdk_wallet"."anchor_tx" CASCADE"#, - r#"TRUNCATE TABLE "bdk_wallet"."txout" CASCADE"#, - r#"TRUNCATE TABLE "bdk_wallet"."tx" CASCADE"#, - r#"TRUNCATE TABLE "bdk_wallet"."block" CASCADE"#, - r#"TRUNCATE TABLE "bdk_wallet"."keychain" CASCADE"#, - r#"TRUNCATE TABLE "bdk_wallet"."network" 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(); @@ -339,8 +319,6 @@ async fn wallet_is_persisted() -> anyhow::Result<()> { } } - drop_tables().await?; - Ok(()) } @@ -438,8 +416,6 @@ async fn test_three_wallets_list_transactions() -> anyhow::Result<()> { assert_eq!(saved_balance, loaded_balance); } - drop_tables().await?; - Ok(()) } @@ -503,8 +479,6 @@ async fn wallet_load_checks() -> anyhow::Result<()> { } } - drop_tables().await?; - Ok(()) } @@ -552,8 +526,6 @@ async fn single_descriptor_wallet_persist_and_recover() -> anyhow::Result<()> { } } - drop_tables().await?; - Ok(()) } @@ -640,7 +612,5 @@ async fn two_wallets_load() -> anyhow::Result<()> { ); } - drop_tables().await?; - Ok(()) } From e49b2ad601450014e9c4c44b0f16a99f58393dc0 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Tue, 22 Apr 2025 10:40:10 -0500 Subject: [PATCH 13/17] fixup: sql conflicts do nothing --- src/postgres.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/postgres.rs b/src/postgres.rs index 66b3c26..132cdc0 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -189,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) @@ -213,7 +213,7 @@ async fn insert_network( network: Network, ) -> 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) From 8f202a9e736181072d3f2cbfc12fce79088c2b42 Mon Sep 17 00:00:00 2001 From: Matthias Debernardini <32387851+matthiasdebernardini@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:58:53 -0500 Subject: [PATCH 14/17] Update postgres.rs --- src/postgres.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/postgres.rs b/src/postgres.rs index 132cdc0..df2b74c 100644 --- a/src/postgres.rs +++ b/src/postgres.rs @@ -64,9 +64,9 @@ impl Store { 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 From f6f241e5b21ca279a4c72e2c43b23061fcbdbe14 Mon Sep 17 00:00:00 2001 From: Matthias Debernardini <32387851+matthiasdebernardini@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:02:11 -0500 Subject: [PATCH 15/17] Update rust.yml --- .github/workflows/rust.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index bbe35c8..4be0c63 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,8 +44,6 @@ jobs: sudo apt-get install libpq-dev -y # Install sqlx-cli cargo install sqlx-cli --no-default-features --features native-tls,postgres - cargo install cargo-audit - cargo install cargo-machete - name: Create and prepare database run: | @@ -85,7 +83,11 @@ jobs: cargo clippy --all-targets -- -Dwarnings - name: Security Audit - run: cargo audit + run: | + cargo install cargo-audit + cargo audit - name: Check Unused Dependencies - run: cargo machete \ No newline at end of file + run: | + cargo install cargo-machete + cargo machete From e9d419435ec2c0a43e3c4c0de774d69ac7a15d72 Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Tue, 22 Apr 2025 11:08:09 -0500 Subject: [PATCH 16/17] remove sqlite from unit tests --- src/test.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test.rs b/src/test.rs index 6a8a38d..91fd1f3 100644 --- a/src/test.rs +++ b/src/test.rs @@ -161,9 +161,9 @@ async fn create_test_stores(wallet_name: String) -> anyhow::Result::new(pool.clone(), wallet_name.clone()).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) } From 2ae1da67a575dadcfc6fab890a28e36c3d12535d Mon Sep 17 00:00:00 2001 From: matthiasdebernardini Date: Tue, 22 Apr 2025 11:13:36 -0500 Subject: [PATCH 17/17] fixup: sqlite --- src/test.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/test.rs b/src/test.rs index 91fd1f3..f857ce2 100644 --- a/src/test.rs +++ b/src/test.rs @@ -104,7 +104,7 @@ impl DropAll for Pool { #[derive(Debug)] enum TestStore { Postgres(Store), - Sqlite(Store), + // Sqlite(Store), } impl AsyncWalletPersister for TestStore { @@ -118,13 +118,13 @@ impl AsyncWalletPersister for TestStore { info!("initialize test store"); match store { TestStore::Postgres(store) => { - migrate!("migrations/postgres"); - Box::pin(store.read()) - } - TestStore::Sqlite(store) => { - migrate!("migrations/sqlite"); + migrate!("migrations/postgres"); Box::pin(store.read()) } + // TestStore::Sqlite(store) => { + // migrate!("migrations/sqlite"); + // Box::pin(store.read()) + // } } } @@ -139,7 +139,7 @@ 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)), } } }