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

Commit f4bfedf

Browse files
committed
Send v2 payjoin
1 parent 009cb89 commit f4bfedf

File tree

2 files changed

+94
-48
lines changed

2 files changed

+94
-48
lines changed

mutiny-core/src/nodemanager.rs

Lines changed: 92 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ use reqwest::Client;
6262
use serde::{Deserialize, Serialize};
6363
use serde_json::Value;
6464
use std::cmp::max;
65-
use std::io::Cursor;
6665
use std::str::FromStr;
6766
use std::sync::atomic::{AtomicBool, Ordering};
6867
#[cfg(not(target_arch = "wasm32"))]
@@ -756,78 +755,126 @@ impl<S: MutinyStorage> NodeManager<S> {
756755
))
757756
}
758757

759-
// Send v1 payjoin request
758+
// Send v2 payjoin request
760759
pub async fn send_payjoin(
761760
&self,
762761
uri: Uri<'_, NetworkUnchecked>,
763762
amount: u64,
764763
labels: Vec<String>,
765764
fee_rate: Option<f32>,
766-
) -> Result<Txid, MutinyError> {
765+
) -> Result<(), MutinyError> {
767766
let uri = uri
768767
.require_network(self.network)
769768
.map_err(|_| MutinyError::IncorrectNetwork)?;
770769
let address = uri.address.clone();
771770
let original_psbt = self.wallet.create_signed_psbt(address, amount, fee_rate)?;
772-
771+
// TODO ensure this creates a pending tx in the UI. Ensure locked UTXO.
773772
let fee_rate = if let Some(rate) = fee_rate {
774773
FeeRate::from_sat_per_vb(rate)
775774
} else {
776775
let sat_per_kwu = self.fee_estimator.get_normal_fee_rate();
777776
FeeRate::from_sat_per_kwu(sat_per_kwu as f32)
778777
};
779778
let fee_rate = payjoin::bitcoin::FeeRate::from_sat_per_kwu(fee_rate.sat_per_kwu() as u64);
780-
let original_psbt = payjoin::bitcoin::psbt::PartiallySignedTransaction::from_str(
781-
&original_psbt.to_string(),
782-
)
783-
.map_err(|_| MutinyError::WalletOperationFailed)?;
784779
log_debug!(self.logger, "Creating payjoin request");
785-
let (req, ctx) =
786-
payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), uri)
787-
.unwrap()
788-
.build_recommended(fee_rate)
789-
.map_err(|_| MutinyError::PayjoinCreateRequest)?
790-
.extract_v1()?;
780+
let req_ctx = payjoin::send::RequestBuilder::from_psbt_and_uri(original_psbt.clone(), uri)
781+
.unwrap()
782+
.build_recommended(fee_rate)
783+
.map_err(|_| MutinyError::PayjoinConfigError)?;
784+
self.spawn_payjoin_sender(labels, original_psbt, req_ctx)
785+
.await;
786+
Ok(())
787+
}
791788

792-
let client = Client::builder()
793-
.build()
794-
.map_err(|e| MutinyError::Other(e.into()))?;
789+
async fn spawn_payjoin_sender(
790+
&self,
791+
labels: Vec<String>,
792+
original_psbt: bitcoin::psbt::Psbt,
793+
req_ctx: payjoin::send::RequestContext,
794+
) {
795+
let wallet = self.wallet.clone();
796+
let logger = self.logger.clone();
797+
let stop = self.stop.clone();
798+
utils::spawn(async move {
799+
let proposal_psbt = match Self::poll_payjoin_sender(stop, req_ctx).await {
800+
Ok(psbt) => psbt,
801+
Err(e) => {
802+
log_error!(logger, "Error polling payjoin sender: {e}");
803+
return;
804+
}
805+
};
795806

796-
log_debug!(self.logger, "Sending payjoin request");
797-
let res = client
798-
.post(req.url)
799-
.body(req.body)
800-
.header("Content-Type", "text/plain")
801-
.send()
802-
.await
803-
.map_err(|_| MutinyError::PayjoinCreateRequest)?
804-
.bytes()
807+
if let Err(e) = Self::handle_proposal_psbt(
808+
logger.clone(),
809+
wallet,
810+
original_psbt,
811+
proposal_psbt,
812+
labels,
813+
)
805814
.await
806-
.map_err(|_| MutinyError::PayjoinCreateRequest)?;
807-
808-
let mut cursor = Cursor::new(res.to_vec());
815+
{
816+
// Ensure ResponseError is logged with debug formatting
817+
log_error!(logger, "Error handling payjoin proposal: {:?}", e);
818+
}
819+
});
820+
}
809821

