From 0928c92a6eafd9dbf658a4fd73e918be55ee95f0 Mon Sep 17 00:00:00 2001 From: DanGould Date: Thu, 30 Nov 2023 15:05:50 -0500 Subject: [PATCH] Enable payjoin expiration --- mutiny-core/src/lib.rs | 6 +++-- mutiny-core/src/nodemanager.rs | 24 ++++++++++++++----- mutiny-core/src/payjoin.rs | 42 +++++++++++++++++++++++++++------- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 5f8eab945..0489a9a1b 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -1539,17 +1539,19 @@ impl MutinyWallet { let enrolled = enroller .process_res(ohttp_response.as_ref(), context) .map_err(|_| MutinyError::PayjoinCreateRequest)?; - self.node_manager + let session = self + .node_manager .storage .persist_payjoin(enrolled.clone())?; let pj_uri = enrolled.fallback_target(); log_debug!(self.logger, "{pj_uri}"); let wallet = self.node_manager.wallet.clone(); let stop = self.node_manager.stop.clone(); + let storage = Arc::new(self.node_manager.storage.clone()); // run await payjoin task in the background as it'll keep polling the relay let logger = self.logger.clone(); utils::spawn(async move { - match NodeManager::receive_payjoin(wallet, stop, enrolled).await { + match NodeManager::receive_payjoin(wallet, stop, storage, session).await { Ok(pj_txid) => log_info!(logger, "Received payjoin txid: {}", pj_txid), Err(e) => log_error!(logger, "Payjoin error: {e}"), } diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 69423dfb3..2264a5a14 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -635,8 +635,11 @@ impl NodeManager { 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, payjoin).await.unwrap(); + let pj_txid = Self::receive_payjoin(wallet, stop, storage, payjoin) + .await + .unwrap(); log::info!("Received payjoin txid: {}", pj_txid); }); } @@ -810,7 +813,8 @@ impl NodeManager { pub async fn receive_payjoin( wallet: Arc>, stop: Arc, - mut enrolled: payjoin::receive::v2::Enrolled, + storage: Arc, + mut session: crate::payjoin::Session, ) -> Result { use crate::payjoin::Error as PayjoinError; @@ -818,7 +822,7 @@ impl NodeManager { .build() .map_err(PayjoinError::Reqwest)?; let proposal: payjoin::receive::v2::UncheckedProposal = - Self::poll_for_fallback_psbt(stop, &http_client, &mut enrolled) + Self::poll_for_fallback_psbt(stop, storage, &http_client, &mut session) .await .map_err(|e| PayjoinError::ReceiverStateMachine(e.to_string()))?; let mut payjoin_proposal = wallet @@ -845,14 +849,20 @@ impl NodeManager { async fn poll_for_fallback_psbt( stop: Arc, + storage: Arc, client: &reqwest::Client, - enroller: &mut payjoin::receive::v2::Enrolled, + session: &mut crate::payjoin::Session, ) -> Result { loop { if stop.load(Ordering::Relaxed) { return Err(crate::payjoin::Error::Shutdown); } - let (req, context) = enroller.extract_req()?; + + if session.expiry < utils::now() { + let _ = storage.delete_payjoin(&session.enrolled.pubkey()); + return Err(crate::payjoin::Error::SessionExpired); + } + let (req, context) = session.enrolled.extract_req()?; let ohttp_response = client .post(req.url) .header("Content-Type", "message/ohttp-req") @@ -860,7 +870,9 @@ impl NodeManager { .send() .await?; let ohttp_response = ohttp_response.bytes().await?; - let proposal = enroller.process_res(ohttp_response.as_ref(), context)?; + let proposal = session + .enrolled + .process_res(ohttp_response.as_ref(), context)?; match proposal { Some(proposal) => return Ok(proposal), None => utils::sleep(5000).await, diff --git a/mutiny-core/src/payjoin.rs b/mutiny-core/src/payjoin.rs index 186af7aad..71f55fb59 100644 --- a/mutiny-core/src/payjoin.rs +++ b/mutiny-core/src/payjoin.rs @@ -2,10 +2,12 @@ use std::collections::HashMap; use crate::error::MutinyError; use crate::storage::MutinyStorage; +use core::time::Duration; use hex_conservative::DisplayHex; use once_cell::sync::Lazy; use payjoin::receive::v2::Enrolled; use payjoin::OhttpKeys; +use serde::{Deserialize, Serialize}; use url::Url; pub(crate) static OHTTP_RELAYS: [Lazy; 3] = [ @@ -17,10 +19,22 @@ pub(crate) static OHTTP_RELAYS: [Lazy; 3] = [ pub(crate) static PAYJOIN_DIR: Lazy = Lazy::new(|| Url::parse("https://payjo.in").expect("Invalid URL")); +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Session { + pub enrolled: Enrolled, + pub expiry: Duration, +} + +impl Session { + pub fn pubkey(&self) -> [u8; 33] { + self.enrolled.pubkey() + } +} pub trait PayjoinStorage { - fn get_payjoin(&self, id: &[u8; 33]) -> Result, MutinyError>; - fn get_payjoins(&self) -> Result, MutinyError>; - fn persist_payjoin(&self, session: Enrolled) -> Result<(), MutinyError>; + fn get_payjoin(&self, id: &[u8; 33]) -> Result, MutinyError>; + fn get_payjoins(&self) -> Result, MutinyError>; + fn persist_payjoin(&self, session: Enrolled) -> Result; + fn delete_payjoin(&self, id: &[u8; 33]) -> Result<(), MutinyError>; } const PAYJOIN_KEY_PREFIX: &str = "payjoin/"; @@ -30,18 +44,28 @@ fn get_payjoin_key(id: &[u8; 33]) -> String { } impl PayjoinStorage for S { - fn get_payjoin(&self, id: &[u8; 33]) -> Result, MutinyError> { + fn get_payjoin(&self, id: &[u8; 33]) -> Result, MutinyError> { let sessions = self.get_data(get_payjoin_key(id))?; Ok(sessions) } - fn get_payjoins(&self) -> Result, MutinyError> { - let map: HashMap = self.scan(PAYJOIN_KEY_PREFIX, None)?; + fn get_payjoins(&self) -> Result, MutinyError> { + let map: HashMap = self.scan(PAYJOIN_KEY_PREFIX, None)?; Ok(map.values().map(|v| v.to_owned()).collect()) } - fn persist_payjoin(&self, session: Enrolled) -> Result<(), MutinyError> { - self.set_data(get_payjoin_key(&session.pubkey()), session, None) + fn persist_payjoin(&self, enrolled: Enrolled) -> Result { + let in_24_hours = crate::utils::now() + Duration::from_secs(60 * 60 * 24); + let session = Session { + enrolled, + expiry: in_24_hours, + }; + self.set_data(get_payjoin_key(&session.pubkey()), session.clone(), None) + .map(|_| session) + } + + fn delete_payjoin(&self, id: &[u8; 33]) -> Result<(), MutinyError> { + self.delete(&[get_payjoin_key(id)]) } } @@ -66,6 +90,7 @@ pub enum Error { ReceiverStateMachine(String), Txid(bitcoin::hashes::hex::Error), Shutdown, + SessionExpired, } impl std::error::Error for Error {} @@ -77,6 +102,7 @@ impl std::fmt::Display for Error { Error::ReceiverStateMachine(e) => write!(f, "Payjoin state machine error: {}", e), Error::Txid(e) => write!(f, "Payjoin txid error: {}", e), Error::Shutdown => write!(f, "Payjoin stopped by application shutdown"), + Error::SessionExpired => write!(f, "Payjoin session expired. Create a new payment request and have the sender try again."), } } }