Skip to content

Commit fa845c8

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 58c4143 commit fa845c8

4 files changed

Lines changed: 54 additions & 196 deletions

File tree

payjoin-ffi/src/receive/mod.rs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -166,22 +166,6 @@ impl SessionHistory {
166166
pub fn fallback_tx(&self) -> Option<Arc<crate::Transaction>> {
167167
self.0.fallback_tx().map(|tx| Arc::new(tx.into()))
168168
}
169-
170-
/// Construct the error request to be posted on the directory if an error occurred.
171-
/// To process the response, use [process_err_res]
172-
pub fn extract_err_req(
173-
&self,
174-
ohttp_relay: String,
175-
) -> Result<Option<RequestResponse>, SessionError> {
176-
match self.0.extract_err_req(ohttp_relay) {
177-
Ok(Some((request, ctx))) => Ok(Some(RequestResponse {
178-
request: request.into(),
179-
client_response: Arc::new(ctx.into()),
180-
})),
181-
Ok(None) => Ok(None),
182-
Err(e) => Err(SessionError::from(e)),
183-
}
184-
}
185169
}
186170

187171
#[derive(uniffi::Object)]
@@ -488,12 +472,6 @@ impl UncheckedOriginalPayload {
488472
}
489473
}
490474

491-
/// Process an OHTTP Encapsulated HTTP POST Error response
492-
/// to ensure it has been posted properly
493-
#[uniffi::export]
494-
pub fn process_err_res(body: &[u8], context: &ClientResponse) -> Result<(), SessionError> {
495-
payjoin::receive::v2::process_err_res(body, context.into()).map_err(Into::into)
496-
}
497475
#[derive(Clone, uniffi::Object)]
498476
pub struct MaybeInputsOwned(payjoin::receive::v2::Receiver<payjoin::receive::v2::MaybeInputsOwned>);
499477

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

Lines changed: 30 additions & 60 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};
@@ -267,34 +267,6 @@ impl<State> core::ops::DerefMut for Receiver<State> {
267267
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.state }
268268
}
269269

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

@@ -1104,13 +1076,31 @@ pub struct HasReplyableError {
11041076
}
11051077

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

1102+
/// Process an OHTTP Encapsulated HTTP POST Error response
1103+
/// to ensure it has been posted properly
11141104
pub fn process_error_response(
11151105
&self,
11161106
res: &[u8],
@@ -1421,11 +1411,7 @@ pub mod test {
14211411
}
14221412

14231413
#[test]
1424-
fn test_extract_err_req() -> Result<(), BoxError> {
1425-
let receiver = Receiver {
1426-
state: unchecked_proposal_v2_from_test_vector(),
1427-
session_context: SHARED_CONTEXT.clone(),
1428-
};
1414+
fn test_create_error_request() -> Result<(), BoxError> {
14291415
let mock_err = mock_err();
14301416
let expected_json = serde_json::json!({
14311417
"errorCode": "unavailable",
@@ -1434,42 +1420,26 @@ pub mod test {
14341420

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

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

1440-
let internal_error: Error = InternalPayloadError::MissingPayment.into();
1441-
let (_req, _ctx) = extract_err_req(
1442-
&(&internal_error).into(),
1443-
EXAMPLE_URL.as_str(),
1444-
&receiver.session_context,
1445-
)?;
14461430
Ok(())
14471431
}
14481432

14491433
#[test]
1450-
fn test_extract_err_req_expiration() -> Result<(), BoxError> {
1434+
fn test_create_error_request_expiration() -> Result<(), BoxError> {
14511435
let now = crate::time::Time::now();
1452-
let noop_persister = NoopSessionPersister::default();
14531436
let context = SessionContext { expiration: now, ..SHARED_CONTEXT.clone() };
14541437
let receiver = Receiver {
1455-
state: UncheckedOriginalPayload {
1456-
original: crate::receive::tests::original_from_test_vector(),
1457-
},
1438+
state: HasReplyableError { error_reply: mock_err() },
14581439
session_context: context.clone(),
14591440
};
14601441

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

14741444
match expiration {
14751445
Err(error) => assert_eq!(

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

Lines changed: 1 addition & 104 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::{SessionEventTrait, SessionPersister};
7-
use crate::receive::v2::{extract_err_req, SessionError};
87
use crate::receive::{common, 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]
@@ -113,35 +112,6 @@ impl SessionHistory {
113112
})
114113
}
115114

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

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

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)