Skip to content

Commit 5e28e3e

Browse files
committed
Remove extract_err_req and process_err_res
Previously extract_err_req was exposed as a public method on SessionHistory, and process_err_res as a pure function, for implementers to manually process error responses. Now this can and should be done via the HasReplyableError typestate, so these functions are no longer needed.
1 parent a905fe9 commit 5e28e3e

4 files changed

Lines changed: 54 additions & 193 deletions

File tree

payjoin-ffi/src/receive/mod.rs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -159,22 +159,6 @@ impl SessionHistory {
159159
pub fn fallback_tx(&self) -> Option<Arc<crate::Transaction>> {
160160
self.0.fallback_tx().map(|tx| Arc::new(tx.into()))
161161
}
162-
163-
/// Construct the error request to be posted on the directory if an error occurred.
164-
/// To process the response, use [process_err_res]
165-
pub fn extract_err_req(
166-
&self,
167-
ohttp_relay: String,
168-
) -> Result<Option<RequestResponse>, SessionError> {
169-
match self.0.extract_err_req(ohttp_relay) {
170-
Ok(Some((request, ctx))) => Ok(Some(RequestResponse {
171-
request: request.into(),
172-
client_response: Arc::new(ctx.into()),
173-
})),
174-
Ok(None) => Ok(None),
175-
Err(e) => Err(SessionError::from(e)),
176-
}
177-
}
178162
}
179163

180164
#[derive(uniffi::Object)]
@@ -475,12 +459,6 @@ impl UncheckedOriginalPayload {
475459
}
476460
}
477461

478-
/// Process an OHTTP Encapsulated HTTP POST Error response
479-
/// to ensure it has been posted properly
480-
#[uniffi::export]
481-
pub fn process_err_res(body: &[u8], context: &ClientResponse) -> Result<(), SessionError> {
482-
payjoin::receive::v2::process_err_res(body, context.into()).map_err(Into::into)
483-
}
484462
#[derive(Clone, uniffi::Object)]
485463
pub struct MaybeInputsOwned(payjoin::receive::v2::Receiver<payjoin::receive::v2::MaybeInputsOwned>);
486464

payjoin/src/core/receive/v2/mod.rs

Lines changed: 30 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub(crate) use error::InternalSessionError;
3434
pub use error::SessionError;
3535
use serde::de::Deserializer;
3636
use serde::{Deserialize, Serialize};
37-
pub use session::{replay_event_log, SessionEvent, SessionHistory};
37+
pub use session::{replay_event_log, SessionEvent, SessionHistory, SessionStatus};
3838
use url::Url;
3939

4040
use super::error::{Error, InputContributionError};
@@ -270,34 +270,6 @@ impl<State> core::ops::DerefMut for Receiver<State> {
270270
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.state }
271271
}
272272

273-
/// Construct an OHTTP Encapsulated HTTP POST request to return
274-
/// a Receiver Error Response
275-
fn extract_err_req(
276-
err: &JsonReply,
277-
ohttp_relay: impl IntoUrl,
278-
session_context: &SessionContext,
279-
) -> Result<(Request, ohttp::ClientResponse), SessionError> {
280-
if session_context.expiration.elapsed() {
281-
return Err(InternalSessionError::Expired(session_context.expiration).into());
282-
}
283-
let mailbox = mailbox_endpoint(&session_context.directory, &session_context.reply_mailbox_id());
284-
let (body, ohttp_ctx) = ohttp_encapsulate(
285-
&session_context.ohttp_keys.0,
286-
"POST",
287-
mailbox.as_str(),
288-
Some(err.to_json().to_string().as_bytes()),
289-
)
290-
.map_err(InternalSessionError::OhttpEncapsulation)?;
291-
let req = Request::new_v2(&session_context.full_relay_url(ohttp_relay)?, &body);
292-
Ok((req, ohttp_ctx))
293-
}
294-
295-
/// Process an OHTTP Encapsulated HTTP POST Error response
296-
/// to ensure it has been posted properly
297-
pub fn process_err_res(body: &[u8], context: ohttp::ClientResponse) -> Result<(), SessionError> {
298-
process_post_res(body, context).map_err(|e| InternalSessionError::DirectoryResponse(e).into())
299-
}
300-
301273
#[derive(Debug, Clone)]
302274
pub struct ReceiverBuilder(SessionContext);
303275

