diff --git a/crates/videre-sdk/src/adapter.rs b/crates/videre-sdk/src/adapter.rs index 0e6f3edb..f48067fe 100644 --- a/crates/videre-sdk/src/adapter.rs +++ b/crates/videre-sdk/src/adapter.rs @@ -10,6 +10,16 @@ use crate::{Config, Fault, IntentHeader, IntentStatus, Quotation, SubmitOutcome, VenueError}; +/// Reject an empty receipt as `invalid-body` before it reaches an +/// adapter. Called by the export shim ahead of `status` and `cancel`. +#[doc(hidden)] +pub fn guard_receipt(receipt: &[u8]) -> Result<(), VenueError> { + if receipt.is_empty() { + return Err(VenueError::InvalidBody("empty receipt".into())); + } + Ok(()) +} + /// One venue's protocol speaker: the guest-side face of the /// `venue-adapter` world. Implement it on a unit struct and apply /// [`#[videre_sdk::venue]`](crate::venue) to the impl; bodies and @@ -45,12 +55,15 @@ pub trait VenueAdapter { /// the host must sign and send before the intent exists. fn submit(body: Vec) -> Result; - /// Report where a previously submitted intent is in its life. + /// Report where a previously submitted intent is in its life. The + /// export shim rejects an empty receipt as `invalid-body` before + /// dispatch. fn status(receipt: Vec) -> Result; /// Ask the venue to withdraw an intent. Success means the venue /// accepted the cancellation, not that an in-flight settlement can - /// no longer win the race. + /// no longer win the race. The export shim rejects an empty receipt + /// as `invalid-body` before dispatch. fn cancel(receipt: Vec) -> Result<(), VenueError>; } @@ -101,12 +114,14 @@ macro_rules! __export_venue_adapter { fn status( receipt: ::std::vec::Vec, ) -> ::core::result::Result<$crate::IntentStatus, $crate::VenueError> { + $crate::adapter::guard_receipt(&receipt)?; <$adapter as $crate::VenueAdapter>::status(receipt) } fn cancel( receipt: ::std::vec::Vec, ) -> ::core::result::Result<(), $crate::VenueError> { + $crate::adapter::guard_receipt(&receipt)?; <$adapter as $crate::VenueAdapter>::cancel(receipt) } } @@ -114,3 +129,17 @@ macro_rules! __export_venue_adapter { export!(__VidereVenueAdapterExport); }; } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_receipt_is_rejected_as_invalid_body() { + match guard_receipt(&[]).unwrap_err() { + VenueError::InvalidBody(detail) => assert_eq!(detail, "empty receipt"), + other => panic!("expected invalid-body, got {other:?}"), + } + guard_receipt(&[1]).unwrap(); + } +} diff --git a/crates/videre-sdk/src/client.rs b/crates/videre-sdk/src/client.rs index f3384d44..bbc8cc0c 100644 --- a/crates/videre-sdk/src/client.rs +++ b/crates/videre-sdk/src/client.rs @@ -290,12 +290,16 @@ impl VenueClient { } /// Report where a previously submitted intent is in its life. + /// Rejects an empty receipt as `invalid-body` before the wire. pub async fn status(&self, receipt: &[u8]) -> Result { + crate::adapter::guard_receipt(receipt).map_err(VenueFault::from)?; Ok(self.transport.status(&V::ID, receipt).await?) } - /// Ask the bound venue to withdraw an intent. + /// Ask the bound venue to withdraw an intent. Rejects an empty + /// receipt as `invalid-body` before the wire. pub async fn cancel(&self, receipt: &[u8]) -> Result<(), ClientError> { + crate::adapter::guard_receipt(receipt).map_err(VenueFault::from)?; Ok(self.transport.cancel(&V::ID, receipt).await?) } } diff --git a/crates/videre-sdk/tests/adapter.rs b/crates/videre-sdk/tests/adapter.rs index d43cd252..fd324f0d 100644 --- a/crates/videre-sdk/tests/adapter.rs +++ b/crates/videre-sdk/tests/adapter.rs @@ -307,6 +307,21 @@ fn quote_typestate_prices_then_submits_the_quoted_body() { assert!(matches!(outcome, SubmitOutcome::Accepted(r) if r == RECEIPT.to_vec())); } +#[test] +fn empty_receipt_is_rejected_before_the_transport() { + // The unbound venue would report unknown-venue, so invalid-body + // proves the guard fires before the transport is consulted. + let client = VenueClient::::with_transport(InProcessClient); + assert!(matches!( + run(client.status(&[])).unwrap_err(), + ClientError::Venue(VenueFault::InvalidBody(detail)) if detail == "empty receipt" + )); + assert!(matches!( + run(client.cancel(&[])).unwrap_err(), + ClientError::Venue(VenueFault::InvalidBody(detail)) if detail == "empty receipt" + )); +} + #[test] fn unbound_venue_is_unknown_at_the_client() { let client = VenueClient::::with_transport(InProcessClient);