From 17252da8fbea3ea9262dd40df2df1109cbaf901b Mon Sep 17 00:00:00 2001 From: mfw78 Date: Fri, 24 Jul 2026 14:09:09 +0000 Subject: [PATCH] venue: restore the dropped empty-receipt guard on status and cancel The videre rename dropped the empty-receipt rejection on the status and cancel paths, so any receipt including empty succeeded unconditionally. Restore the guard at the receipt seam: the venue export shim rejects an empty receipt before adapter dispatch and VenueClient::status/cancel reject it before the wire, both through a shared guard_receipt helper. The original check used a dedicated invalid-receipt variant; that would extend the frozen videre:types@0.1.0 venue-error, so the guard reuses the existing invalid-body case with an empty-receipt message. A unit test and a before-the-transport client test restore the dropped coverage. --- crates/videre-sdk/src/adapter.rs | 33 ++++++++++++++++++++++++++++-- crates/videre-sdk/src/client.rs | 6 +++++- crates/videre-sdk/tests/adapter.rs | 15 ++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) 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);