• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payjoin / rust-payjoin / 17624704310

10 Sep 2025 07:28PM UTC coverage: 86.041% (+0.02%) from 86.019%
17624704310

Pull #1055

github

web-flow
Merge 2a1875d38 into 52cfeef1a
Pull Request #1055: Remove optional return types from session history methods

14 of 14 new or added lines in 3 files covered. (100.0%)

4 existing lines in 2 files now uncovered.

8235 of 9571 relevant lines covered (86.04%)

488.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.65
/payjoin/src/core/send/v2/session.rs
1
use super::WithReplyKey;
2
use crate::error::{InternalReplayError, ReplayError};
3
use crate::persist::SessionPersister;
4
use crate::send::v2::{SendSession, V2GetContext};
5
use crate::uri::v2::PjParam;
6
use crate::ImplementationError;
7

8
pub fn replay_event_log<P>(
3✔
9
    persister: &P,
3✔
10
) -> Result<(SendSession, SessionHistory), ReplayError<SendSession, SessionEvent>>
3✔
11
where
3✔
12
    P: SessionPersister + Clone,
3✔
13
    P::SessionEvent: Into<SessionEvent> + Clone,
3✔
14
    P::SessionEvent: From<SessionEvent>,
3✔
15
{
16
    let mut logs = persister
3✔
17
        .load()
3✔
18
        .map_err(|e| InternalReplayError::PersistenceFailure(ImplementationError::new(e)))?;
3✔
19
    let mut history = SessionHistory::default();
3✔
20
    let first_event = logs.next().ok_or(InternalReplayError::NoEvents)?.into();
3✔
21
    history.events.push(first_event.clone());
3✔
22
    let mut sender = match first_event {
3✔
23
        SessionEvent::CreatedReplyKey(reply_key) => SendSession::new(reply_key),
3✔
24
        _ => return Err(InternalReplayError::InvalidEvent(Box::new(first_event), None).into()),
×
25
    };
26

27
    for log in logs {
4✔
28
        let session_event = log.into();
1✔
29
        history.events.push(session_event.clone());
1✔
30
        match sender.clone().process_event(session_event) {
1✔
31
            Ok(next_sender) => sender = next_sender,
1✔
32
            Err(_e) => {
×
33
                persister.close().map_err(|e| {
×
34
                    InternalReplayError::PersistenceFailure(ImplementationError::new(e))
×
35
                })?;
×
36
                break;
×
37
            }
38
        }
39
    }
40

41
    let pj_param = history.pj_param();
3✔
42
    if std::time::SystemTime::now() > pj_param.expiration() {
3✔
43
        // Session has expired: close the session and persist a fatal error
44
        persister
1✔
45
            .save_event(SessionEvent::SessionInvalid("Session expired".to_string()).into())
1✔
46
            .map_err(|e| InternalReplayError::PersistenceFailure(ImplementationError::new(e)))?;
1✔
47
        persister
1✔
48
            .close()
1✔
49
            .map_err(|e| InternalReplayError::PersistenceFailure(ImplementationError::new(e)))?;
1✔
50

51
        return Ok((SendSession::TerminalFailure, history));
1✔
52
    }
2✔
53
    Ok((sender, history))
2✔
54
}
3✔
55

56
#[derive(Default, Clone)]
57
pub struct SessionHistory {
58
    events: Vec<SessionEvent>,
59
}
60

61
impl SessionHistory {
62
    /// Fallback transaction from the session
63
    pub fn fallback_tx(&self) -> bitcoin::Transaction {
2✔
64
        self.events
2✔
65
            .iter()
2✔
66
            .find_map(|event| match event {
2✔
67
                SessionEvent::CreatedReplyKey(proposal) =>
2✔
68
                    Some(proposal.psbt_ctx.original_psbt.clone().extract_tx_unchecked_fee_rate()),
2✔
UNCOV
69
                _ => None,
×
70
            })
2✔
71
            .expect("Session event log must contain at least one event with fallback_tx")
2✔
72
    }
2✔
73

74
    pub fn pj_param(&self) -> &PjParam {
5✔
75
        self.events
5✔
76
            .iter()
5✔
77
            .find_map(|event| match event {
5✔
78
                SessionEvent::CreatedReplyKey(proposal) => Some(&proposal.pj_param),
5✔
UNCOV
79
                _ => None,
×
80
            })
5✔
81
            .expect("Session event log must contain at least one event with pj_param")
5✔
82
    }
5✔
83
}
84

85
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
86
pub enum SessionEvent {
87
    /// Sender was created with a HPKE key pair
88
    CreatedReplyKey(WithReplyKey),
89
    /// Sender POST'd the original PSBT, and waiting to receive a Proposal PSBT using GET context
90
    V2GetContext(V2GetContext),
91
    /// Sender received a Proposal PSBT
92
    ProposalReceived(bitcoin::Psbt),
93
    /// Invalid session
94
    SessionInvalid(String),
95
}
96