@@ -1106,13 +1078,31 @@ pub struct HasReplyableError {
11061078
}
11071079

11081080
impl Receiver<HasReplyableError> {
1081+
/// Construct an OHTTP Encapsulated HTTP POST request to return
1082+
/// a Receiver Error Response
11091083
pub fn create_error_request(
11101084
&self,
11111085
ohttp_relay: impl IntoUrl,
11121086
) -> Result<(Request, ohttp::ClientResponse), SessionError> {
1113-
extract_err_req(&self.error_reply, ohttp_relay, &self.session_context)
1087+
let session_context = &self.session_context;
1088+
if session_context.expiration.elapsed() {
1089+
return Err(InternalSessionError::Expired(session_context.expiration).into());
1090+
}
1091+
let mailbox =
1092+
mailbox_endpoint(&session_context.directory, &session_context.reply_mailbox_id());
1093+
let (body, ohttp_ctx) = ohttp_encapsulate(
1094+
&session_context.ohttp_keys.0,
1095+
"POST",
1096+
mailbox.as_str(),
1097+
Some(self.error_reply.to_json().to_string().as_bytes()),
1098+
)
1099+
.map_err(InternalSessionError::OhttpEncapsulation)?;
1100+
let req = Request::new_v2(&session_context.full_relay_url(ohttp_relay)?, &body);
1101+
Ok((req, ohttp_ctx))
11141102
}
11151103

1104+
/// Process an OHTTP Encapsulated HTTP POST Error response
1105+
/// to ensure it has been posted properly
11161106
pub fn process_error_response(
11171107
&self,
11181108
res: &[u8],
@@ -1422,11 +1412,7 @@ pub mod test {
14221412
}
14231413

14241414
#[test]
1425-
fn test_extract_err_req() -> Result<(), BoxError> {
1426-
let receiver = Receiver {
1427-
state: unchecked_proposal_v2_from_test_vector(),
1428-
session_context: SHARED_CONTEXT.clone(),
1429-
};
1415+
fn test_create_error_request() -> Result<(), BoxError> {
14301416
let mock_err = mock_err();
14311417
let expected_json = serde_json::json!({
14321418
"errorCode": "unavailable",
@@ -1435,38 +1421,26 @@ pub mod test {
14351421

14361422
assert_eq!(mock_err.to_json(), expected_json);
14371423

1438-
let (_req, _ctx) = extract_err_req(&mock_err, EXAMPLE_URL, &receiver.session_context)?;
1424+
let receiver = Receiver {
1425+
state: HasReplyableError { error_reply: mock_err.clone() },
1426+
session_context: SHARED_CONTEXT.clone(),
1427+
};
1428+
1429+
let (_req, _ctx) = receiver.create_error_request(EXAMPLE_URL)?;
14391430

1440-
let internal_error: Error = InternalPayloadError::MissingPayment.into();
1441-
let (_req, _ctx) =
1442-
extract_err_req(&(&internal_error).into(), EXAMPLE_URL, &receiver.session_context)?;
14431431
Ok(())
14441432
}
14451433

14461434
#[test]
1447-
fn test_extract_err_req_expiration() -> Result<(), BoxError> {
1435+
fn test_create_error_request_expiration() -> Result<(), BoxError> {
14481436
let now = crate::time::Time::now();
1449-
let noop_persister = NoopSessionPersister::default();
14501437
let context = SessionContext { expiration: now, ..SHARED_CONTEXT.clone() };
14511438
let receiver = Receiver {
1452-
state: UncheckedOriginalPayload {
1453-
original: crate::receive::tests::original_from_test_vector(),
1454-
},
1439+
state: HasReplyableError { error_reply: mock_err() },
14551440
session_context: context.clone(),
14561441
};
14571442

1458-
let server_error = || {
1459-
receiver
1460-
.clone()
1461-
.check_broadcast_suitability(None, |_| Err("mock error".into()))
1462-
.save(&noop_persister)
1463-
};
1464-
1465-
let error = server_error().expect_err("Server error should be populated with mock error");
1466-
let res = error.api_error().expect("check_broadcast error should propagate to api error");
1467-
let actual_json = JsonReply::from(&res);
1468-
1469-
let expiration = extract_err_req(&actual_json, EXAMPLE_URL, &context);
1443+
let expiration = receiver.create_error_request(EXAMPLE_URL);
14701444

14711445
match expiration {
14721446
Err(error) => assert_eq!(

payjoin/src/core/receive/v2/session.rs

Lines changed: 1 addition & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ use super::{ReceiveSession, SessionContext};
44
use crate::error::{InternalReplayError, ReplayError};
55
use crate::output_substitution::OutputSubstitution;
66
use crate::persist::SessionPersister;
7-
use crate::receive::v2::{extract_err_req, SessionError};
87
use crate::receive::{InputPair, JsonReply, OriginalPayload, PsbtContext};
9-
use crate::{ImplementationError, IntoUrl, PjUri, Request};
8+
use crate::{ImplementationError, PjUri};
109

1110
/// Replay a receiver event log to get the receiver in its current state [ReceiveSession]
1211
/// and a session history [SessionHistory]
@@ -112,35 +111,6 @@ impl SessionHistory {
112111
})
113112
}
114113

115-
/// Construct the error request to be posted on the directory if an error occurred.
116-
/// To process the response, use [crate::receive::v2::process_err_res]
117-
pub fn extract_err_req(
118-
&self,
119-
ohttp_relay: impl IntoUrl,
120-
) -> Result<Option<(Request, ohttp::ClientResponse)>, SessionError> {
121-
// FIXME ideally this should be more like a method of
122-
// Receiver<UncheckedOriginalPayload> and subsequent states instead of the
123-
// history as a whole since it doesn't make sense to call it before,
124-
// reaching that state.
125-
if !self.received_sender_proposal() {
126-
return Ok(None);
127-
}
128-
129-
let session_context = self.session_context();
130-
let json_reply = match self.terminal_error() {
131-
Some(json_reply) => json_reply,
132-
_ => return Ok(None),
133-
};
134-
let (req, ctx) = extract_err_req(&json_reply, ohttp_relay, &session_context)?;
135-
Ok(Some((req, ctx)))
136-
}
137-
138-
fn received_sender_proposal(&self) -> bool {
139-
self.events
140-
.iter()
141-
.any(|event| matches!(event, SessionEvent::RetrievedOriginalPayload { .. }))
142-
}
143-
144114
fn session_context(&self) -> SessionContext {
145115
let mut initial_session_context = self
146116
.events
@@ -703,78 +673,4 @@ mod tests {
703673

704674
Ok(())
705675
}
706-
707-
#[test]
708-
fn test_skipped_session_extract_err_request() -> Result<(), BoxError> {
709-
let ohttp_relay = EXAMPLE_URL;
710-
let mock_err = mock_err();
711-
712-
let session_history =
713-
SessionHistory { events: vec![SessionEvent::CheckedBroadcastSuitability()] };
714-
let err_req = session_history.extract_err_req(ohttp_relay)?;
715-
assert!(err_req.is_none());
716-
717-
let session_history = SessionHistory {
718-
events: vec![
719-
SessionEvent::CheckedBroadcastSuitability(),
720-
SessionEvent::GotReplyableError(mock_err.clone()),
721-
],
722-
};
723-
724-
let err_req = session_history.extract_err_req(ohttp_relay)?;
725-
assert!(err_req.is_none());
726-
727-
let session_history = SessionHistory {
728-
events: vec![
729-
SessionEvent::Created(SHARED_CONTEXT.clone()),
730-
SessionEvent::CheckedBroadcastSuitability(),
731-
SessionEvent::GotReplyableError(mock_err.clone()),
732-
],
733-
};
734-
735-
let err_req = session_history.extract_err_req(ohttp_relay)?;
736-
assert!(err_req.is_none());
737-
Ok(())
738-
}
739-
740-
#[test]
741-
fn test_session_extract_err_req_reply_key() -> Result<(), BoxError> {
742-
let proposal = original_from_test_vector();
743-
let ohttp_relay = EXAMPLE_URL;
744-
let mock_err = mock_err();
745-
746-
let session_history_one = SessionHistory {
747-
events: vec![
748-
SessionEvent::Created(SHARED_CONTEXT.clone()),
749-
SessionEvent::RetrievedOriginalPayload {
750-
original: proposal.clone(),
751-
reply_key: Some(crate::HpkeKeyPair::gen_keypair().1),
752-
},
753-
SessionEvent::GotReplyableError(mock_err.clone()),
754-
],
755-
};
756-
757-
let err_req_one = session_history_one.extract_err_req(ohttp_relay)?;
758-
assert!(err_req_one.is_some());
759-
760-
let session_history_two = SessionHistory {
761-
events: vec![
762-
SessionEvent::Created(SHARED_CONTEXT.clone()),
763-
SessionEvent::RetrievedOriginalPayload {
764-
original: proposal.clone(),
765-
reply_key: Some(crate::HpkeKeyPair::gen_keypair().1),
766-
},
767-
SessionEvent::GotReplyableError(mock_err.clone()),
768-
],
769-
};
770-
771-
let err_req_two = session_history_two.extract_err_req(ohttp_relay)?;
772-
assert!(err_req_two.is_some());
773-
assert_ne!(
774-
session_history_one.session_context().reply_key,
775-
session_history_two.session_context().reply_key
776-
);
777-
778-
Ok(())
779-
}
780676
}

payjoin/tests/integration.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ mod integration {
198198
use http::StatusCode;
199199
use payjoin::persist::{NoopSessionPersister, OptionalTransitionOutcome};
200200
use payjoin::receive::v2::{
201-
replay_event_log as replay_receiver_event_log, PayjoinProposal, Receiver,
202-
ReceiverBuilder, UncheckedOriginalPayload,
201+
replay_event_log as replay_receiver_event_log, PayjoinProposal, ReceiveSession,
202+
Receiver, ReceiverBuilder, SessionStatus, UncheckedOriginalPayload,
203203
};
204204
use payjoin::send::v2::SenderBuilder;
205205
use payjoin::{OhttpKeys, PjUri, UriExt};
@@ -317,12 +317,12 @@ mod integration {
317317
let result = tokio::select!(
318318
err = services.take_ohttp_relay_handle() => panic!("Ohttp relay exited early: {:?}", err),
319319
err = services.take_directory_handle() => panic!("Directory server exited early: {:?}", err),
320-
res = process_err_res(&services) => res
320+
res = do_err_test(&services) => res
321321
);
322322

323323
assert!(result.is_ok(), "v2 send receive failed: {:#?}", result.unwrap_err());
324324

325-
async fn process_err_res(services: &TestServices) -> Result<(), BoxError> {
325+
async fn do_err_test(services: &TestServices) -> Result<(), BoxError> {
326326
let (_bitcoind, sender, receiver) = init_bitcoind_sender_receiver(None, None)?;
327327
let agent = services.http_agent();
328328
services.wait_for_services_ready().await?;
@@ -419,10 +419,14 @@ mod integration {
419419
"Protocol error: Can't broadcast. PSBT rejected by mempool."
420420
);
421421

422-
let (_, session_history) = replay_receiver_event_log(&persister)?;
423-
let (err_req, err_ctx) = session_history
424-
.extract_err_req(services.ohttp_relay_url().as_str())?
425-
.expect("error request should exist");
422+
let (session, session_history) = replay_receiver_event_log(&persister)?;
423+
assert_eq!(session_history.status(), SessionStatus::Active);
424+
let has_error = match session {
425+
ReceiveSession::HasReplyableError(r) => r,
426+
_ => panic!("Expected HasError"),
427+
};
428+
let (err_req, err_ctx) =
429+
has_error.create_error_request(services.ohttp_relay_url().as_str())?;
426430
let err_response = agent
427431
.post(err_req.url)
428432
.header("Content-Type", err_req.content_type)
@@ -431,8 +435,17 @@ mod integration {
431435
.await?;
432436

433437
let err_bytes = err_response.bytes().await?;
434-
// Ensure that the error was handled properly
435-
assert!(payjoin::receive::v2::process_err_res(&err_bytes, err_ctx).is_ok());
438+
has_error.process_error_response(&err_bytes, err_ctx).save(&persister)?;
439+
440+
// Ensure the session is closed properly
441+
let (_, session_history) = replay_receiver_event_log(&persister)?;
442+
assert_eq!(session_history.status(), SessionStatus::Failed);
443+
assert_eq!(
444+
session_history.terminal_error().expect("should have error"),
445+
(&server_error).into()
446+
);
447+
448+
// TODO: Sender should retrieve the error response to complete the error flow
436449

437450
Ok(())
438451
}

0 commit comments

Comments
 (0)