Skip to content

Commit

Permalink
Send v2 payjoin
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould committed Jan 6, 2024
1 parent d629f0c commit 363826d
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 44 deletions.
132 changes: 91 additions & 41 deletions mutiny-core/src/nodemanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -811,75 +810,126 @@ impl<S: MutinyStorage> NodeManager<S> {
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<String>,
fee_rate: Option<f32>,
) -> Result<Txid, MutinyError> {
) -> 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 {
let sat_per_kwu = self.fee_estimator.get_normal_fee_rate();
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<String>,
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<AtomicBool>,
req_ctx: payjoin::send::RequestContext,
) -> Result<bitcoin::psbt::Psbt, MutinyError> {
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<MutinyLogger>,
wallet: Arc<OnChainWallet<S>>,
original_psbt: PartiallySignedTransaction,
proposal_psbt: PartiallySignedTransaction,
labels: Vec<String>,
) -> Result<Txid, MutinyError> {
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)
}

Expand Down
5 changes: 2 additions & 3 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ impl MutinyWallet {
amount: u64, /* override the uri amount if desired */
labels: Vec<String>,
fee_rate: Option<f32>,
) -> Result<String, MutinyJsError> {
) -> 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)?
Expand All @@ -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.
Expand Down

0 comments on commit 363826d

Please sign in to comment.