97
#[cfg(test)]
98
mod tests {
99
    use bitcoin::{FeeRate, ScriptBuf};
100
    use payjoin_test_utils::{KEM, KEY_ID, PARSED_ORIGINAL_PSBT, SYMMETRIC};
101

102
    use super::*;
103
    use crate::output_substitution::OutputSubstitution;
104
    use crate::persist::test_utils::InMemoryTestPersister;
105
    use crate::send::v1::SenderBuilder;
106
    use crate::send::v2::Sender;
107
    use crate::send::PsbtContext;
108
    use crate::{HpkeKeyPair, Uri, UriExt};
109

110
    const PJ_URI: &str =
111
        "bitcoin:2N47mmrWXsNBvQR6k78hWJoTji57zXwNcU7?amount=0.02&pjos=0&pj=HTTPS://EXAMPLE.COM/";
112

113
    #[test]
114
    fn test_sender_session_event_serialization_roundtrip() {
1✔
115
        let keypair = HpkeKeyPair::gen_keypair();
1✔
116
        let id = crate::uri::ShortId::try_from(&b"12345670"[..]).expect("valid short id");
1✔
117
        let endpoint = url::Url::parse("http://localhost:1234").expect("valid url");
1✔
118
        let pj_param = crate::uri::v2::PjParam::new(
1✔
119
            endpoint,
1✔
120
            id,
1✔
121
            std::time::SystemTime::now() + std::time::Duration::from_secs(60),
1✔
122
            crate::OhttpKeys(
1✔
123
                ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).expect("valid key config"),
1✔
124
            ),
1✔
125
            HpkeKeyPair::gen_keypair().1,
1✔
126
        );
127
        let sender_with_reply_key = WithReplyKey {
1✔
128
            pj_param: pj_param.clone(),
1✔
129
            psbt_ctx: PsbtContext {
1✔
130
                original_psbt: PARSED_ORIGINAL_PSBT.clone(),
1✔
131
                output_substitution: OutputSubstitution::Enabled,
1✔
132
                fee_contribution: None,
1✔
133
                min_fee_rate: FeeRate::ZERO,
1✔
134
                payee: ScriptBuf::from(vec![0x00]),
1✔
135
            },
1✔
136
            reply_key: keypair.0.clone(),
1✔
137
        };
1✔
138

139
        let v2_get_context = V2GetContext {
1✔
140
            pj_param: pj_param.clone(),
1✔
141
            psbt_ctx: PsbtContext {
1✔
142
                original_psbt: PARSED_ORIGINAL_PSBT.clone(),
1✔
143
                output_substitution: OutputSubstitution::Enabled,
1✔
144
                fee_contribution: None,
1✔
145
                min_fee_rate: FeeRate::ZERO,
1✔
146
                payee: ScriptBuf::from(vec![0x00]),
1✔
147
            },
1✔
148
            reply_key: keypair.0.clone(),
1✔
149
        };
1✔
150

151
        let test_cases = vec![
1✔
152
            SessionEvent::CreatedReplyKey(sender_with_reply_key.clone()),
1✔
153
            SessionEvent::V2GetContext(v2_get_context.clone()),
1✔
154
            SessionEvent::ProposalReceived(PARSED_ORIGINAL_PSBT.clone()),
1✔
155
            SessionEvent::SessionInvalid("error message".to_string()),
1✔
156
        ];
157

158
        for event in test_cases {
5✔
159
            let serialized = serde_json::to_string(&event).expect("Should serialize");
4✔
160
            let deserialized: SessionEvent =
4✔
161
                serde_json::from_str(&serialized).expect("Should deserialize");
4✔
162
            assert_eq!(event, deserialized);
4✔
163
        }
164
    }
1✔
165

166
    struct SessionHistoryExpectedOutcome {
167
        fallback_tx: bitcoin::Transaction,
168
        pj_param: PjParam,
169
    }
170

171
    struct SessionHistoryTest {
172
        events: Vec<SessionEvent>,
173
        expected_session_history: SessionHistoryExpectedOutcome,
174
        expected_sender_state: SendSession,
175
    }
176

177
    fn run_session_history_test(test: SessionHistoryTest) {
2✔
178
        let persister = InMemoryTestPersister::<SessionEvent>::default();
2✔
179
        for event in test.events {
4✔
180
            persister.save_event(event).expect("In memory persister shouldn't fail");
2✔
181
        }
2✔
182

183
        let (sender, session_history) =
2✔
184
            replay_event_log(&persister).expect("In memory persister shouldn't fail");
2✔
185
        assert_eq!(sender, test.expected_sender_state);
2✔
186
        assert_eq!(session_history.fallback_tx(), test.expected_session_history.fallback_tx);
2✔
187
        assert_eq!(*session_history.pj_param(), test.expected_session_history.pj_param);
2✔
188
    }
