Skip to content
Draft
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
93 changes: 93 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,103 @@ pub enum BDKCliError {
))]
#[error("Reqwest error: {0}")]
ReqwestError(#[from] reqwest::Error),

#[cfg(feature = "payjoin")]
#[error("Payjoin URL parse error: {0}")]
PayjoinUrlParse(#[from] payjoin::IntoUrlError),

#[cfg(feature = "payjoin")]
#[error("Payjoin send response error: {0}")]
PayjoinSendResponse(#[from] payjoin::send::ResponseError),

#[cfg(feature = "payjoin")]
#[error("Payjoin sender build error: {0}")]
PayjoinSenderBuild(#[from] payjoin::send::BuildSenderError),

#[cfg(feature = "payjoin")]
#[error("Payjoin receive error: {0}")]
PayjoinReceive(#[from] payjoin::receive::Error),

#[cfg(feature = "payjoin")]
#[error("Payjoin selection error: {0}")]
PayjoinSelection(#[from] payjoin::receive::SelectionError),

#[cfg(feature = "payjoin")]
#[error("Payjoin input contribution error: {0}")]
PayjoinInputContribution(#[from] payjoin::receive::InputContributionError),

#[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 {
fn from(value: ExtractTxError) -> Self {
BDKCliError::PsbtExtractTxError(Box::new(value))
}
}

/// 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())
}
}
Loading
Loading