From 363826dd8a67f7ac4daa69e7cb94cfea7f6e50e4 Mon Sep 17 00:00:00 2001 From: DanGould Date: Thu, 21 Dec 2023 17:32:07 -0500 Subject: [PATCH] Send v2 payjoin --- mutiny-core/src/nodemanager.rs | 132 +++++++++++++++++++++++---------- mutiny-wasm/src/lib.rs | 5 +- 2 files changed, 93 insertions(+), 44 deletions(-) diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 9ce461740..838c093f1 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -52,7 +52,6 @@ use payjoin::Uri; use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::io::Cursor; use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use std::{collections::HashMap, ops::Deref, sync::Arc}; @@ -811,18 +810,18 @@ impl NodeManager { Ok(enroller.process_res(ohttp_response.as_ref(), context)?) } - // Send v1 payjoin request + // Send v2 payjoin request pub async fn send_payjoin( &self, uri: Uri<'_, payjoin::bitcoin::address::NetworkChecked>, amount: u64, labels: Vec, fee_rate: Option, - ) -> Result { + ) -> Result<(), MutinyError> { let address = Address::from_str(&uri.address.to_string()) .map_err(|_| MutinyError::PayjoinConfigError)?; let original_psbt = self.wallet.create_signed_psbt(address, amount, fee_rate)?; - + // TODO ensure this creates a pending tx in the UI. Ensure locked UTXO. let fee_rate = if let Some(rate) = fee_rate { FeeRate::from_sat_per_vb(rate) } else { @@ -830,56 +829,107 @@ impl NodeManager { FeeRate::from_sat_per_kwu(sat_per_kwu as f32) }; let fee_rate = payjoin::bitcoin::FeeRate::from_sat_per_kwu(fee_rate.sat_per_kwu() as u64); - let original_psbt = payjoin::bitcoin::psbt::PartiallySignedTransaction::from_str( + let original_psbt_30 = payjoin::bitcoin::psbt::PartiallySignedTransaction::from_str( &original_psbt.to_string(), ) .map_err(|_| MutinyError::PayjoinConfigError)?; log_debug!(self.logger, "Creating payjoin request"); - let (req, ctx) = - payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), uri) - .unwrap() - .build_recommended(fee_rate) - .map_err(|_| MutinyError::PayjoinConfigError)? - .extract_v1()?; - - let client = Client::builder() - .build() + let req_ctx = payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt_30, uri) + .unwrap() + .build_recommended(fee_rate) .map_err(|_| MutinyError::PayjoinConfigError)?; + self.spawn_payjoin_sender(labels, original_psbt, req_ctx) + .await; + Ok(()) + } - log_debug!(self.logger, "Sending payjoin request"); - let res = client - .post(req.url) - .body(req.body) - .header("Content-Type", "text/plain") - .send() - .await - .map_err(|_| MutinyError::PayjoinCreateRequest)? - .bytes() - .await - .map_err(|_| MutinyError::PayjoinCreateRequest)?; + async fn spawn_payjoin_sender( + &self, + labels: Vec, + original_psbt: bitcoin::psbt::Psbt, + req_ctx: payjoin::send::RequestContext, + ) { + let wallet = self.wallet.clone(); + let logger = self.logger.clone(); + let stop = self.stop.clone(); + utils::spawn(async move { + let proposal_psbt = match Self::poll_payjoin_sender(stop, req_ctx).await { + Ok(psbt) => psbt, + Err(e) => { + log_error!(logger, "Error polling payjoin sender: {e}"); + return; + } + }; - let mut cursor = Cursor::new(res.to_vec()); + if let Err(e) = Self::handle_proposal_psbt( + logger.clone(), + wallet, + original_psbt, + proposal_psbt, + labels, + ) + .await + { + log_error!(logger, "Error handling payjoin proposal: {e}"); + } + }); + } - log_debug!(self.logger, "Processing payjoin response"); - let proposal_psbt = ctx.process_response(&mut cursor).map_err(|e| { - log_error!(self.logger, "Error processing payjoin response: {e}"); - e - })?; + async fn poll_payjoin_sender( + stop: Arc, + req_ctx: payjoin::send::RequestContext, + ) -> Result { + let http = Client::builder() + .build() + .map_err(|_| MutinyError::Other(anyhow!("failed to build http client")))?; + loop { + if stop.load(Ordering::Relaxed) { + return Err(MutinyError::NotRunning); + } - // convert to pdk types - let original_psbt = PartiallySignedTransaction::from_str(&original_psbt.to_string()) - .map_err(|_| MutinyError::PayjoinConfigError)?; - let proposal_psbt = PartiallySignedTransaction::from_str(&proposal_psbt.to_string()) - .map_err(|_| MutinyError::PayjoinConfigError)?; + let (req, ctx) = req_ctx + .extract_v2(crate::payjoin::OHTTP_RELAYS[0]) + .map_err(|_| MutinyError::PayjoinConfigError)?; + let response = http + .post(req.url) + .body(req.body) + .send() + .await + .map_err(|_| MutinyError::Other(anyhow!("failed to parse payjoin response")))?; + let mut reader = + std::io::Cursor::new(response.bytes().await.map_err(|_| { + MutinyError::Other(anyhow!("failed to parse payjoin response")) + })?); + + println!("Sent fallback transaction"); + let psbt = ctx + .process_response(&mut reader) + .map_err(MutinyError::PayjoinResponse)?; + if let Some(psbt) = psbt { + let psbt = bitcoin::psbt::Psbt::from_str(&psbt.to_string()) + .map_err(|_| MutinyError::Other(anyhow!("psbt conversion failed")))?; + return Ok(psbt); + } else { + log::info!("No response yet for POST payjoin request, retrying some seconds"); + std::thread::sleep(std::time::Duration::from_secs(5)); + } + } + } - log_debug!(self.logger, "Sending payjoin.."); - let tx = self - .wallet + async fn handle_proposal_psbt( + logger: Arc, + wallet: Arc>, + original_psbt: PartiallySignedTransaction, + proposal_psbt: PartiallySignedTransaction, + labels: Vec, + ) -> Result { + log_debug!(logger, "Sending payjoin.."); + let tx = wallet .send_payjoin(original_psbt, proposal_psbt, labels) .await?; let txid = tx.txid(); - self.broadcast_transaction(tx).await?; - log_debug!(self.logger, "Payjoin broadcast! TXID: {txid}"); + wallet.broadcast_transaction(tx).await?; + log_info!(logger, "Payjoin broadcast! TXID: {txid}"); Ok(txid) } diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index b15a9e35d..9dcc7032c 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -483,7 +483,7 @@ impl MutinyWallet { amount: u64, /* override the uri amount if desired */ labels: Vec, fee_rate: Option, - ) -> Result { + ) -> Result<(), MutinyJsError> { // I know walia parses `pj=` and `pjos=` but payjoin::Uri parses the whole bip21 uri let pj_uri = payjoin::Uri::try_from(payjoin_uri.as_str()) .map_err(|_| MutinyJsError::InvalidArgumentsError)? @@ -492,8 +492,7 @@ impl MutinyWallet { .inner .node_manager .send_payjoin(pj_uri, amount, labels, fee_rate) - .await? - .to_string()) + .await?) } /// Sweeps all the funds from the wallet to the given address.