Skip to content

Commit a838c95

Browse files
authored
Expose fallback tx off receiver session history (payjoin#799)
If a `MaybeInputsOwned` session event is present in the log then we know the session has persisted a "broadcastable" fallback tx by virute of successfully transitioning past `UncheckedProposal` via `check_broadcast_suitability` and persisting the result. This commit explicitly does not use the unchecked session event because the fallback at that typestate may fail consensus or policy checks. Resolves payjoin#798
2 parents 74bb0e8 + 979ca78 commit a838c95

File tree

2 files changed

+49
-1
lines changed

2 files changed

+49
-1
lines changed

payjoin-ffi/src/receive/uni.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ impl SessionHistory {
126126
})
127127
}
128128

129+
/// Fallback transaction from the session if present
130+
pub fn fallback_tx(&self) -> Option<Arc<crate::Transaction>> {
131+
self.0 .0.fallback_tx().map(|tx| Arc::new(tx.into()))
132+
}
133+
129134
/// Extract the error request to be posted on the directory if an error occurred.
130135
/// To process the response, use [process_err_res]
131136
pub fn extract_err_req(

payjoin/src/receive/v2/session.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ impl SessionHistory {
100100
})
101101
}
102102

103+
/// Fallback transaction from the session if present
104+
pub fn fallback_tx(&self) -> Option<bitcoin::Transaction> {
105+
self.events.iter().find_map(|event| match event {
106+
SessionEvent::MaybeInputsOwned(proposal) =>
107+
Some(proposal.extract_tx_to_schedule_broadcast()),
108+
_ => None,
109+
})
110+
}
111+
103112
/// Psbt with receiver contributed inputs
104113
pub fn psbt_with_contributed_inputs(&self) -> Option<bitcoin::Psbt> {
105114
self.events.iter().find_map(|event| match event {
@@ -169,7 +178,7 @@ mod tests {
169178
use crate::receive::v1::test::unchecked_proposal_from_test_vector;
170179
use crate::receive::v2::test::SHARED_CONTEXT;
171180
use crate::receive::v2::{
172-
PayjoinProposal, ProvisionalProposal, UncheckedProposal, WithContext,
181+
MaybeInputsOwned, PayjoinProposal, ProvisionalProposal, UncheckedProposal, WithContext,
173182
};
174183

175184
#[test]
@@ -221,6 +230,7 @@ mod tests {
221230

222231
struct SessionHistoryExpectedOutcome {
223232
psbt_with_contributed_inputs: Option<bitcoin::Psbt>,
233+
fallback_tx: Option<bitcoin::Transaction>,
224234
}
225235

226236
struct SessionHistoryTest {
@@ -242,6 +252,7 @@ mod tests {
242252
session_history.psbt_with_contributed_inputs(),
243253
test.expected_session_history.psbt_with_contributed_inputs
244254
);
255+
assert_eq!(session_history.fallback_tx(), test.expected_session_history.fallback_tx);
245256
}
246257

247258
#[test]
@@ -251,6 +262,7 @@ mod tests {
251262
events: vec![SessionEvent::Created(session_context.clone())],
252263
expected_session_history: SessionHistoryExpectedOutcome {
253264
psbt_with_contributed_inputs: None,
265+
fallback_tx: None,
254266
},
255267
expected_receiver_state: ReceiverTypeState::WithContext(Receiver {
256268
state: WithContext { context: session_context },
@@ -270,6 +282,7 @@ mod tests {
270282
],
271283
expected_session_history: SessionHistoryExpectedOutcome {
272284
psbt_with_contributed_inputs: None,
285+
fallback_tx: None,
273286
},
274287
expected_receiver_state: ReceiverTypeState::UncheckedProposal(Receiver {
275288
state: UncheckedProposal {
@@ -295,6 +308,7 @@ mod tests {
295308
],
296309
expected_session_history: SessionHistoryExpectedOutcome {
297310
psbt_with_contributed_inputs: None,
311+
fallback_tx: None,
298312
},
299313
expected_receiver_state: ReceiverTypeState::UncheckedProposal(Receiver {
300314
state: UncheckedProposal {
@@ -306,6 +320,31 @@ mod tests {
306320
run_session_history_test(test);
307321
}
308322

323+
#[test]
324+
fn getting_fallback_tx() {
325+
let session_context = SHARED_CONTEXT.clone();
326+
let mut events = vec![];
327+
let unchecked_proposal = unchecked_proposal_from_test_vector();
328+
let maybe_inputs_owned = unchecked_proposal.clone().assume_interactive_receiver();
329+
let expected_fallback = maybe_inputs_owned.extract_tx_to_schedule_broadcast();
330+
331+
events.push(SessionEvent::Created(session_context.clone()));
332+
events.push(SessionEvent::UncheckedProposal((unchecked_proposal, None)));
333+
events.push(SessionEvent::MaybeInputsOwned(maybe_inputs_owned.clone()));
334+
335+
let test = SessionHistoryTest {
336+
events,
337+
expected_session_history: SessionHistoryExpectedOutcome {
338+
psbt_with_contributed_inputs: None,
339+
fallback_tx: Some(expected_fallback),
340+
},
341+
expected_receiver_state: ReceiverTypeState::MaybeInputsOwned(Receiver {
342+
state: MaybeInputsOwned { v1: maybe_inputs_owned, context: session_context },
343+
}),
344+
};
345+
run_session_history_test(test);
346+
}
347+
309348
#[test]
310349
fn test_contributed_inputs() {
311350
let session_context = SHARED_CONTEXT.clone();
@@ -327,6 +366,7 @@ mod tests {
327366
.expect("Outputs should be identified");
328367
let wants_inputs = wants_outputs.clone().commit_outputs();
329368
let provisional_proposal = wants_inputs.clone().commit_inputs();
369+
let expected_fallback = maybe_inputs_owned.extract_tx_to_schedule_broadcast();
330370

331371
events.push(SessionEvent::Created(session_context.clone()));
332372
events.push(SessionEvent::UncheckedProposal((unchecked_proposal, None)));
@@ -341,6 +381,7 @@ mod tests {
341381
events,
342382
expected_session_history: SessionHistoryExpectedOutcome {
343383
psbt_with_contributed_inputs: Some(provisional_proposal.payjoin_psbt.clone()),
384+
fallback_tx: Some(expected_fallback),
344385
},
345386
expected_receiver_state: ReceiverTypeState::ProvisionalProposal(Receiver {
346387
state: ProvisionalProposal { v1: provisional_proposal, context: session_context },
@@ -374,6 +415,7 @@ mod tests {
374415
.clone()
375416
.finalize_proposal(|psbt| Ok(psbt.clone()), None, None)
376417
.expect("Payjoin proposal should be finalized");
418+
let expected_fallback = maybe_inputs_owned.extract_tx_to_schedule_broadcast();
377419

378420
events.push(SessionEvent::Created(session_context.clone()));
379421
events.push(SessionEvent::UncheckedProposal((unchecked_proposal, None)));
@@ -389,6 +431,7 @@ mod tests {
389431
events,
390432
expected_session_history: SessionHistoryExpectedOutcome {
391433
psbt_with_contributed_inputs: Some(provisional_proposal.payjoin_psbt.clone()),
434+
fallback_tx: Some(expected_fallback),
392435
},
393436
expected_receiver_state: ReceiverTypeState::PayjoinProposal(Receiver {
394437
state: PayjoinProposal { v1: payjoin_proposal, context: session_context },

0 commit comments

Comments
 (0)