Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ esplora = ["bdk_esplora", "_payjoin-dependencies"]
rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"]

# Internal features
_payjoin-dependencies = ["payjoin", "reqwest", "url"]
_payjoin-dependencies = ["payjoin", "reqwest", "url", "sqlite"]

# Use this to consensus verify transactions at sync time
verify = []
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ And yes, it can do Taproot!!
This crate can be used for the following purposes:
- Instantly create a miniscript based wallet and connect to your backend of choice (Electrum, Esplora, Core RPC, Kyoto etc) and quickly play around with your own complex bitcoin scripting workflow. With one or many wallets, connected with one or many backends.
- The `tests/integration.rs` module is used to document high level complex workflows between BDK and different Bitcoin infrastructure systems, like Core, Electrum and Lightning(soon TM).
- Receive and send Async Payjoins. Note that even though Async Payjoin as a protocol allows the receiver and sender to go offline during the payjoin, the BDK CLI implementation currently does not support persisting.
- Receive and send Async Payjoins with session persistence. Sessions can be resumed if interrupted.
- (Planned) Expose the basic command handler via `wasm` to integrate `bdk-cli` functionality natively into the web platform. See also the [playground](https://bitcoindevkit.org/bdk-cli/playground/) page.

If you are considering using BDK in your own wallet project bdk-cli is a nice playground to get started with. It allows easy testnet and regtest wallet operations, to try out what's possible with descriptors, miniscript, and BDK APIs. For more information on BDK refer to the [website](https://bitcoindevkit.org/) and the [rust docs](https://docs.rs/bdk_wallet/1.0.0/bdk_wallet/index.html)
Expand Down Expand Up @@ -140,6 +140,31 @@ cargo run --features rpc -- wallet --wallet payjoin_wallet2 balance
cargo run --features rpc -- wallet --wallet payjoin_wallet2 send_payjoin --ohttp_relay "https://pj.bobspacebkk.com" --ohttp_relay "https://pj.benalleng.com" --fee_rate 1 --uri "<URI>"
```

### Payjoin Session Persistence

Payjoin sessions are automatically persisted to a SQLite database (`payjoin.sqlite`) in the data directory. This allows sessions to be resumed if interrupted.

#### Resume Payjoin Sessions

Resume all pending sessions:
```
cargo run --features rpc -- wallet --wallet <wallet_name> resume_payjoin --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com"
```

Resume a specific session by ID:
```
cargo run --features rpc -- wallet --wallet <wallet_name> resume_payjoin --directory "https://payjo.in" --ohttp_relay "https://pj.bobspacebkk.com" --session_id <id>
```

Sessions are processed sequentially (not concurrently) due to BDK-CLI's architecture. Each session waits up to 30 seconds for updates before timing out. If no session ID is specified, the most recent active sessions are resumed first.

#### View Session History

View all payjoin sessions (active and completed) and also see their status:
```
cargo run -- wallet --wallet <wallet_name> payjoin_history
```

## Justfile

We have added the `just` command runner to help you with common commands (during development) and running regtest `bitcoind` if you are using the `rpc` feature.
Expand Down
14 changes: 14 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,20 @@ pub enum OnlineWalletSubCommand {
)]
fee_rate: u64,
},
/// Resume pending payjoin sessions.
ResumePayjoin {
/// Payjoin directory for the session
#[arg(env = "PAYJOIN_DIRECTORY", long = "directory", required = true)]
directory: String,
/// URL of the Payjoin OHTTP relay. Can be repeated multiple times.
#[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)]
ohttp_relay: Vec<String>,
/// Resume only a specific active session ID (sender and/or receiver).
#[arg(env = "PAYJOIN_SESSION_ID", long = "session_id")]
session_id: Option<i64>,
},
/// Show payjoin session history.
PayjoinHistory,
}

/// Subcommands for Key operations.
Expand Down
65 changes: 65 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ pub enum BDKCliError {
#[cfg(feature = "payjoin")]
#[error("Payjoin create request error: {0}")]
PayjoinCreateRequest(#[from] payjoin::send::v2::CreateRequestError),

#[cfg(feature = "payjoin")]
#[error("Payjoin database error: {0}")]
PayjoinDb(#[from] PayjoinDbError),
}

impl From<ExtractTxError> for BDKCliError {
Expand Down Expand Up @@ -168,3 +172,64 @@ impl From<bdk_wallet::rusqlite::Error> for BDKCliError {
BDKCliError::RusqliteError(Box::new(err))
}
}

/// Error type for payjoin database operations
#[cfg(feature = "payjoin")]
#[derive(Debug)]
pub enum PayjoinDbError {
/// SQLite database error
Rusqlite(bdk_wallet::rusqlite::Error),
/// JSON serialization error
Serialize(serde_json::Error),
/// JSON deserialization error
Deserialize(serde_json::Error),
}

#[cfg(feature = "payjoin")]
impl std::fmt::Display for PayjoinDbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PayjoinDbError::Rusqlite(e) => write!(f, "Database operation failed: {e}"),
PayjoinDbError::Serialize(e) => write!(f, "Serialization failed: {e}"),
PayjoinDbError::Deserialize(e) => write!(f, "Deserialization failed: {e}"),
}
}
}

#[cfg(feature = "payjoin")]
impl std::error::Error for PayjoinDbError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
PayjoinDbError::Rusqlite(e) => Some(e),
PayjoinDbError::Serialize(e) => Some(e),
PayjoinDbError::Deserialize(e) => Some(e),
}
}
}

#[cfg(feature = "payjoin")]
impl From<bdk_wallet::rusqlite::Error> for PayjoinDbError {
fn from(error: bdk_wallet::rusqlite::Error) -> Self {
PayjoinDbError::Rusqlite(error)
}
}

#[cfg(feature = "payjoin")]
impl From<PayjoinDbError> for payjoin::ImplementationError {
fn from(error: PayjoinDbError) -> Self {
payjoin::ImplementationError::new(error)
}
}

#[cfg(feature = "payjoin")]
impl<ApiErr, StorageErr, ErrorState>
From<payjoin::persist::PersistedError<ApiErr, StorageErr, ErrorState>> for BDKCliError
where
ApiErr: std::error::Error,
StorageErr: std::error::Error,
ErrorState: std::fmt::Debug,
{
fn from(e: payjoin::persist::PersistedError<ApiErr, StorageErr, ErrorState>) -> Self {
BDKCliError::Generic(e.to_string())
}
}
53 changes: 45 additions & 8 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,16 @@
}
}

#[cfg(feature = "payjoin")]
pub fn open_payjoin_db(
datadir: Option<std::path::PathBuf>,
) -> Result<std::sync::Arc<crate::payjoin::db::Database>, Error> {
use crate::payjoin::db::{DB_FILENAME, Database};
let home_dir = prepare_home_dir(datadir)?;
let db_path = home_dir.join(DB_FILENAME);
Ok(std::sync::Arc::new(Database::create(&db_path)?))
}

/// Execute an online wallet sub-command
///
/// Online wallet sub-commands are described in [`OnlineWalletSubCommand`].
Expand All @@ -607,6 +617,7 @@
wallet: &mut Wallet,
client: &BlockchainClient,
online_subcommand: OnlineWalletSubCommand,
datadir: Option<std::path::PathBuf>,
) -> Result<String, Error> {
match online_subcommand {
FullScan {
Expand Down Expand Up @@ -724,7 +735,8 @@
max_fee_rate,
} => {
let relay_manager = Arc::new(Mutex::new(RelayManager::new()));
let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager);
let db = open_payjoin_db(datadir.clone())?;
let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager, db);
return payjoin_manager
.receive_payjoin(amount, directory, max_fee_rate, ohttp_relay, client)
.await;
Expand All @@ -735,11 +747,25 @@
fee_rate,
} => {
let relay_manager = Arc::new(Mutex::new(RelayManager::new()));
let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager);
let db = open_payjoin_db(datadir.clone())?;
let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager, db);
return payjoin_manager
.send_payjoin(uri, fee_rate, ohttp_relay, client)
.await;
}
ResumePayjoin {
directory,
ohttp_relay,
session_id,
} => {
let relay_manager = Arc::new(Mutex::new(RelayManager::new()));
let db = open_payjoin_db(datadir)?;
let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager, db);
return payjoin_manager
.resume_payjoins(directory, ohttp_relay, session_id, client)

Check warning on line 765 in src/handlers.rs

View workflow job for this annotation

GitHub Actions / Rust fmt

Diff in /home/runner/work/bdk-cli/bdk-cli/src/handlers.rs
.await;
}
PayjoinHistory => crate::payjoin::PayjoinManager::history(datadir)
}
}

Expand Down Expand Up @@ -1210,7 +1236,7 @@
wallet,
subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
} => {
let home_dir = prepare_home_dir(cli_opts.datadir)?;
let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;

let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet)?;

Expand Down Expand Up @@ -1249,6 +1275,7 @@
&mut wallet,
&blockchain_client,
online_subcommand,
cli_opts.datadir.clone(),
)
.await?;
wallet.persist(&mut persister)?;
Expand All @@ -1259,8 +1286,13 @@
let mut wallet = new_wallet(network, wallet_opts)?;
let blockchain_client =
crate::utils::new_blockchain_client(wallet_opts, &wallet, database_path)?;
handle_online_wallet_subcommand(&mut wallet, &blockchain_client, online_subcommand)
.await?
handle_online_wallet_subcommand(
&mut wallet,
&blockchain_client,
online_subcommand,
cli_opts.datadir.clone(),
)
.await?
};
Ok(result)
}
Expand Down Expand Up @@ -1453,9 +1485,14 @@
} => {
let blockchain =
new_blockchain_client(wallet_opts, wallet, _datadir).map_err(|e| e.to_string())?;
let value = handle_online_wallet_subcommand(wallet, &blockchain, online_subcommand)
.await
.map_err(|e| e.to_string())?;
let value = handle_online_wallet_subcommand(
wallet,
&blockchain,
online_subcommand,
cli_opts.datadir.clone(),
)
.await
.map_err(|e| e.to_string())?;
Some(value)
}
ReplSubCommand::Wallet {
Expand Down
Loading
Loading