Skip to content

Commit c98d5bb

Browse files
authored
Sender Session Events (payjoin#777)
This PR adds session events generated by processing the sender state machine. To enable serialization and deserialization of session events, we implement `serde::Serialize` and `serde::Deserialize` on the underlying v1 & v2 types that are captured in session events. To enable comparisons between `SessionEvents` during tests, all event types must also implement `PartialEq` and `Eq`. Tests cover roundtrip ser/deserialization of each session event.
2 parents fe70e21 + 54302e9 commit c98d5bb

File tree

6 files changed

+108
-5
lines changed

6 files changed

+108
-5
lines changed

payjoin-ffi/src/receive/uni.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::uri::error::IntoUrlError;
1111
use crate::{ClientResponse, OhttpKeys, OutputSubstitution, Request};
1212

1313
#[derive(Clone, uniffi::Object, serde::Serialize, serde::Deserialize)]
14-
pub struct SessionEvent(super::SessionEvent);
14+
pub struct ReceiverSessionEvent(super::SessionEvent);
1515

1616
#[derive(Debug, uniffi::Object)]
1717
pub struct NewReceiver(pub super::NewReceiver);

payjoin-ffi/src/send/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ pub mod error;
1414
#[cfg(feature = "uniffi")]
1515
pub mod uni;
1616

17+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
18+
pub struct SessionEvent(payjoin::send::v2::SessionEvent);
19+
20+
impl From<SessionEvent> for payjoin::send::v2::SessionEvent {
21+
fn from(value: SessionEvent) -> Self { value.0 }
22+
}
23+
24+
impl From<payjoin::send::v2::SessionEvent> for SessionEvent {
25+
fn from(value: payjoin::send::v2::SessionEvent) -> Self { SessionEvent(value) }
26+
}
27+
1728
///Builder for sender-side payjoin parameters
1829
///
1930
///These parameters define how client wants to handle Payjoin.

payjoin-ffi/src/send/uni.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ pub use crate::send::{
66
};
77
use crate::{ClientResponse, ImplementationError, PjUri, Request};
88

9+
#[derive(uniffi::Object, Debug, Clone, serde::Serialize, serde::Deserialize)]
10+
pub struct SenderSessionEvent(super::SessionEvent);
11+
12+
impl From<SenderSessionEvent> for super::SessionEvent {
13+
fn from(value: SenderSessionEvent) -> Self { value.0 }
14+
}
15+
16+
impl From<super::SessionEvent> for SenderSessionEvent {
17+
fn from(value: super::SessionEvent) -> Self { SenderSessionEvent(value) }
18+
}
19+
20+
#[uniffi::export]
21+
impl SenderSessionEvent {
22+
pub fn to_json(&self) -> Result<String, SerdeJsonError> {
23+
serde_json::to_string(&self.0).map_err(Into::into)
24+
}
25+
26+
#[uniffi::constructor]
27+
pub fn from_json(json: String) -> Result<Self, SerdeJsonError> {
28+
let event: payjoin::send::v2::SessionEvent = serde_json::from_str(&json)?;
29+
Ok(SenderSessionEvent(event.into()))
30+
}
31+
}
32+
933
#[derive(uniffi::Object)]
1034
pub struct SenderBuilder(super::SenderBuilder);
1135

payjoin/src/send/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub(crate) struct AdditionalFeeContribution {
5252

5353
/// Data required to validate the response against the original PSBT.
5454
#[derive(Debug, Clone)]
55+
#[cfg_attr(feature = "v2", derive(serde::Serialize, serde::Deserialize, PartialEq, Eq))]
5556
pub struct PsbtContext {
5657
original_psbt: Psbt,
5758
output_substitution: OutputSubstitution,

payjoin/src/send/v2/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use bitcoin::hashes::{sha256, Hash};
2525
pub use error::{CreateRequestError, EncapsulationError};
2626
use error::{InternalCreateRequestError, InternalEncapsulationError};
2727
use ohttp::ClientResponse;
28-
pub use persist::SenderToken;
28+
pub use persist::{SenderToken, SessionEvent};
2929
use serde::{Deserialize, Serialize};
3030
use url::Url;
3131

@@ -163,7 +163,7 @@ impl NewSender {
163163

164164
/// A payjoin V2 sender, allowing the construction of a payjoin V2 request
165165
/// and the resulting [`V2PostContext`].
166-
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
166+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167167
pub struct WithReplyKey {
168168
/// The v1 Sender.
169169
pub(crate) v1: v1::Sender,
@@ -341,7 +341,7 @@ impl Sender<V2PostContext> {
341341
///
342342
/// This type is used to make a BIP77 GET request and process the response.
343343
/// Call [`Sender<V2GetContext>::process_response`] on it to continue the BIP77 flow.
344-
#[derive(Debug, Clone)]
344+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
345345
pub struct V2GetContext {
346346
/// The endpoint in the Payjoin URI
347347
pub(crate) endpoint: Url,
@@ -415,7 +415,7 @@ impl Sender<V2GetContext> {
415415
}
416416

417417
#[cfg(feature = "v2")]
418-
#[derive(Debug, Clone)]
418+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
419419
pub(crate) struct HpkeContext {
420420
pub(crate) receiver: HpkePublicKey,
421421
pub(crate) reply_pair: HpkeKeyPair,

payjoin/src/send/v2/persist.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use url::Url;
44

55
use super::{Sender, WithReplyKey};
66
use crate::persist::Value;
7+
use crate::send::v2::V2GetContext;
78

89
/// Opaque key type for the sender
910
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -26,3 +27,69 @@ impl Value for Sender<WithReplyKey> {
2627

2728
fn key(&self) -> Self::Key { SenderToken(self.endpoint().clone()) }
2829
}
30+
31+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
32+
pub enum SessionEvent {
33+
/// Sender was created with a HPKE key pair
34+
CreatedReplyKey(WithReplyKey),
35+
/// Sender POST'd the original PSBT, and waiting to receive a Proposal PSBT using GET context
36+
V2GetContext(V2GetContext),
37+
/// Sender received a Proposal PSBT
38+
ProposalReceived(bitcoin::Psbt),
39+
/// Invalid session
40+
SessionInvalid(String),
41+
}
42+
43+
#[cfg(test)]
44+
mod tests {
45+
use bitcoin::{FeeRate, ScriptBuf};
46+
use payjoin_test_utils::PARSED_ORIGINAL_PSBT;
47+
48+
use super::*;
49+
use crate::send::v2::HpkeContext;
50+
use crate::send::{v1, PsbtContext};
51+
use crate::{HpkeKeyPair, OutputSubstitution};
52+
53+
#[test]
54+
fn test_sender_session_event_serialization_roundtrip() {
55+
let endpoint = Url::parse("http://localhost:1234").expect("Valid URL");
56+
let keypair = HpkeKeyPair::gen_keypair();
57+
let sender_with_reply_key = WithReplyKey {
58+
v1: v1::Sender {
59+
psbt: PARSED_ORIGINAL_PSBT.clone(),
60+
endpoint: endpoint.clone(),
61+
output_substitution: OutputSubstitution::Enabled,
62+
fee_contribution: None,
63+
min_fee_rate: FeeRate::ZERO,
64+
payee: ScriptBuf::from(vec![0x00]),
65+
},
66+
reply_key: keypair.0.clone(),
67+
};
68+
69+
let v2_get_context = V2GetContext {
70+
endpoint,
71+
psbt_ctx: PsbtContext {
72+
original_psbt: PARSED_ORIGINAL_PSBT.clone(),
73+
output_substitution: OutputSubstitution::Enabled,
74+
fee_contribution: None,
75+
min_fee_rate: FeeRate::ZERO,
76+
payee: ScriptBuf::from(vec![0x00]),
77+
},
78+
hpke_ctx: HpkeContext { receiver: keypair.clone().1, reply_pair: keypair },
79+
};
80+
81+
let test_cases = vec![
82+
SessionEvent::CreatedReplyKey(sender_with_reply_key.clone()),
83+
SessionEvent::V2GetContext(v2_get_context.clone()),
84+
SessionEvent::ProposalReceived(PARSED_ORIGINAL_PSBT.clone()),
85+
SessionEvent::SessionInvalid("error message".to_string()),
86+
];
87+
88+
for event in test_cases {
89+
let serialized = serde_json::to_string(&event).expect("Should serialize");
90+
let deserialized: SessionEvent =
91+
serde_json::from_str(&serialized).expect("Should deserialize");
92+
assert_eq!(event, deserialized);
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)