diff --git a/crates/cow-venue/src/adapter.rs b/crates/cow-venue/src/adapter.rs index 8f30e462..018f7717 100644 --- a/crates/cow-venue/src/adapter.rs +++ b/crates/cow-venue/src/adapter.rs @@ -217,9 +217,7 @@ fn reconciled_uid( ) -> Result { let derived = assembly::order_uid(config.chain, order, owner); if server != derived { - return Err(VenueError::InvalidBody(format!( - "orderbook uid {server} disagrees with derived uid {derived}" - ))); + return Err(VenueError::ReceiptMismatch); } Ok(server) } @@ -230,8 +228,7 @@ pub(crate) fn status_with( config: &AdapterConfig, receipt: &[u8], ) -> Result { - let uid = OrderUid::try_from(receipt) - .map_err(|_| VenueError::InvalidBody("receipt is not a 56-byte order uid".to_owned()))?; + let uid = OrderUid::try_from(receipt).map_err(|_| VenueError::InvalidReceipt)?; let url = join(config, &format!("api/v1/orders/{uid}"))?; let response = call(fetch, http::Method::GET, url, None)?; if response.status() == http::StatusCode::NOT_FOUND { @@ -735,7 +732,7 @@ mod tests { assert!(matches!( submit_with(&fetch, &config, &signed_bytes()), - Err(VenueError::InvalidBody(detail)) if detail.contains("disagrees") + Err(VenueError::ReceiptMismatch) )); } @@ -903,7 +900,7 @@ mod tests { let fetch = MockFetch::default(); assert!(matches!( status_with(&fetch, &config, &[0xAB; 3]), - Err(VenueError::InvalidBody(_)) + Err(VenueError::InvalidReceipt) )); let uid = OrderUid([0xAB; 56]); diff --git a/crates/videre-host/src/bindings.rs b/crates/videre-host/src/bindings.rs index 06e2b887..3eea4883 100644 --- a/crates/videre-host/src/bindings.rs +++ b/crates/videre-host/src/bindings.rs @@ -93,6 +93,8 @@ pub(crate) fn venue_error_message(err: &VenueError) -> std::borrow::Cow<'_, str> }, VenueError::Unavailable(detail) => Cow::Owned(format!("unavailable: {detail}")), VenueError::Timeout => Cow::Borrowed("timeout"), + VenueError::InvalidReceipt => Cow::Borrowed("invalid receipt"), + VenueError::ReceiptMismatch => Cow::Borrowed("receipt mismatch"), } } diff --git a/crates/videre-sdk/src/adapter.rs b/crates/videre-sdk/src/adapter.rs index f48067fe..28d4451b 100644 --- a/crates/videre-sdk/src/adapter.rs +++ b/crates/videre-sdk/src/adapter.rs @@ -10,12 +10,12 @@ use crate::{Config, Fault, IntentHeader, IntentStatus, Quotation, SubmitOutcome, VenueError}; -/// Reject an empty receipt as `invalid-body` before it reaches an +/// Reject an empty receipt as `invalid-receipt` 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())); + return Err(VenueError::InvalidReceipt); } Ok(()) } @@ -56,14 +56,14 @@ pub trait VenueAdapter { fn submit(body: Vec) -> Result; /// Report where a previously submitted intent is in its life. The - /// export shim rejects an empty receipt as `invalid-body` before + /// export shim rejects an empty receipt as `invalid-receipt` 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. The export shim rejects an empty receipt - /// as `invalid-body` before dispatch. + /// as `invalid-receipt` before dispatch. fn cancel(receipt: Vec) -> Result<(), VenueError>; } @@ -135,10 +135,10 @@ mod tests { use super::*; #[test] - fn empty_receipt_is_rejected_as_invalid_body() { + fn empty_receipt_is_rejected_as_invalid_receipt() { match guard_receipt(&[]).unwrap_err() { - VenueError::InvalidBody(detail) => assert_eq!(detail, "empty receipt"), - other => panic!("expected invalid-body, got {other:?}"), + VenueError::InvalidReceipt => {} + other => panic!("expected invalid-receipt, got {other:?}"), } guard_receipt(&[1]).unwrap(); } diff --git a/crates/videre-sdk/src/client.rs b/crates/videre-sdk/src/client.rs index bbc8cc0c..cc87a19b 100644 --- a/crates/videre-sdk/src/client.rs +++ b/crates/videre-sdk/src/client.rs @@ -290,14 +290,14 @@ impl VenueClient { } /// Report where a previously submitted intent is in its life. - /// Rejects an empty receipt as `invalid-body` before the wire. + /// Rejects an empty receipt as `invalid-receipt` 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. Rejects an empty - /// receipt as `invalid-body` before the wire. + /// receipt as `invalid-receipt` 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/src/faults.rs b/crates/videre-sdk/src/faults.rs index ac57203f..a4516a55 100644 --- a/crates/videre-sdk/src/faults.rs +++ b/crates/videre-sdk/src/faults.rs @@ -51,6 +51,13 @@ pub enum VenueFault { /// The call timed out. #[error("timeout")] Timeout, + /// The receipt is empty or structurally invalid. + #[error("invalid receipt")] + InvalidReceipt, + /// The venue-returned identifier disagrees with the locally derived + /// one. + #[error("receipt mismatch")] + ReceiptMismatch, } /// Lift the wire error into the owned mirror. Exhaustive: the wire enum @@ -67,6 +74,8 @@ impl From for VenueFault { }, VenueError::Unavailable(s) => Self::Unavailable(s), VenueError::Timeout => Self::Timeout, + VenueError::InvalidReceipt => Self::InvalidReceipt, + VenueError::ReceiptMismatch => Self::ReceiptMismatch, } } } @@ -132,8 +141,10 @@ impl From for VenueError { } /// Fold a typed client failure into the SDK-neutral fault a keeper -/// handler returns: an encode failure and a misnamed venue are the -/// caller's `invalid-input`; venue refusals map structurally. +/// handler returns: an encode failure, a misnamed venue, and an invalid +/// receipt are the caller's `invalid-input`; a receipt mismatch is +/// `internal` (a venue integrity failure, not the caller's); other venue +/// refusals map structurally. impl From for host::Fault { fn from(err: ClientError) -> Self { match err { @@ -148,6 +159,8 @@ impl From for host::Fault { } VenueFault::Unavailable(s) => host::Fault::Unavailable(s), VenueFault::Timeout => host::Fault::Timeout, + VenueFault::InvalidReceipt => host::Fault::InvalidInput(fault.to_string()), + VenueFault::ReceiptMismatch => host::Fault::Internal(fault.to_string()), }, } } diff --git a/crates/videre-sdk/src/keeper.rs b/crates/videre-sdk/src/keeper.rs index 084d2959..b066e0fc 100644 --- a/crates/videre-sdk/src/keeper.rs +++ b/crates/videre-sdk/src/keeper.rs @@ -350,7 +350,9 @@ pub fn retry_action(fault: &VenueFault) -> RetryAction { VenueFault::UnknownVenue | VenueFault::InvalidBody(_) | VenueFault::Unsupported - | VenueFault::Denied(_) => RetryAction::Drop, + | VenueFault::Denied(_) + | VenueFault::InvalidReceipt + | VenueFault::ReceiptMismatch => RetryAction::Drop, } } diff --git a/crates/videre-sdk/tests/adapter.rs b/crates/videre-sdk/tests/adapter.rs index fd324f0d..f50b108f 100644 --- a/crates/videre-sdk/tests/adapter.rs +++ b/crates/videre-sdk/tests/adapter.rs @@ -309,16 +309,16 @@ fn quote_typestate_prices_then_submits_the_quoted_body() { #[test] fn empty_receipt_is_rejected_before_the_transport() { - // The unbound venue would report unknown-venue, so invalid-body + // The unbound venue would report unknown-venue, so invalid-receipt // 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" + ClientError::Venue(VenueFault::InvalidReceipt) )); assert!(matches!( run(client.cancel(&[])).unwrap_err(), - ClientError::Venue(VenueFault::InvalidBody(detail)) if detail == "empty receipt" + ClientError::Venue(VenueFault::InvalidReceipt) )); } diff --git a/wit/videre-types/types.wit b/wit/videre-types/types.wit index e8b69c81..d77d5f61 100644 --- a/wit/videre-types/types.wit +++ b/wit/videre-types/types.wit @@ -67,6 +67,10 @@ interface types { rate-limited(rate-limit), unavailable(string), timeout, + /// An empty or structurally invalid receipt. + invalid-receipt, + /// The venue-returned identifier disagrees with the locally derived one. + receipt-mismatch, } record rate-limit {