810-
log_debug!(self.logger, "Processing payjoin response");
811-
let proposal_psbt = ctx.process_response(&mut cursor).map_err(|e| {
812-
// unrecognized error contents may only appear in debug logs and will not Display
813-
log_debug!(self.logger, "Payjoin response error: {:?}", e);
814-
e
815-
})?;
822+
async fn poll_payjoin_sender(
823+
stop: Arc<AtomicBool>,
824+
mut req_ctx: payjoin::send::RequestContext,
825+
) -> Result<bitcoin::psbt::Psbt, MutinyError> {
826+
let http = Client::builder()
827+
.build()
828+
.map_err(|_| MutinyError::Other(anyhow!("failed to build http client")))?;
829+
loop {
830+
if stop.load(Ordering::Relaxed) {
831+
return Err(MutinyError::NotRunning);
832+
}
816833

817-
// convert to pdk types
818-
let original_psbt = PartiallySignedTransaction::from_str(&original_psbt.to_string())
819-
.map_err(|_| MutinyError::PayjoinConfigError)?;
820-
let proposal_psbt = PartiallySignedTransaction::from_str(&proposal_psbt.to_string())
821-
.map_err(|_| MutinyError::PayjoinConfigError)?;
834+
let (req, ctx) = req_ctx
835+
.extract_v2(crate::payjoin::OHTTP_RELAYS[0].to_owned())
836+
.map_err(|_| MutinyError::PayjoinConfigError)?;
837+
let response = http
838+
.post(req.url)
839+
.header("Content-Type", "message/ohttp-req")
840+
.body(req.body)
841+
.send()
842+
.await
843+
.map_err(|_| MutinyError::Other(anyhow!("failed to parse payjoin response")))?;
844+
let mut reader =
845+
std::io::Cursor::new(response.bytes().await.map_err(|_| {
846+
MutinyError::Other(anyhow!("failed to parse payjoin response"))
847+
})?);
848+
849+
println!("Sent fallback transaction");
850+
let psbt = ctx
851+
.process_response(&mut reader)
852+
.map_err(MutinyError::PayjoinResponse)?;
853+
if let Some(psbt) = psbt {
854+
let psbt = bitcoin::psbt::Psbt::from_str(&psbt.to_string())
855+
.map_err(|_| MutinyError::Other(anyhow!("psbt conversion failed")))?;
856+
return Ok(psbt);
857+
} else {
858+
log::info!("No response yet for POST payjoin request, retrying some seconds");
859+
std::thread::sleep(std::time::Duration::from_secs(5));
860+
}
861+
}
862+
}
822863

823-
log_debug!(self.logger, "Sending payjoin..");
824-
let tx = self
825-
.wallet
864+
async fn handle_proposal_psbt(
865+
logger: Arc<MutinyLogger>,
866+
wallet: Arc<OnChainWallet<S>>,
867+
original_psbt: PartiallySignedTransaction,
868+
proposal_psbt: PartiallySignedTransaction,
869+
labels: Vec<String>,
870+
) -> Result<Txid, MutinyError> {
871+
log_debug!(logger, "Sending payjoin..");
872+
let tx = wallet
826873
.send_payjoin(original_psbt, proposal_psbt, labels)
827874
.await?;
828875
let txid = tx.txid();
829-
self.broadcast_transaction(tx).await?;
830-
log_debug!(self.logger, "Payjoin broadcast! TXID: {txid}");
876+
wallet.broadcast_transaction(tx).await?;
877+
log_info!(logger, "Payjoin broadcast! TXID: {txid}");
831878
Ok(txid)
832879
}
833880

mutiny-wasm/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -539,16 +539,15 @@ impl MutinyWallet {
539539
amount: u64, /* override the uri amount if desired */
540540
labels: Vec<String>,
541541
fee_rate: Option<f32>,
542-
) -> Result<String, MutinyJsError> {
542+
) -> Result<(), MutinyJsError> {
543543
// I know walia parses `pj=` and `pjos=` but payjoin::Uri parses the whole bip21 uri
544544
let pj_uri = payjoin::Uri::try_from(payjoin_uri.as_str())
545545
.map_err(|_| MutinyJsError::InvalidArgumentsError)?;
546546
Ok(self
547547
.inner
548548
.node_manager
549549
.send_payjoin(pj_uri, amount, labels, fee_rate)
550-
.await?
551-
.to_string())
550+
.await?)
552551
}
553552

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

0 commit comments

Comments
 (0)