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

Commit 841cbe6

Browse files
committed
Send v2 payjoin
1 parent f17c3f8 commit 841cbe6

File tree

2 files changed

+94
-45
lines changed

2 files changed

+94
-45
lines changed

mutiny-core/src/nodemanager.rs

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ use reqwest::Client;
5454
use serde::{Deserialize, Serialize};
5555
use serde_json::Value;
5656
use std::cmp::max;
57-
use std::io::Cursor;
5857
use std::str::FromStr;
5958
use std::sync::atomic::{AtomicBool, Ordering};
6059
use std::{collections::HashMap, ops::Deref, sync::Arc};
@@ -698,76 +697,127 @@ impl<S: MutinyStorage> NodeManager<S> {
698697
Ok(enroller.process_res(ohttp_response.as_ref(), context)?)
699698
}
700699

701-
// Send v1 payjoin request
700+
// Send v2 payjoin request
702701
pub async fn send_payjoin(
703702
&self,
704703
uri: Uri<'_, payjoin::bitcoin::address::NetworkChecked>,
705704
amount: u64,
706705
labels: Vec<String>,
707706
fee_rate: Option<f32>,
708-
) -> Result<Txid, MutinyError> {
707+
) -> Result<(), MutinyError> {
709708
let address = Address::from_str(&uri.address.to_string())
710709
.map_err(|_| MutinyError::InvalidArgumentsError)?;
711710
let original_psbt = self.wallet.create_signed_psbt(address, amount, fee_rate)?;
712-
711+
// TODO ensure this creates a pending tx in the UI. Ensure locked UTXO.
713712
let fee_rate = if let Some(rate) = fee_rate {
714713
FeeRate::from_sat_per_vb(rate)
715714
} else {
716715
let sat_per_kwu = self.fee_estimator.get_normal_fee_rate();
717716
FeeRate::from_sat_per_kwu(sat_per_kwu as f32)
718717
};
719718
let fee_rate = payjoin::bitcoin::FeeRate::from_sat_per_kwu(fee_rate.sat_per_kwu() as u64);
720-
let original_psbt = payjoin::bitcoin::psbt::PartiallySignedTransaction::from_str(
719+
let original_psbt_30 = payjoin::bitcoin::psbt::PartiallySignedTransaction::from_str(
721720
&original_psbt.to_string(),
722721
)
723722
.map_err(|_| MutinyError::WalletOperationFailed)?;
724723
log_debug!(self.logger, "Creating payjoin request");
725-
let (req, ctx) =
726-
payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), uri)
727-
.unwrap()
728-
.build_recommended(fee_rate)
729-
.map_err(|_| MutinyError::PayjoinCreateRequest)?
730-
.extract_v1()?;
724+
let req_ctx = payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt_30, uri)
725+
.unwrap()
726+
.build_recommended(fee_rate)
727+
.map_err(|_| MutinyError::PayjoinConfigError)?;
728+
self.spawn_payjoin_sender(labels, original_psbt, req_ctx)
729+
.await;
730+
Ok(())
731+
}
731732

732-
let client = Client::builder()
733-
.build()
734-
.map_err(|e| MutinyError::Other(e.into()))?;
733+
async fn spawn_payjoin_sender(
734+
&self,
735+
labels: Vec<String>,
736+
original_psbt: bitcoin::psbt::Psbt,
737+
req_ctx: payjoin::send::RequestContext,
738+
) {
739+
let wallet = self.wallet.clone();
740+
let logger = self.logger.clone();
741+
let stop = self.stop.clone();
742+
utils::spawn(async move {
743+
let proposal_psbt = match Self::poll_payjoin_sender(stop, req_ctx).await {
744+
Ok(psbt) => psbt,
745+
Err(e) => {
746+
log_error!(logger, "Error polling payjoin sender: {e}");
747+
return;
748+
}
749+
};
735750

736-
log_debug!(self.logger, "Sending payjoin request");
737-
let res = client
738-
.post(req.url)
739-
.body(req.body)
740-
.header("Content-Type", "text/plain")
741-
.send()
742-
.await
743-
.map_err(|_| MutinyError::PayjoinCreateRequest)?
744-
.bytes()
751+
if let Err(e) = Self::handle_proposal_psbt(
752+
logger.clone(),
753+
wallet,
754+
original_psbt,
755+
proposal_psbt,
756+
labels,
757+
)
745758
.await
746-
.map_err(|_| MutinyError::PayjoinCreateRequest)?;
747-
748-
let mut cursor = Cursor::new(res.to_vec());
759+
{
760+
// Ensure ResponseError is logged with debug formatting
761+
log_error!(logger, "Error handling payjoin proposal: {:?}", e);
762+
}
763+
});
764+
}
749765

