Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit a958d61

Browse files
committed
Send v2 payjoin
1 parent c4e239c commit a958d61

2 files changed

Lines changed: 94 additions & 45 deletions

File tree

mutiny-core/src/nodemanager.rs

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ use payjoin::Uri;
5252
use reqwest::Client;
5353
use serde::{Deserialize, Serialize};
5454
use serde_json::Value;
55-
use std::io::Cursor;
5655
use std::str::FromStr;
5756
use std::sync::atomic::{AtomicBool, Ordering};
5857
use std::{collections::HashMap, ops::Deref, sync::Arc};
@@ -811,76 +810,127 @@ impl<S: MutinyStorage> NodeManager<S> {
811810
Ok(enroller.process_res(ohttp_response.as_ref(), context)?)
812811
}
813812

814-
// Send v1 payjoin request
813+
// Send v2 payjoin request
815814
pub async fn send_payjoin(
816815
&self,
817816
uri: Uri<'_, payjoin::bitcoin::address::NetworkChecked>,
818817
amount: u64,
819818
labels: Vec<String>,
820819
fee_rate: Option<f32>,
821-
) -> Result<Txid, MutinyError> {
820+
) -> Result<(), MutinyError> {
822821
let address = Address::from_str(&uri.address.to_string())
823822
.map_err(|_| MutinyError::InvalidArgumentsError)?;
824823
let original_psbt = self.wallet.create_signed_psbt(address, amount, fee_rate)?;
825-
824+
// TODO ensure this creates a pending tx in the UI. Ensure locked UTXO.
826825
let fee_rate = if let Some(rate) = fee_rate {
827826
FeeRate::from_sat_per_vb(rate)
828827
} else {
829828
let sat_per_kwu = self.fee_estimator.get_normal_fee_rate();
830829
FeeRate::from_sat_per_kwu(sat_per_kwu as f32)
831830
};
832831
let fee_rate = payjoin::bitcoin::FeeRate::from_sat_per_kwu(fee_rate.sat_per_kwu() as u64);
833-
let original_psbt = payjoin::bitcoin::psbt::PartiallySignedTransaction::from_str(
832+
let original_psbt_30 = payjoin::bitcoin::psbt::PartiallySignedTransaction::from_str(
834833
&original_psbt.to_string(),
835834
)
836835
.map_err(|_| MutinyError::WalletOperationFailed)?;
837836
log_debug!(self.logger, "Creating payjoin request");
838-
let (req, ctx) =
839-
payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), uri)
840-
.unwrap()
841-
.build_recommended(fee_rate)
842-
.map_err(|_| MutinyError::PayjoinCreateRequest)?
843-
.extract_v1()?;
837+
let req_ctx = payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt_30, uri)
838+
.unwrap()
839+
.build_recommended(fee_rate)
840+
.map_err(|_| MutinyError::PayjoinConfigError)?;
841+
self.spawn_payjoin_sender(labels, original_psbt, req_ctx)
842+
.await;
843+
Ok(())
844+
}
844845

845-
let client = Client::builder()
846-
.build()
847-
.map_err(|e| MutinyError::Other(e.into()))?;
846+
async fn spawn_payjoin_sender(
847+
&self,
848+
labels: Vec<String>,
849+
original_psbt: bitcoin::psbt::Psbt,
850+
req_ctx: payjoin::send::RequestContext,
851+
) {
852+
let wallet = self.wallet.clone();
853+
let logger = self.logger.clone();
854+
let stop = self.stop.clone();
855+
utils::spawn(async move {
856+
let proposal_psbt = match Self::poll_payjoin_sender(stop, req_ctx).await {
857+
Ok(psbt) => psbt,
858+
Err(e) => {
859+
log_error!(logger, "Error polling payjoin sender: {e}");
860+
return;
861+
}
862+
};
848863

849-
log_debug!(self.logger, "Sending payjoin request");
850-
let res = client
851-
.post(req.url)
852-
.body(req.body)
853-
.header("Content-Type", "text/plain")
854-
.send()
855-
.await
856-
.map_err(|_| MutinyError::PayjoinCreateRequest)?
857-
.bytes()
864+
if let Err(e) = Self::handle_proposal_psbt(
865+
logger.clone(),
866+
wallet,
867+
original_psbt,
868+
proposal_psbt,
869+
labels,
870+
)
858871
.await
859-
.map_err(|_| MutinyError::PayjoinCreateRequest)?;
860-
861-
let mut cursor = Cursor::new(res.to_vec());
872+
{
873+
// Ensure ResponseError is logged with debug formatting
874+
log_error!(logger, "Error handling payjoin proposal: {:?}", e);
875+
}
876+
});
877+
}
862878

