Skip to content

Commit

Permalink
Handle payjoin errors
Browse files Browse the repository at this point in the history
  • Loading branch information
DanGould committed Dec 20, 2023
1 parent 570e695 commit dfbbf0e
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 74 deletions.
138 changes: 64 additions & 74 deletions mutiny-core/src/nodemanager.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::lnurlauth::AuthManager;
use crate::logging::LOGGING_KEY;
use crate::payjoin::PayjoinStorage;
use crate::payjoin::{Error as PayjoinError, PayjoinStorage};
use crate::redshift::{RedshiftManager, RedshiftStatus, RedshiftStorage};
use crate::storage::{MutinyStorage, DEVICE_ID_KEY, KEYCHAIN_STORE_KEY, NEED_FULL_SYNC_KEY};
use crate::utils::{sleep, spawn};
Expand Down Expand Up @@ -52,6 +52,7 @@ use lnurl::lnurl::LnUrl;
use lnurl::{AsyncClient as LnUrlClient, LnUrlResponse, Response};
use nostr::key::XOnlyPublicKey;
use nostr::{EventBuilder, Keys, Kind, Tag, TagKind};
use payjoin::receive::v2::Enrolled;
use payjoin::Uri;
use reqwest::Client;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -807,15 +808,7 @@ impl<S: MutinyStorage> NodeManager<S> {
pub(crate) fn resume_payjoins(nm: Arc<NodeManager<S>>) {
let all = nm.storage.get_payjoins().unwrap_or_default();
for payjoin in all {
let wallet = nm.wallet.clone();
let stop = nm.stop.clone();
let storage = Arc::new(nm.storage.clone());
utils::spawn(async move {
let pj_txid = Self::receive_payjoin(wallet, stop, storage, payjoin)
.await
.unwrap();
log::info!("Received payjoin txid: {}", pj_txid);
});
nm.clone().spawn_payjoin_receiver(payjoin);
}
}

Expand Down Expand Up @@ -1021,53 +1014,18 @@ impl<S: MutinyStorage> NodeManager<S> {
return Err(MutinyError::WalletOperationFailed);
};

let pj = {
// DANGER! TODO get from &self config, do not get config directly from PAYJOIN_DIR ohttp-gateway
// That would reveal IP address

let http_client = reqwest::Client::builder().build().unwrap();

let ohttp_config_base64 = http_client
.get(format!("{}/ohttp-config", crate::payjoin::PAYJOIN_DIR))
.send()
.await
.unwrap()
.text()
.await
.unwrap();

let mut enroller = payjoin::receive::v2::Enroller::from_relay_config(
crate::payjoin::PAYJOIN_DIR,
&ohttp_config_base64,
crate::payjoin::OHTTP_RELAYS[0], // TODO pick ohttp relay at random
);
// enroll client
let (req, context) = enroller.extract_req().unwrap();
let ohttp_response = http_client
.post(req.url)
.body(req.body)
.send()
.await
.unwrap();
let ohttp_response = ohttp_response.bytes().await.unwrap();
let enrolled = enroller
.process_res(ohttp_response.as_ref(), context)
.map_err(|e| anyhow!("parse error {}", e))
.unwrap();
let session = self.storage.persist_payjoin(enrolled.clone()).unwrap();
let pj_uri = enrolled.fallback_target();
log_debug!(self.logger, "{pj_uri}");
// run await payjoin task in the background as it'll keep polling the relay
let wallet = self.wallet.clone();
let stop = self.stop.clone();
let storage = Arc::new(self.storage.clone());
utils::spawn(async move {
let pj_txid = Self::receive_payjoin(wallet, stop, storage, session)
.await
.unwrap();
log::info!("Received payjoin txid: {}", pj_txid);
});
Some(pj_uri)
let pj = match self.start_payjoin_session().await {
Ok(enrolled) => {
let session = self.storage.persist_payjoin(enrolled.clone())?;
let pj_uri = session.enrolled.fallback_target();
log_debug!(self.logger, "{pj_uri}");
self.spawn_payjoin_receiver(session);
Some(pj_uri)
}
Err(e) => {
log_error!(self.logger, "Error enrolling payjoin: {e}");
None
}
};

Ok(MutinyBip21RawMaterials {
Expand All @@ -1079,6 +1037,31 @@ impl<S: MutinyStorage> NodeManager<S> {
})
}

async fn start_payjoin_session(&self) -> Result<Enrolled, PayjoinError> {
// DANGER! TODO get from &self config, do not get config directly from PAYJOIN_DIR ohttp-gateway
// That would reveal IP address

let http_client = reqwest::Client::builder().build()?;

let ohttp_config_base64 = http_client
.get(format!("{}/ohttp-config", crate::payjoin::PAYJOIN_DIR))
.send()
.await?
.text()
.await?;

let mut enroller = payjoin::receive::v2::Enroller::from_relay_config(
crate::payjoin::PAYJOIN_DIR,
&ohttp_config_base64,
crate::payjoin::OHTTP_RELAYS[0], // TODO pick ohttp relay at random
);
// enroll client
let (req, context) = enroller.extract_req()?;
let ohttp_response = http_client.post(req.url).body(req.body).send().await?;
let ohttp_response = ohttp_response.bytes().await?;
Ok(enroller.process_res(ohttp_response.as_ref(), context)?)
}

// Send v1 payjoin request
pub async fn send_payjoin(
&self,
Expand Down Expand Up @@ -1151,38 +1134,45 @@ impl<S: MutinyStorage> NodeManager<S> {
Ok(txid)
}

pub fn spawn_payjoin_receiver(&self, session: crate::payjoin::Session) {
let logger = self.logger.clone();
let wallet = self.wallet.clone();
let stop = self.stop.clone();
let storage = Arc::new(self.storage.clone());
utils::spawn(async move {
match Self::receive_payjoin(wallet, stop, storage, session).await {
Ok(txid) => log_info!(logger, "Received payjoin txid: {txid}"),
Err(e) => log_error!(logger, "Error receiving payjoin: {e}"),
};
});
}

/// Poll the payjoin relay to maintain a payjoin session and create a payjoin proposal.
pub async fn receive_payjoin(
async fn receive_payjoin(
wallet: Arc<OnChainWallet<S>>,
stop: Arc<AtomicBool>,
storage: Arc<S>,
mut session: crate::payjoin::Session,
) -> Result<Txid, MutinyError> {
) -> Result<Txid, PayjoinError> {
let http_client = reqwest::Client::builder()
//.danger_accept_invalid_certs(true) ? is tls unchecked :O
.build()
.unwrap();
.build()?;
let proposal: payjoin::receive::v2::UncheckedProposal =
Self::poll_for_fallback_psbt(stop, storage, &http_client, &mut session)
.await
.unwrap();
let payjoin_proposal = wallet.process_payjoin_proposal(proposal).unwrap();
let payjoin_proposal = wallet
.process_payjoin_proposal(proposal)
.map_err(PayjoinError::Wallet)?;

let (req, ohttp_ctx) = payjoin_proposal.extract_v2_req().unwrap(); // extraction failed
let res = http_client
.post(req.url)
.body(req.body)
.send()
.await
.unwrap();
let res = res.bytes().await.unwrap();
let (req, ohttp_ctx) = payjoin_proposal.extract_v2_req()?; // extraction failed
let res = http_client.post(req.url).body(req.body).send().await?;
let res = res.bytes().await?;
// enroll must succeed
let _res = payjoin_proposal
.deserialize_res(res.to_vec(), ohttp_ctx)
.unwrap();
let _res = payjoin_proposal.deserialize_res(res.to_vec(), ohttp_ctx)?;
// convert from bitcoin 29 to 30
let txid = payjoin_proposal.psbt().clone().extract_tx().txid();
let txid = Txid::from_str(&txid.to_string()).unwrap();
let txid = Txid::from_str(&txid.to_string()).map_err(PayjoinError::Txid)?;
Ok(txid)
}

Expand Down
41 changes: 41 additions & 0 deletions mutiny-core/src/payjoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,44 @@ impl<S: MutinyStorage> PayjoinStorage for S {
self.delete(&[get_payjoin_key(id)])
}
}

#[derive(Debug)]
pub(crate) enum Error {
Reqwest(reqwest::Error),
ReceiverStateMachine(payjoin::receive::Error),
V2Encapsulation(payjoin::v2::Error),
Wallet(payjoin::Error),
Txid(bitcoin::hashes::hex::Error),
}

impl std::error::Error for Error {}

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self {
Error::Reqwest(e) => write!(f, "Reqwest error: {}", e),
Error::ReceiverStateMachine(e) => write!(f, "Payjoin error: {}", e),
Error::V2Encapsulation(e) => write!(f, "Payjoin v2 error: {}", e),
Error::Wallet(e) => write!(f, "Payjoin wallet error: {}", e),
Error::Txid(e) => write!(f, "Payjoin txid error: {}", e),
}
}
}

impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
Error::Reqwest(e)
}
}

impl From<payjoin::receive::Error> for Error {
fn from(e: payjoin::receive::Error) -> Self {
Error::ReceiverStateMachine(e)
}
}

impl From<payjoin::v2::Error> for Error {
fn from(e: payjoin::v2::Error) -> Self {
Error::V2Encapsulation(e)
}
}

0 comments on commit dfbbf0e

Please sign in to comment.