750-
log_debug!(self.logger, "Processing payjoin response");
751-
let proposal_psbt = ctx.process_response(&mut cursor).map_err(|e| {
752-
// unrecognized error contents may only appear in debug logs and will not Display
753-
log_debug!(self.logger, "Payjoin response error: {:?}", e);
754-
e
755-
})?;
766+
async fn poll_payjoin_sender(
767+
stop: Arc<AtomicBool>,
768+
req_ctx: payjoin::send::RequestContext,
769+
) -> Result<bitcoin::psbt::Psbt, MutinyError> {
770+
let http = Client::builder()
771+
.build()
772+
.map_err(|_| MutinyError::Other(anyhow!("failed to build http client")))?;
773+
loop {
774+
if stop.load(Ordering::Relaxed) {
775+
return Err(MutinyError::NotRunning);
776+
}
756777

757-
// convert to pdk types
758-
let original_psbt = PartiallySignedTransaction::from_str(&original_psbt.to_string())
759-
.map_err(|_| MutinyError::PayjoinConfigError)?;
760-
let proposal_psbt = PartiallySignedTransaction::from_str(&proposal_psbt.to_string())
761-
.map_err(|_| MutinyError::PayjoinConfigError)?;
778+
let (req, ctx) = req_ctx
779+
.extract_v2(crate::payjoin::OHTTP_RELAYS[0])
780+
.map_err(|_| MutinyError::PayjoinConfigError)?;
781+
let response = http
782+
.post(req.url)
783+
.body(req.body)
784+
.send()
785+
.await
786+
.map_err(|_| MutinyError::Other(anyhow!("failed to parse payjoin response")))?;
787+
let mut reader =
788+
std::io::Cursor::new(response.bytes().await.map_err(|_| {
789+
MutinyError::Other(anyhow!("failed to parse payjoin response"))
790+
})?);
791+
792+
println!("Sent fallback transaction");
793+
let psbt = ctx
794+
.process_response(&mut reader)
795+
.map_err(MutinyError::PayjoinResponse)?;
796+
if let Some(psbt) = psbt {
797+
let psbt = bitcoin::psbt::Psbt::from_str(&psbt.to_string())
798+
.map_err(|_| MutinyError::Other(anyhow!("psbt conversion failed")))?;
799+
return Ok(psbt);
800+
} else {
801+
log::info!("No response yet for POST payjoin request, retrying some seconds");
802+
std::thread::sleep(std::time::Duration::from_secs(5));
803+
}
804+
}
805+
}
762806

763-
log_debug!(self.logger, "Sending payjoin..");
764-
let tx = self
765-
.wallet
807+
async fn handle_proposal_psbt(
808+
logger: Arc<MutinyLogger>,
809+
wallet: Arc<OnChainWallet<S>>,
810+
original_psbt: PartiallySignedTransaction,
811+
proposal_psbt: PartiallySignedTransaction,
812+
labels: Vec<String>,
813+
) -> Result<Txid, MutinyError> {
814+
log_debug!(logger, "Sending payjoin..");
815+
let tx = wallet
766816
.send_payjoin(original_psbt, proposal_psbt, labels)
767817
.await?;
768818
let txid = tx.txid();
769-
self.broadcast_transaction(tx).await?;
770-
log_debug!(self.logger, "Payjoin broadcast! TXID: {txid}");
819+
wallet.broadcast_transaction(tx).await?;
820+
log_info!(logger, "Payjoin broadcast! TXID: {txid}");
771821
Ok(txid)
772822
}
773823

mutiny-wasm/src/lib.rs

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

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

0 commit comments

Comments
 (0)