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

Commit 5c17520

Browse files
committed
Persist payjoin receive sessions temporarily
Payjoin sessions poll while the wallet is active until they expire.
1 parent 6978393 commit 5c17520

3 files changed

Lines changed: 88 additions & 10 deletions

File tree

mutiny-core/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY
4949
pub use crate::keymanager::generate_seed;
5050
pub use crate::ldkstorage::{CHANNEL_CLOSURE_PREFIX, CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY};
5151
use crate::nostr::primal::{PrimalApi, PrimalClient};
52+
use crate::payjoin::PayjoinStorage;
5253
use crate::storage::{
5354
get_payment_hash_from_key, list_payment_info, persist_payment_info, update_nostr_contact_list,
5455
IndexItem, MutinyStorage, DEVICE_ID_KEY, EXPECTED_NETWORK_KEY, NEED_FULL_SYNC_KEY,
@@ -1188,6 +1189,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
11881189
// when we restart, gen a new session id
11891190
self.node_manager = Arc::new(nm_builder.build().await?);
11901191
NodeManager::start_sync(self.node_manager.clone());
1192+
NodeManager::resume_payjoins(self.node_manager.clone());
11911193

11921194
Ok(())
11931195
}
@@ -1577,8 +1579,12 @@ impl<S: MutinyStorage> MutinyWallet<S> {
15771579

15781580
let (pj, ohttp) = match self.node_manager.start_payjoin_session().await {
15791581
Ok((enrolled, ohttp_keys)) => {
1580-
let pj_uri = enrolled.fallback_target();
1581-
self.node_manager.spawn_payjoin_receiver(enrolled);
1582+
let session = self
1583+
.node_manager
1584+
.storage
1585+
.store_new_recv_session(enrolled.clone())?;
1586+
let pj_uri = session.enrolled.fallback_target();
1587+
self.node_manager.spawn_payjoin_receiver(session);
15821588
let ohttp = base64::encode_config(
15831589
ohttp_keys
15841590
.encode()

mutiny-core/src/nodemanager.rs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::auth::MutinyAuthClient;
22
use crate::labels::LabelStorage;
33
use crate::ldkstorage::CHANNEL_CLOSURE_PREFIX;
44
use crate::logging::LOGGING_KEY;
5-
use crate::payjoin::Error as PayjoinError;
5+
use crate::payjoin::{Error as PayjoinError, PayjoinStorage, RecvSession};
66
use crate::utils::{sleep, spawn};
77
use crate::MutinyInvoice;
88
use crate::MutinyWalletConfig;
@@ -578,6 +578,14 @@ impl<S: MutinyStorage> NodeManager<S> {
578578
Ok(())
579579
}
580580

581+
/// Starts a background task to poll payjoin sessions to attempt receiving.
582+
pub(crate) fn resume_payjoins(nm: Arc<NodeManager<S>>) {
583+
let all = nm.storage.list_recv_sessions().unwrap_or_default();
584+
for payjoin in all {
585+
nm.clone().spawn_payjoin_receiver(payjoin);
586+
}
587+
}
588+
581589
/// Creates a background process that will sync the wallet with the blockchain.
582590
/// This will also update the fee estimates every 10 minutes.
583591
pub fn start_sync(nm: Arc<NodeManager<S>>) {
@@ -769,12 +777,13 @@ impl<S: MutinyStorage> NodeManager<S> {
769777
Ok(txid)
770778
}
771779

772-
pub fn spawn_payjoin_receiver(&self, enrolled: Enrolled) {
780+
pub fn spawn_payjoin_receiver(&self, session: RecvSession) {
773781
let logger = self.logger.clone();
774782
let stop = self.stop.clone();
783+
let storage = Arc::new(self.storage.clone());
775784
let wallet = self.wallet.clone();
776785
utils::spawn(async move {
777-
match Self::receive_payjoin(wallet, stop, enrolled).await {
786+
match Self::receive_payjoin(wallet, stop, storage, session).await {
778787
Ok(txid) => log_info!(logger, "Received payjoin txid: {txid}"),
779788
Err(e) => log_error!(logger, "Error receiving payjoin: {e}"),
780789
};
@@ -785,13 +794,14 @@ impl<S: MutinyStorage> NodeManager<S> {
785794
async fn receive_payjoin(
786795
wallet: Arc<OnChainWallet<S>>,
787796
stop: Arc<AtomicBool>,
788-
mut enrolled: payjoin::receive::v2::Enrolled,
797+
storage: Arc<S>,
798+
mut session: crate::payjoin::RecvSession,
789799
) -> Result<Txid, MutinyError> {
790800
let http_client = reqwest::Client::builder()
791801
.build()
792802
.map_err(PayjoinError::Reqwest)?;
793803
let proposal: payjoin::receive::v2::UncheckedProposal =
794-
Self::poll_for_fallback_psbt(stop, &http_client, &mut enrolled).await?;
804+
Self::poll_for_fallback_psbt(stop, storage, &http_client, &mut session).await?;
795805
let original_tx = proposal.extract_tx_to_schedule_broadcast();
796806
let mut payjoin_proposal = match wallet.process_payjoin_proposal(proposal) {
797807
Ok(p) => p,
@@ -821,22 +831,30 @@ impl<S: MutinyStorage> NodeManager<S> {
821831

822832
async fn poll_for_fallback_psbt(
823833
stop: Arc<AtomicBool>,
834+
storage: Arc<S>,
824835
client: &reqwest::Client,
825-
enroller: &mut payjoin::receive::v2::Enrolled,
836+
session: &mut crate::payjoin::RecvSession,
826837
) -> Result<payjoin::receive::v2::UncheckedProposal, PayjoinError> {
827838
loop {
828839
if stop.load(Ordering::Relaxed) {
829840
return Err(crate::payjoin::Error::Shutdown);
830841
}
831-
let (req, context) = enroller.extract_req()?;
842+
843+
if session.expiry < utils::now() {
844+
let _ = storage.delete_recv_session(&session.enrolled.pubkey());
845+
return Err(crate::payjoin::Error::SessionExpired);
846+
}
847+
let (req, context) = session.enrolled.extract_req()?;
832848
let ohttp_response = client
833849
.post(req.url)
834850
.header("Content-Type", "message/ohttp-req")
835851
.body(req.body)
836852
.send()
837853
.await?;
838854
let ohttp_response = ohttp_response.bytes().await?;
839-
let proposal = enroller.process_res(ohttp_response.as_ref(), context)?;
855+
let proposal = session
856+
.enrolled
857+
.process_res(ohttp_response.as_ref(), context)?;
840858
match proposal {
841859
Some(proposal) => return Ok(proposal),
842860
None => utils::sleep(5000).await,

mutiny-core/src/payjoin.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
use std::collections::HashMap;
2+
3+
use crate::error::MutinyError;
4+
use crate::storage::MutinyStorage;
5+
use core::time::Duration;
6+
use hex_conservative::DisplayHex;
17
use once_cell::sync::Lazy;
8+
use payjoin::receive::v2::Enrolled;
29
use payjoin::OhttpKeys;
10+
use serde::{Deserialize, Serialize};
311
use url::Url;
412

513
pub(crate) static OHTTP_RELAYS: [Lazy<Url>; 2] = [
@@ -17,6 +25,50 @@ pub fn random_ohttp_relay() -> &'static Url {
1725
pub(crate) static PAYJOIN_DIR: Lazy<Url> =
1826
Lazy::new(|| Url::parse("https://payjo.in").expect("Invalid URL"));
1927

28+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29+
pub struct RecvSession {
30+
pub enrolled: Enrolled,
31+
pub expiry: Duration,
32+
}
33+
34+
impl RecvSession {
35+
pub fn pubkey(&self) -> [u8; 33] {
36+
self.enrolled.pubkey()
37+
}
38+
}
39+
pub trait PayjoinStorage {
40+
fn list_recv_sessions(&self) -> Result<Vec<RecvSession>, MutinyError>;
41+
fn store_new_recv_session(&self, session: Enrolled) -> Result<RecvSession, MutinyError>;
42+
fn delete_recv_session(&self, id: &[u8; 33]) -> Result<(), MutinyError>;
43+
}
44+
45+
const PAYJOIN_KEY_PREFIX: &str = "recvpj/";
46+
47+
fn get_payjoin_key(id: &[u8; 33]) -> String {
48+
format!("{PAYJOIN_KEY_PREFIX}{}", id.as_hex())
49+
}
50+
51+
impl<S: MutinyStorage> PayjoinStorage for S {
52+
fn list_recv_sessions(&self) -> Result<Vec<RecvSession>, MutinyError> {
53+
let map: HashMap<String, RecvSession> = self.scan(PAYJOIN_KEY_PREFIX, None)?;
54+
Ok(map.values().map(|v| v.to_owned()).collect())
55+
}
56+
57+
fn store_new_recv_session(&self, enrolled: Enrolled) -> Result<RecvSession, MutinyError> {
58+
let in_24_hours = crate::utils::now() + Duration::from_secs(60 * 60 * 24);
59+
let session = RecvSession {
60+
enrolled,
61+
expiry: in_24_hours,
62+
};
63+
self.set_data(get_payjoin_key(&session.pubkey()), session.clone(), None)
64+
.map(|_| session)
65+
}
66+
67+
fn delete_recv_session(&self, id: &[u8; 33]) -> Result<(), MutinyError> {
68+
self.delete(&[get_payjoin_key(id)])
69+
}
70+
}
71+
2072
pub async fn fetch_ohttp_keys(directory: Url) -> Result<OhttpKeys, Error> {
2173
let http_client = reqwest::Client::builder().build()?;
2274

@@ -36,6 +88,7 @@ pub enum Error {
3688
Txid(bitcoin::hashes::hex::Error),
3789
OhttpDecodeFailed,
3890
Shutdown,
91+
SessionExpired,
3992
}
4093

4194
impl std::error::Error for Error {}
@@ -48,6 +101,7 @@ impl std::fmt::Display for Error {
48101
Error::Txid(e) => write!(f, "Payjoin txid error: {}", e),
49102
Error::OhttpDecodeFailed => write!(f, "Failed to decode ohttp keys"),
50103
Error::Shutdown => write!(f, "Payjoin stopped by application shutdown"),
104+
Error::SessionExpired => write!(f, "Payjoin session expired. Create a new payment request and have the sender try again."),
51105
}
52106
}
53107
}

0 commit comments

Comments
 (0)