2✔
189

190
    #[test]
191
    fn test_sender_session_history_with_expired_session() {
1✔
192
        // TODO(armins): how can we reduce the boilerplate for these tests?
193
        let psbt = PARSED_ORIGINAL_PSBT.clone();
1✔
194
        let sender = SenderBuilder::new(
1✔
195
            psbt.clone(),
1✔
196
            Uri::try_from(PJ_URI)
1✔
197
                .expect("Valid uri")
1✔
198
                .assume_checked()
1✔
199
                .check_pj_supported()
1✔
200
                .expect("Payjoin to be supported"),
1✔
201
        )
202
        .build_recommended(FeeRate::BROADCAST_MIN)
1✔
203
        .unwrap();
1✔
204
        let reply_key = HpkeKeyPair::gen_keypair();
1✔
205
        let endpoint = sender.endpoint().clone();
1✔
206
        let fallback_tx = sender.psbt_ctx.original_psbt.clone().extract_tx_unchecked_fee_rate();
1✔
207
        let id = crate::uri::ShortId::try_from(&b"12345670"[..]).expect("valid short id");
1✔
208
        let pj_param = crate::uri::v2::PjParam::new(
1✔
209
            endpoint,
1✔
210
            id,
1✔
211
            std::time::SystemTime::now() - std::time::Duration::from_secs(1),
1✔
212
            crate::OhttpKeys(
1✔
213
                ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).expect("valid key config"),
1✔
214
            ),
1✔
215
            reply_key.1,
1✔
216
        );
217
        let with_reply_key = WithReplyKey {
1✔
218
            pj_param: pj_param.clone(),
1✔
219
            psbt_ctx: sender.psbt_ctx.clone(),
1✔
220
            reply_key: reply_key.0,
1✔
221
        };
1✔
222
        let test = SessionHistoryTest {
1✔
223
            events: vec![SessionEvent::CreatedReplyKey(with_reply_key)],
1✔
224
            expected_session_history: SessionHistoryExpectedOutcome { fallback_tx, pj_param },
1✔
225
            expected_sender_state: SendSession::TerminalFailure,
1✔
226
        };
1✔
227
        run_session_history_test(test);
1✔
228
    }
1✔
229

230
    #[test]
231
    fn test_sender_session_history_with_reply_key_event() {
1✔
232
        let psbt = PARSED_ORIGINAL_PSBT.clone();
1✔
233
        let sender = SenderBuilder::new(
1✔
234
            psbt.clone(),
1✔
235
            Uri::try_from(PJ_URI)
1✔
236
                .expect("Valid uri")
1✔
237
                .assume_checked()
1✔
238
                .check_pj_supported()
1✔
239
                .expect("Payjoin to be supported"),
1✔
240
        )
241
        .build_recommended(FeeRate::BROADCAST_MIN)
1✔
242
        .unwrap();
1✔
243
        let reply_key = HpkeKeyPair::gen_keypair();
1✔
244
        let endpoint = sender.endpoint().clone();
1✔
245
        let fallback_tx = sender.psbt_ctx.original_psbt.clone().extract_tx_unchecked_fee_rate();
1✔
246
        let id = crate::uri::ShortId::try_from(&b"12345670"[..]).expect("valid short id");
1✔
247
        let pj_param = crate::uri::v2::PjParam::new(
1✔
248
            endpoint,
1✔
249
            id,
1✔
250
            std::time::SystemTime::now() + std::time::Duration::from_secs(60),
1✔
251
            crate::OhttpKeys(
1✔
252
                ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).expect("valid key config"),
1✔
253
            ),
1✔
254
            HpkeKeyPair::gen_keypair().1,
1✔
255
        );
256
        let with_reply_key = WithReplyKey {
1✔
257
            pj_param: pj_param.clone(),
1✔
258
            psbt_ctx: sender.psbt_ctx.clone(),
1✔
259
            reply_key: reply_key.0,
1✔
260
        };
1✔
261
        let sender = Sender { state: with_reply_key.clone() };
1✔
262
        let test = SessionHistoryTest {
1✔
263
            events: vec![SessionEvent::CreatedReplyKey(with_reply_key)],
1✔
264
            expected_session_history: SessionHistoryExpectedOutcome { fallback_tx, pj_param },
1✔
265
            expected_sender_state: SendSession::WithReplyKey(sender),
1✔
266
        };
1✔
267
        run_session_history_test(test);
1✔
268
    }
1✔
269
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc