Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/target
target
Cargo.lock

*.sqlite
.env
36 changes: 13 additions & 23 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,34 +1,24 @@
[package]
name = "bdk-sqlx"
name = "bdk_sqlx"
version = "0.1.0"
edition = "2021"

[lib]
name = "bdk_sqlx"
path = "src/lib.rs"

[[bin]]
name = "async_wallet_bdk_sqlx"
path = "src/main.rs"

[dependencies]
sqlx = { version = "0.8.1", default-features = false, features = ["runtime-tokio", "tls-rustls-ring","derive", "postgres", "json", "chrono", "uuid", "sqlx-macros", "migrate"] }
thiserror = "1"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
bdk_wallet = { version = "1.0.0-beta.5" }
serde = { version = "1.0.208", features = ["derive"] }
serde_json = "1.0.125"
better-panic = "0.3.0"
rustls = "0.23.12"
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"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde_json", "json"] }
anyhow = "1.0.86"
rand = "0.8.5"
uuid = "1.10.0"
assert_matches = "1.5.0"
pg-embed = { version = "0.7.1", features = ["default"] }

[dev-dependencies]
assert_matches = "1.5.0"
anyhow = "1.0.89"
bdk_electrum = { version = "0.19.0"}
rustls = "0.23.14"

bdk_wallet = { git = "https://github.com/bitcoindevkit/bdk", tag = "v1.0.0-beta.2", features = ["std"], default-features = false }
bdk_chain = { git = "https://github.com/bitcoindevkit/bdk", tag = "v1.0.0-beta.2" }
bdk_electrum = { git = "https://github.com/bitcoindevkit/bdk", tag = "v1.0.0-beta.2" }
bdk_testenv = { git = "https://github.com/bitcoindevkit/bdk", tag = "v1.0.0-beta.2" }
[[example]]
name = "bdk_sqlx_postgres"
path = "examples/bdk_sqlx_postgres.rs"
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,43 @@
# bdk-sqlx

## Status

This crate is still **EXPERIMENTAL** do not use with mainnet wallets.

## Testing

1. Install postgresql with `psql` tool. For example (macos):
```
brew update
brew install postgresql
```
2. Create empty test database:
```
psql postgres
postgres=# create database test_bdk_wallet;
```
3. Set DATABASE_URL to test database:
```
export DATABASE_TEST_URL=postgresql://localhost/test_bdk_wallet
```
4. Run tests, must use a single test thread since we reuse the postgres db:
```
cargo test -- --test-threads=1
```

## Example

1. Create empty test database:
```
psql postgres
postgres=# create database example_bdk_wallet;
postgres=# \q
```
2. Set DATABASE_URL to test database:
```
export DATABASE_URL=postgresql://localhost/example_bdk_wallet
```
3. Run example:
```
cargo run --example bdk_sqlx_postgres
```
19 changes: 9 additions & 10 deletions src/main.rs → examples/bdk_sqlx_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ use std::collections::HashSet;
use std::io::Write;

use bdk_electrum::{electrum_client, BdkElectrumClient};
use bdk_sqlx::sqlx::Postgres;
use bdk_sqlx::Store;
use bdk_wallet::bitcoin::secp256k1::Secp256k1;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::{KeychainKind, PersistedWallet, Wallet};
use better_panic::Settings;
use rustls::crypto::ring::default_provider;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
Expand All @@ -34,15 +34,12 @@ async fn main() -> anyhow::Result<()> {
default_provider()
.install_default()
.expect("Failed to install rustls default crypto provider");
Settings::debug()
.most_recent_first(false)
.lineno_suffix(true)
.install();

tracing_subscriber::registry()
.with(EnvFilter::new(std::env::var("RUST_LOG").unwrap_or_else(
|_| {
"sqlx=warn,\
bdk_sqlx=info"
bdk_sqlx=debug"
.into()
},
)))
Expand All @@ -59,7 +56,8 @@ async fn main() -> anyhow::Result<()> {
NETWORK,
&secp,
)?;
let mut store = bdk_sqlx::Store::new_with_url(url.clone(), Some(wallet_name)).await?;
let mut store =
bdk_sqlx::Store::<Postgres>::new_with_url(url.clone(), wallet_name, true).await?;

let mut wallet = match Wallet::load().load_wallet_async(&mut store).await? {
Some(wallet) => wallet,
Expand All @@ -86,7 +84,8 @@ async fn main() -> anyhow::Result<()> {
let wallet_name =
bdk_wallet::wallet_name_from_descriptor(VAULT_DESC, Some(CHANGE_DESC), NETWORK, &secp)?;

let mut store = bdk_sqlx::Store::new_with_url(url.clone(), Some(wallet_name)).await?;
let mut store =
bdk_sqlx::Store::<Postgres>::new_with_url(url.clone(), wallet_name, true).await?;

let mut wallet = match Wallet::load().load_wallet_async(&mut store).await? {
Some(wallet) => wallet,
Expand Down Expand Up @@ -114,10 +113,10 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}

fn electrum(wallet: &mut PersistedWallet<Store>) -> anyhow::Result<()> {
fn electrum(wallet: &mut PersistedWallet<Store<Postgres>>) -> anyhow::Result<()> {
let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?);

// Populate the electrum client's transaction cache so it doesn't redownload transaction we
// Populate the electrum client's transaction cache so it doesn't re-download transaction we
// already have.
client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS keychain (
-- Hash is block hash hex string,
-- Block height is a u32
CREATE TABLE IF NOT EXISTS block (
wallet_name TEXT,
wallet_name TEXT NOT NULL,
hash TEXT NOT NULL,
height INTEGER NOT NULL,
PRIMARY KEY (wallet_name, hash)
Expand All @@ -37,7 +37,7 @@ CREATE INDEX idx_block_height ON block (height);
-- Whole_tx is a consensus encoded transaction,
-- Last seen is a u64 unix epoch seconds
CREATE TABLE IF NOT EXISTS tx (
wallet_name TEXT,
wallet_name TEXT NOT NULL,
txid TEXT NOT NULL,
whole_tx BYTEA,
last_seen BIGINT,
Expand All @@ -49,7 +49,7 @@ CREATE TABLE IF NOT EXISTS tx (
-- TxOut value as SATs
-- TxOut script consensus encoded
CREATE TABLE IF NOT EXISTS txout (
wallet_name TEXT,
wallet_name TEXT NOT NULL,
txid TEXT NOT NULL,
vout INTEGER NOT NULL,
value BIGINT NOT NULL,
Expand All @@ -62,7 +62,7 @@ CREATE TABLE IF NOT EXISTS txout (
-- Anchor is a json serialized Anchor structure as JSONB,
-- Txid is transaction hash hex string (reversed)
CREATE TABLE IF NOT EXISTS anchor_tx (
wallet_name TEXT,
wallet_name TEXT NOT NULL,
block_hash TEXT NOT NULL,
anchor JSONB NOT NULL,
txid TEXT NOT NULL,
Expand Down
73 changes: 73 additions & 0 deletions migrations/sqlite/01_bdk_wallet.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
-- Schema version control
CREATE TABLE IF NOT EXISTS version (
version INTEGER PRIMARY KEY
);

-- Network is the valid network for all other table data
CREATE TABLE IF NOT EXISTS network (
wallet_name TEXT PRIMARY KEY,
name TEXT NOT NULL
);

-- Keychain is the json serialized keychain structure as JSONB,
-- descriptor is the complete descriptor string,
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum,
-- last revealed index is a u32
CREATE TABLE IF NOT EXISTS keychain (
wallet_name TEXT NOT NULL,
keychainkind TEXT NOT NULL,
descriptor TEXT NOT NULL,
descriptor_id BLOB NOT NULL,
last_revealed INTEGER DEFAULT 0,
PRIMARY KEY (wallet_name, keychainkind)

);

-- Hash is block hash hex string,
-- Block height is a u32
CREATE TABLE IF NOT EXISTS block (
wallet_name TEXT NOT NULL,
hash TEXT NOT NULL,
height INTEGER NOT NULL,
PRIMARY KEY (wallet_name, hash)
);
CREATE INDEX idx_block_height ON block (height);

-- Txid is transaction hash hex string (reversed)
-- Whole_tx is a consensus encoded transaction,
-- Last seen is a u64 unix epoch seconds
CREATE TABLE IF NOT EXISTS tx (
wallet_name TEXT NOT NULL,
txid TEXT NOT NULL,
whole_tx BLOB,
last_seen INTEGER,
PRIMARY KEY (wallet_name, txid)
);

-- Outpoint txid hash hex string (reversed)
-- Outpoint vout
-- TxOut value as SATs
-- TxOut script consensus encoded
CREATE TABLE IF NOT EXISTS txout (
wallet_name TEXT NOT NULL,
txid TEXT NOT NULL,
vout INTEGER NOT NULL,
value INTEGER NOT NULL,
script BLOB NOT NULL,
PRIMARY KEY (wallet_name, txid, vout)
);

-- Join table between anchor and tx
-- Block hash hex string
-- Anchor is a json serialized Anchor structure as JSONB,
-- Txid is transaction hash hex string (reversed)
CREATE TABLE IF NOT EXISTS anchor_tx (
wallet_name TEXT NOT NULL,
block_hash TEXT NOT NULL,
anchor BLOB NOT NULL,
txid TEXT NOT NULL,
PRIMARY KEY (wallet_name, block_hash, txid),
FOREIGN KEY (wallet_name, block_hash) REFERENCES block(wallet_name, hash),
FOREIGN KEY (wallet_name, txid) REFERENCES tx(wallet_name, txid)
);
CREATE INDEX idx_anchor_tx_txid ON anchor_tx (txid);
Loading