Skip to content

Commit ab142b3

Browse files
authored
Sender session history fallback (payjoin#805)
Extract the unchecked transaction from the original psbt which becomes asccessible in the first session event -- `withReplyKey`.
2 parents a838c95 + 4c17ab6 commit ab142b3

File tree

3 files changed

+77
-5
lines changed

3 files changed

+77
-5
lines changed

payjoin-ffi/src/send/uni.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,14 @@ impl From<SenderSessionHistory> for super::SessionHistory {
7070

7171
#[uniffi::export]
7272
impl SenderSessionHistory {
73-
pub fn endpoints(&self) -> Option<Arc<Url>> {
73+
pub fn endpoint(&self) -> Option<Arc<Url>> {
7474
self.0.0.endpoint().map(|url| Arc::new(url.clone().into()))
7575
}
76+
77+
/// Fallback transaction from the session if present
78+
pub fn fallback_tx(&self) -> Option<Arc<crate::Transaction>> {
79+
self.0.0.fallback_tx().map(|tx| Arc::new(tx.into()))
80+
}
7681
}
7782

7883
#[derive(uniffi::Object)]

payjoin/src/send/v2/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ impl<State: SenderState> core::ops::DerefMut for Sender<State> {
161161
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.state }
162162
}
163163

164-
#[derive(Debug, Clone)]
164+
#[derive(Debug, Clone, PartialEq, Eq)]
165165
pub enum SenderTypeState {
166166
Uninitialized(),
167167
WithReplyKey(Sender<WithReplyKey>),

payjoin/src/send/v2/session.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,18 @@ pub struct SessionHistory {
6767
}
6868

6969
impl SessionHistory {
70+
/// Fallback transaction from the session if present
71+
pub fn fallback_tx(&self) -> Option<bitcoin::Transaction> {
72+
self.events.iter().find_map(|event| match event {
73+
SessionEvent::CreatedReplyKey(proposal) =>
74+
Some(proposal.v1.psbt.clone().extract_tx_unchecked_fee_rate()),
75+
_ => None,
76+
})
77+
}
78+
7079
pub fn endpoint(&self) -> Option<&Url> {
7180
self.events.iter().find_map(|event| match event {
72-
SessionEvent::V2GetContext(ctx) => Some(&ctx.endpoint),
81+
SessionEvent::CreatedReplyKey(proposal) => Some(&proposal.v1.endpoint),
7382
_ => None,
7483
})
7584
}
@@ -94,9 +103,14 @@ mod tests {
94103

95104
use super::*;
96105
use crate::output_substitution::OutputSubstitution;
97-
use crate::send::v2::HpkeContext;
106+
use crate::persist::test_utils::InMemoryTestPersister;
107+
use crate::send::v1::SenderBuilder;
108+
use crate::send::v2::{HpkeContext, Sender};
98109
use crate::send::{v1, PsbtContext};
99-
use crate::HpkeKeyPair;
110+
use crate::{HpkeKeyPair, Uri, UriExt};
111+
112+
const PJ_URI: &str =
113+
"bitcoin:2N47mmrWXsNBvQR6k78hWJoTji57zXwNcU7?amount=0.02&pjos=0&pj=HTTPS://EXAMPLE.COM/";
100114

101115
#[test]
102116
fn test_sender_session_event_serialization_roundtrip() {
@@ -140,4 +154,57 @@ mod tests {
140154
assert_eq!(event, deserialized);
141155
}
142156
}
157+
158+
struct SessionHistoryExpectedOutcome {
159+
fallback_tx: Option<bitcoin::Transaction>,
160+
endpoint: Option<Url>,
161+
}
162+
163+
struct SessionHistoryTest {
164+
events: Vec<SessionEvent>,
165+
expected_session_history: SessionHistoryExpectedOutcome,
166+
expected_sender_state: SenderTypeState,
167+
}
168+
169+
fn run_session_history_test(test: SessionHistoryTest) {
170+
let persister = InMemoryTestPersister::<SessionEvent>::default();
171+
for event in test.events {
172+
persister.save_event(&event).expect("In memory persister shouldn't fail");
173+
}
174+
175+
let (sender, session_history) =
176+
replay_event_log(&persister).expect("In memory persister shouldn't fail");
177+
assert_eq!(sender, test.expected_sender_state);
178+
assert_eq!(session_history.fallback_tx(), test.expected_session_history.fallback_tx);
179+
assert_eq!(session_history.endpoint().cloned(), test.expected_session_history.endpoint);
180+
}
181+
182+
#[test]
183+
fn test_sender_session_history_with_reply_key_event() {
184+
let psbt = PARSED_ORIGINAL_PSBT.clone();
185+
let sender = SenderBuilder::new(
186+
psbt.clone(),
187+
Uri::try_from(PJ_URI)
188+
.expect("Valid uri")
189+
.assume_checked()
190+
.check_pj_supported()
191+
.expect("Payjoin to be supported"),
192+
)
193+
.build_recommended(FeeRate::BROADCAST_MIN)
194+
.unwrap();
195+
let reply_key = HpkeKeyPair::gen_keypair();
196+
let endpoint = sender.endpoint().clone();
197+
let fallback_tx = sender.psbt.clone().extract_tx_unchecked_fee_rate();
198+
let with_reply_key = WithReplyKey { v1: sender, reply_key: reply_key.0 };
199+
let sender = Sender { state: with_reply_key.clone() };
200+
let test = SessionHistoryTest {
201+
events: vec![SessionEvent::CreatedReplyKey(with_reply_key)],
202+
expected_session_history: SessionHistoryExpectedOutcome {
203+
fallback_tx: Some(fallback_tx),
204+
endpoint: Some(endpoint),
205+
},
206+
expected_sender_state: SenderTypeState::WithReplyKey(sender),
207+
};
208+
run_session_history_test(test);
209+
}
143210
}

0 commit comments

Comments
 (0)