diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index 59a1042d0..0e0066de8 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -39,6 +39,7 @@ nostr-sdk = { version = "0.22.0-bitcoin-v0.29", default-features = false } cbc = { version = "0.1", features = ["alloc"] } aes = { version = "0.8" } jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] } +payjoin = { version = "0.8.1", features = ["send"] } base64 = "0.13.0" pbkdf2 = "0.11" diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index e88755e24..295661747 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -978,6 +978,23 @@ impl NodeManager { }) } + pub async fn send_payjoin( + &self, + send_to: Address, + amount: u64, + labels: Vec, + fee_rate: Option, // Why not FeeRate? + pj: String, + ) -> Result { + if !send_to.is_valid_for_network(self.network) { + return Err(MutinyError::IncorrectNetwork(send_to.network)); + } + + self.wallet + .send_payjoin(send_to, amount, labels, fee_rate, pj) + .await + } + /// Sends an on-chain transaction to the given address. /// The amount is in satoshis and the fee rate is in sat/vbyte. /// diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index eab337b51..61f0196c8 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use payjoin::{UriExt, PjUriExt}; use std::collections::HashSet; use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; @@ -404,6 +405,58 @@ impl OnChainWallet { Ok(txid) } + pub async fn send_payjoin( + &self, + uri: String, + amount: u64, + labels: Vec, + fee_rate: Option, + ) -> Result { + let uri = payjoin::Uri::try_from(uri) + .expect("Invalid payjoin URI") + .require_network(self.network) + .expect("Payjoin network mismatch") + .check_pj_supported() + .expect("Payjoin not supported"); + + let amount = uri.amount().unwrap_or(amount); + + // is this finalized? + let original_psbt = self.create_signed_psbt(uri.address, amount, Some(fee_rate))?; + + let pj_params = payjoin::send::Configuration::min_fee_rate_sat_per_vb(self, fee_rate); + let (req, ctx) = uri.create_pj_request(psbt, pj_params).expect("Could not create Payjoin request"); + + let client = reqwest::ClientBuilder::new() + .build() + .expect("Could not build HTTP client"); + + let &mut res = client + .post(req.url) + .body(req.body) + .header("Content-Type", "text/plain") + .send() + .await + .expect("Payjoin request failed") + .text() + .await + .expect("Could not read Payjoin response"); + + let mut proposal_psbt = ctx.process_response(res).expect("Could not process Payjoin response"); + + // TODO add original psbt input map data in place so BDK knows which scripts to sign, + // proposal_psbt only contains the sender input outpoints, not scripts, which BDK + // does not look up + + // sign and finalize payjoin + let payjoin = self.wallet.read().unwrap().sign(&mut psbt, SignOptions::default())?.extract_tx(); + let txid = payjoin.txid(); + + self.broadcast_transaction(payjoin.clone()).await?; + log_debug!(self.logger, "Payjoin broadcast! TXID: {txid}"); + Ok(txid) + } + pub fn create_sweep_psbt( &self, spk: Script,