863-
log_debug!(self.logger, "Processing payjoin response");
864-
let proposal_psbt = ctx.process_response(&mut cursor).map_err(|e| {
865-
// unrecognized error contents may only appear in debug logs and will not Display
866-
log_debug!(self.logger, "Payjoin response error: {:?}", e);
867-
e
868-
})?;
879+
async fn poll_payjoin_sender(
880+
stop: Arc<AtomicBool>,
881+
req_ctx: payjoin::send::RequestContext,
882+
) -> Result<bitcoin::psbt::Psbt, MutinyError> {
883+
let http = Client::builder()
884+
.build()
885+
.map_err(|_| MutinyError::Other(anyhow!("failed to build http client")))?;
886+
loop {
887+
if stop.load(Ordering::Relaxed) {
888+
return Err(MutinyError::NotRunning);
889+
}
869890

870-
// convert to pdk types
871-
let original_psbt = PartiallySignedTransaction::from_str(&original_psbt.to_string())
872-
.map_err(|_| MutinyError::PayjoinConfigError)?;
873-
let proposal_psbt = PartiallySignedTransaction::from_str(&proposal_psbt.to_string())
874-
.map_err(|_| MutinyError::PayjoinConfigError)?;
891+
let (req, ctx) = req_ctx
892+
.extract_v2(crate::payjoin::OHTTP_RELAYS[0])
893+
.map_err(|_| MutinyError::PayjoinConfigError)?;
894+
let response = http
895+
.post(req.url)
896+
.body(req.body)
897+
.send()
898+
.await
899+
.map_err(|_| MutinyError::Other(anyhow!("failed to parse payjoin response")))?;
900+
let mut reader =
901+
std::io::Cursor::new(response.bytes().await.map_err(|_| {
902+
MutinyError::Other(anyhow!("failed to parse payjoin response"))
903+
})?);
904+
905+
println!("Sent fallback transaction");
906+
let psbt = ctx
907+
.process_response(&mut reader)
908+
.map_err(MutinyError::PayjoinResponse)?;
909+
if let Some(psbt) = psbt {
910+
let psbt = bitcoin::psbt::Psbt::from_str(&psbt.to_string())
911+
.map_err(|_| MutinyError::Other(anyhow!("psbt conversion failed")))?;
912+
return Ok(psbt);
913+
} else {
914+
log::info!("No response yet for POST payjoin request, retrying some seconds");
915+
std::thread::sleep(std::time::Duration::from_secs(5));
916+
}
917+
}
918+
}
875919

876-
log_debug!(self.logger, "Sending payjoin..");
877-
let tx = self
878-
.wallet
920+
async fn handle_proposal_psbt(
921+
logger: Arc<MutinyLogger>,
922+
wallet: Arc<OnChainWallet<S>>,
923+
original_psbt: PartiallySignedTransaction,
924+
proposal_psbt: PartiallySignedTransaction,
925+
labels: Vec<String>,
926+
) -> Result<Txid, MutinyError> {
927+
log_debug!(logger, "Sending payjoin..");
928+
let tx = wallet
879929
.send_payjoin(original_psbt, proposal_psbt, labels)
880930
.await?;
881931
let txid = tx.txid();
882-
self.broadcast_transaction(tx).await?;
883-
log_debug!(self.logger, "Payjoin broadcast! TXID: {txid}");
932+
wallet.broadcast_transaction(tx).await?;
933+
log_info!(logger, "Payjoin broadcast! TXID: {txid}");
884934
Ok(txid)
885935
}
886936

mutiny-wasm/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ impl MutinyWallet {
483483
amount: u64, /* override the uri amount if desired */
484484
labels: Vec<String>,
485485
fee_rate: Option<f32>,
486-
) -> Result<String, MutinyJsError> {
486+
) -> Result<(), MutinyJsError> {
487487
// I know walia parses `pj=` and `pjos=` but payjoin::Uri parses the whole bip21 uri
488488
let pj_uri = payjoin::Uri::try_from(payjoin_uri.as_str())
489489
.map_err(|_| MutinyJsError::InvalidArgumentsError)?
@@ -492,8 +492,7 @@ impl MutinyWallet {
492492
.inner
493493
.node_manager
494494
.send_payjoin(pj_uri, amount, labels, fee_rate)
495-
.await?
496-
.to_string())
495+
.await?)
497496
}
498497

499498
/// Sweeps all the funds from the wallet to the given address.

0 commit comments

Comments
 (0)